ARC: заметки с фронта войны за память

в 8:53, , рубрики: ARC, objective-c, память, разработка под iOS, ссылки, метки: , , ,

Здравствуйте, многоуважаемые коллеги.

Возможно, вы не знаете, но каждый день, каждый час, каждую секунду мы ведем войну за память устройств. Для кого-то эта война незаметна, кто-то не придает ей значение, кто-то воюет по-старинке. Однако же, я пишу это письмо вам, пишу для всех моих сослуживцев в войсках UIKit, Objective-C и Cocoa Framework.
ARC: заметки с фронта войны за память
Много байт мы потеряли, много еще будет потеряно, но все же фронт мы не сдаем. Мы получаем новое и интересное оружие, одно из которых – это 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js