開(kāi)發(fā)中容易忽略的循環(huán)引用問(wèn)題

在以前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)題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時(shí)...
    歐辰_OSR閱讀 30,194評(píng)論 8 265
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類(lèi)型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,621評(píng)論 1 32
  • iOS網(wǎng)絡(luò)架構(gòu)討論梳理整理中。。。 其實(shí)如果沒(méi)有APIManager這一層是沒(méi)法使用delegate的,畢竟多個(gè)單...
    yhtang閱讀 5,466評(píng)論 1 23
  • 明天就要考英語(yǔ)四級(jí)了,我卻不想去考了。 因?yàn)椋抑?,這半年根本就沒(méi)把英語(yǔ)放在心上。 根據(jù)前段時(shí)間學(xué)到的沉沒(méi)成本,...
    小獅子1024閱讀 553評(píng)論 0 0
  • 100天30篇文章,第12篇 《你不得不知道的4個(gè)摸得著的~高效個(gè)人管理工具!》
    曼思閱讀 160評(píng)論 0 0

友情鏈接更多精彩內(nèi)容