在以前MRC時(shí)代,我們管理對(duì)象的時(shí)候必須小心謹(jǐn)慎,避免對(duì)象不能正常釋放。后來(lái)到了ARC時(shí)代了,雖然大大簡(jiǎn)化了我們對(duì)對(duì)象生命周期的管理,但是稍不注意還是會(huì)導(dǎo)致對(duì)象不能釋放的問(wèn)題。非常常見(jiàn)的情況就是因?yàn)閷?duì)象之間形成了循環(huán)引用,導(dǎo)致對(duì)象不能正常釋放。這里列舉幾種比較容易被我們忽略的循環(huán)引用問(wèn)題。
一、cell的block中使用了self
比如一個(gè)自定義UITableViewCell或者UICollectionViewCell中定義一個(gè)block,用于把cell中的事件往外傳遞:
@interface YLTableViewCell : UITableViewCell
@property (nonatomic, copy) void (^actionBlock)(NSInteger type);
@end
在cellForRow中使用actionBlock時(shí)稍不注意使用了self:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
YLTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
cell.actionBlock = ^(NSInteger type) {
[self doSomethingWithType:type];
};
return cell;
}
在我剛接觸公司項(xiàng)目的時(shí)候,發(fā)現(xiàn)項(xiàng)目中有不少頁(yè)面不能正常釋放的問(wèn)題,經(jīng)過(guò)排查發(fā)現(xiàn)全都是在cell的block中直接使用了self導(dǎo)致的內(nèi)存泄露。這種情況其實(shí)也比較好理解,因?yàn)閟elf強(qiáng)引用了tableView,而tableView對(duì)cell也是強(qiáng)引用,cell又通過(guò)block強(qiáng)引用了self,因此造成了循環(huán)引用。
二、block中使用的宏定義中使用了self
也許在你的項(xiàng)目中也有類(lèi)似DLog這樣的一個(gè)宏定義方法,用于在DEBUG模式下正常輸出日志,在release模式下不輸出日志的控制。像我們的DLog的定義如下:
#ifdef DEBUG
#define DLog(s, ...) NSLog( @"<%p %@:(%d)> %@", self, [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, [NSString stringWithFormat:(s), ##__VA_ARGS__] )
#else
#define DLog(s, ...)
#endif
然后,你會(huì)不會(huì)不經(jīng)意間在一個(gè)不該直接使用self的block中順手寫(xiě)了個(gè)DLog("some log")呢?你的一個(gè)不經(jīng)意,可能會(huì)導(dǎo)致排查內(nèi)存泄露問(wèn)題排查倆小時(shí)。這里為什么,因?yàn)镈Log的宏定義中直接使用了self,所以在block中使用宏定義時(shí)一定要確保你的宏定義中沒(méi)有直接使用self。
三、通知addObserverForName:object:queue:usingBlock:中使用了self
我們?cè)谑褂猛ㄖ臅r(shí)候,如果我們是使用常見(jiàn)的addObserver和removeObserver的方式,只要記得移除通知的監(jiān)聽(tīng),一般不會(huì)造成內(nèi)存泄露的問(wèn)題,即使不移除,也是會(huì)造成向一個(gè)對(duì)象發(fā)送不能識(shí)別的消息的奔潰問(wèn)題。
但是,系統(tǒng)通知也為我們提供了一種更為簡(jiǎn)潔的,使用block處理通知回調(diào)的方法,比如這樣子:
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
NSOperationQueue *queue = [NSOperationQueue mainQueue];
[notificationCenter addObserverForName:UIApplicationDidEnterBackgroundNotification
object:nil
queue:queue
usingBlock:^(NSNotification * _Nonnull note) {
NSLog(@"%@", self.view);
}];
乍一看,這代碼沒(méi)啥問(wèn)題啊,self又沒(méi)有持有通知什么的。但是通過(guò)官方文檔我們會(huì)發(fā)現(xiàn)這個(gè)方法會(huì)將block添加到系統(tǒng)通知調(diào)度表中,block會(huì)被copy一份到通知中心,知道被登記的觀察者被移除。問(wèn)題就出在這里,系統(tǒng)通知中心會(huì)持有這個(gè)block,而一旦你在該block中持有了self,那么系統(tǒng)通知就間接的持有了self,導(dǎo)致self不能正常釋放。
正確的使用方式是不要在block中直接使用self,把該方法返回的觀察者記錄下來(lái),在不需要繼續(xù)監(jiān)聽(tīng)時(shí),把觀察者從系統(tǒng)通知中心中移除:
- (void)dealloc
{
[[NSNotificationCenter defaultCenter]removeObserver:_backgroundObserver];
}
- (void)viewDidLoad {
[super viewDidLoad];
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
NSOperationQueue *queue = [NSOperationQueue mainQueue];
__weak typeof(self) weakSelf = self;
self.backgroundObserver = [notificationCenter addObserverForName:UIApplicationDidEnterBackgroundNotification
object:nil
queue:queue
usingBlock:^(NSNotification * _Nonnull note) {
NSLog(@"%@", weakSelf.view);
}];
或者對(duì)于一次性的通知,在收到通知后在block中直接把觀察者從系統(tǒng)通知中心中移除就好了:
NSNotificationCenter * __weak center = [NSNotificationCenter defaultCenter];
id __block token = [center addObserverForName:@"OneTimeNotification"
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *note) {
NSLog(@"Received the notification!");
[center removeObserver:token];
}];
四、單例的數(shù)組中add了self
例如有這樣的場(chǎng)景:你有一個(gè)單例,在很多地方需要使用這個(gè)單例,當(dāng)這個(gè)單例的某屬性值發(fā)生改變時(shí),你需要通知使用到了該單例的對(duì)象們。于是你給這個(gè)單例創(chuàng)建了一個(gè)數(shù)組,用于記錄需要監(jiān)聽(tīng)某屬性發(fā)生改變的“代理們”。這樣就很容易造成循環(huán)引用了,因?yàn)閿?shù)組對(duì)添加進(jìn)來(lái)的對(duì)象是強(qiáng)引用。即使沒(méi)有形成循環(huán)引用,也會(huì)導(dǎo)致添加進(jìn)來(lái)的對(duì)象,在從這個(gè)數(shù)組中移除前不能正常釋放,你需要兼顧很多場(chǎng)景下如何將這些“代理們“正常的從單例的數(shù)組中移除。在我們的項(xiàng)目中,當(dāng)時(shí)做這塊功能的小伙伴,統(tǒng)一在這些”代理們“被pop出棧的時(shí)候從數(shù)組中移除了,在正常的流程下沒(méi)有任何問(wèn)題。但是隨著業(yè)務(wù)的發(fā)展,當(dāng)遇到這些”代理們“不是被pop出棧的情況時(shí),就會(huì)造成內(nèi)存泄露了。
遇到這種該怎么辦呢?在不改變這種給所有”代理們”循環(huán)發(fā)送消息的這種方式的情況下,我們可以考慮將數(shù)組換成NSPointerArray來(lái)記錄這些“代理們”。NSPointerArray是一個(gè)仿照數(shù)組功能的一個(gè)類(lèi),它可以指定添加進(jìn)來(lái)的對(duì)象是強(qiáng)引用還是弱引用,它還能添加nil。我們這里只需創(chuàng)建NSPointerArray對(duì)象時(shí)指定它對(duì)數(shù)組內(nèi)的對(duì)象是弱引用就好了。
NSPointerArray的簡(jiǎn)單使用舉例:
NSPointerArray *pointArray = [[NSPointerArray alloc]initWithOptions:NSPointerFunctionsWeakMemory];//弱引用
ViewController *vc = [ViewController new];
[_pointArray addPointer:nil];
[_pointArray addPointer:(__bridge void * _Nullable)(vc)];
NSLog(@"count=%li", _pointArray.count);//2
[_pointArray compact];//移除空對(duì)象
NSLog(@"count=%li", _pointArray.count);//1
for(id pointer in _pointArray){
NSLog(@"%@", pointer);
}
對(duì)應(yīng)NSDictionary和NSSet,系統(tǒng)也提供了NSMapTable和NSHashTable,在需要對(duì)集合中的對(duì)象指定弱引用的時(shí)候,大家可以考慮一下使用它們。
五、NSTimer導(dǎo)致的循環(huán)引用
我們常用NSTimer作為延遲執(zhí)行代碼或者定時(shí)執(zhí)行代碼的一種實(shí)現(xiàn)方式,但是很遺憾的是NSTimer會(huì)對(duì)它的target進(jìn)行強(qiáng)引用。這也就是為什么我們往往看到很多人使用NSTimer的時(shí)候都會(huì)找個(gè)“合適的時(shí)機(jī)”手動(dòng)銷(xiāo)毀掉timer。
比如我們常常像下面這樣使用timer:
_timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
但是系統(tǒng)內(nèi)部會(huì)強(qiáng)引用這個(gè)target也就是self,即使我們傳入weakSelf依然不奏效,可見(jiàn)系統(tǒng)內(nèi)部對(duì)target進(jìn)行了一次__strong操作。
其實(shí)我們沒(méi)必要在使用NSTimer的地方尋找“手動(dòng)釋放timer的時(shí)機(jī)”,這樣是不太安全的,因?yàn)殡S著業(yè)務(wù)的發(fā)展,難免會(huì)導(dǎo)致這個(gè)釋放時(shí)機(jī)不正常的問(wèn)題。事實(shí)上,系統(tǒng)有提供NSProxy代理類(lèi)來(lái)專(zhuān)門(mén)處理這種問(wèn)題(當(dāng)然不限于此),我們可以寫(xiě)一個(gè)繼承自NSProxy類(lèi)的子類(lèi),在其內(nèi)部弱持有timer的target對(duì)象,然后簡(jiǎn)單實(shí)現(xiàn)消息轉(zhuǎn)發(fā)的方法就可以了,這里就不詳細(xì)說(shuō)明了,網(wǎng)上已經(jīng)有很多這個(gè)問(wèn)題的介紹了,大家看看YYWeaKProxy的實(shí)現(xiàn)就好了。
看到這里,你一定想吐槽系統(tǒng)NSTimer設(shè)計(jì)的太不人性化了吧?我們想使用個(gè)定時(shí)器都這么麻煩,還要弄個(gè)代理類(lèi)來(lái)解決循環(huán)引用的問(wèn)題,如果不仔細(xì)看官方文檔還很容易忽略這個(gè)問(wèn)題。
可能蘋(píng)果也意識(shí)到了這一點(diǎn),從iOS10開(kāi)始,系統(tǒng)為我們提供了新的不會(huì)造成循環(huán)引用的使用NSTimer的方式:
__weak typeof(self) weakSelf = self;
_timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer *timer) {
[weakSelf timerAction];
}];
六、performSelector類(lèi)的延遲執(zhí)行代碼導(dǎo)致的循環(huán)引用
官方文檔中已明確說(shuō)明,performSelector:withObject:afterDelay:和performSelector:withObject:afterDelay:inModes:方法內(nèi)部就是用NSTimer來(lái)實(shí)現(xiàn)的延遲操作,所以,大家在使用這類(lèi)方法的時(shí)候也需注意一下是否造成循環(huán)引用問(wèn)題。