Здравствуйте, многоуважаемые коллеги.
Возможно, вы не знаете, но каждый день, каждый час, каждую секунду мы ведем войну за память устройств. Для кого-то эта война незаметна, кто-то не придает ей значение, кто-то воюет по-старинке. Однако же, я пишу это письмо вам, пишу для всех моих сослуживцев в войсках UIKit, Objective-C и Cocoa Framework.
Много байт мы потеряли, много еще будет потеряно, но все же фронт мы не сдаем. Мы получаем новое и интересное оружие, одно из которых – это ARC, Каунтер ссылок автоматический. Воистину, с новым оружием нам открылись новые горизонты, и мы было уже начали побеждать, но мы чрезмерно расслабились.
О чем я говорю, спросите вы? О том, что память не сдается! Да, часто, но нет, не всегда мы ее получаем, завоевываем.
Начну я с примера, который я положил на github, открывайте аккуратнее, враги могли проникнуть туда, к тому времени, как вы читаете это письмо.
ARC прекрасно обрабатывает слабые (weak) ссылки, и не переносит сильных (strong). Хотя, возможно, я не прав, когда так говорю. Сильные ссылки обязательны к существованию, но вот в чем беда. Некоторые системные библиотеки тоже затачивались на сильные ссылки, и вы можете убедиться в этом открыв пример.
Давайте посмотрим на интересную особенность NSTimer. Вот, скажем так, типовой контроллер:
@interface TestViewController : UIViewController
{
int myNumber;
}
@end
#import "TestViewController.h"
static int controllerCount = 0;
@implementation TestViewController
-(id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
myNumber = ++controllerCount;
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self)
{
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doEvilThing) userInfo:nil repeats:YES];
}
return self;
}
-(void)doEvilThing
{
NSLog(@"I'm doing very evil thing %d", myNumber);
}
-(void)dealloc
{
NSLog(@"TestViewController deallocated. Thanks god");
}
@end
Метод dealloc вызывается, когда контроллер освобожден. Попробуем с ним поиграться?
Добавляем:
-(IBAction)openEvilController:(id)sender
{
TestViewController * tc = [[TestViewController alloc] initWithNibName:@"TestViewController" bundle:nil];
[self.navigationController pushViewController:tc animated:YES];
}
А затем пытаемся пойти назад. Контроллер должен удалиться.
Стоп. Смотрим в консоль:
2012-03-25 16:21:20.006 ARC_example[585:f803] I'm doing very evil thing 1
2012-03-25 16:21:21.007 ARC_example[585:f803] I'm doing very evil thing 1
2012-03-25 16:21:22.007 ARC_example[585:f803] I'm doing very evil thing 1
2012-03-25 16:21:23.006 ARC_example[585:f803] I'm doing very evil thing 1
...
Он не удалился. В наших рядах предатель, и это NSTimer! Как оказалось, NSTimer принимает в качестве делегата сильную ссылку, и сохраняет ее. Таким образом, зациклив действие на каком-нибудь объекте (любом объекте), вы навсегда оставите его в памяти. Оказывается, этим грешат многие методы и классы.
Вы можете убедиться, что контроллеры остаются, добавив еще парочку.
При этом, мы не можем убить таймер из метода dealloc, ведь он никогда не вызовется, равно, как и метод release, который запрещен.
Что делать?
Во-первых, взять себя в руки! Обязательно, всегда и безоговорочно используйте только слабые ссылки на делегаты. Это поможет вам избежать подобных проблем. Везде и всюду это советуют, не забывайте об этом.
@property (unsafe_unretained, nonatomic) id delegate;
Во-вторых, нужно что-то сделать с этим конкретным примером. К сожалению, я не нашел примеров создания слабой ссылки (ткните меня пилоткой!). Поэтому, я предлагаю вам действовать следующим образом.
Первым делом, наследуемся от UINavigationController:
@interface CloserNavigationController : UINavigationController
@end
#import "CloserNavigationController.h"
@implementation CloserNavigationController
-(UIViewController *)popViewControllerAnimated:(BOOL)animated
{
UIViewController * vc = [super popViewControllerAnimated:animated];
if ([vc respondsToSelector:@selector(unretainAll)])
{
[vc performSelector:@selector(unretainAll)];
}
return vc;
}
@end
Дальше, в злом контроллере создаем метод unretainAll, и немного переписываем код:
@interface TestViewController : UIViewController
{
int myNumber;
NSTimer * currentRuningTimer;
}
@end
#import "TestViewController.h"
static int controllerCount = 0;
@implementation TestViewController
-(id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
myNumber = ++controllerCount;
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self)
{
currentRuningTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doEvilThing) userInfo:nil repeats:YES];
}
return self;
}
-(void)unretainAll
{
NSLog(@"Make unretain %d", myNumber);
if (currentRuningTimer)
{
[currentRuningTimer invalidate];
currentRuningTimer = nil;
}
}
-(void)doEvilThing
{
NSLog(@"I'm doing very evil thing %d", myNumber);
}
-(void)dealloc
{
NSLog(@"TestViewController deallocated. Thanks god");
}
@end
Все. «Саня, мы отбились» © Очередная битва выйграна. Используйте этот метод, чтобы побеждать в новых боях!
Удачи, всегда ваш боевой товарищ, Dreddik.
Автор: Dreddik