开发中容易忽略的循环引用问题

在以前MRC时代,我们管理对象的时候必须小心谨慎,避免对象不能正常释放。后来到了ARC时代了,虽然大大简化了我们对对象生命周期的管理,但是稍不注意还是会导致对象不能释放的问题。非常常见的情况就是因为对象之间形成了循环引用,导致对象不能正常释放。这里列举几种比较容易被我们忽略的循环引用问题。

一、cell的block中使用了self

比如一个自定义UITableViewCell或者UICollectionViewCell中定义一个block,用于把cell中的事件往外传递:

@interface YLTableViewCell : UITableViewCell
@property (nonatomic, copy) void (^actionBlock)(NSInteger type);
@end

在cellForRow中使用actionBlock时稍不注意使用了self:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    YLTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    cell.actionBlock = ^(NSInteger type) {
        [self doSomethingWithType:type];
    };
    return cell;
}

在我刚接触公司项目的时候,发现项目中有不少页面不能正常释放的问题,经过排查发现全都是在cell的block中直接使用了self导致的内存泄露。这种情况其实也比较好理解,因为self强引用了tableView,而tableView对cell也是强引用,cell又通过block强引用了self,因此造成了循环引用。

二、block中使用的宏定义中使用了self

也许在你的项目中也有类似DLog这样的一个宏定义方法,用于在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

然后,你会不会不经意间在一个不该直接使用self的block中顺手写了个DLog("some log")呢?你的一个不经意,可能会导致排查内存泄露问题排查俩小时。这里为什么,因为DLog的宏定义中直接使用了self,所以在block中使用宏定义时一定要确保你的宏定义中没有直接使用self。

三、通知addObserverForName:object:queue:usingBlock:中使用了self

我们在使用通知的时候,如果我们是使用常见的addObserver和removeObserver的方式,只要记得移除通知的监听,一般不会造成内存泄露的问题,即使不移除,也是会造成向一个对象发送不能识别的消息的奔溃问题。

但是,系统通知也为我们提供了一种更为简洁的,使用block处理通知回调的方法,比如这样子:

NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    NSOperationQueue *queue = [NSOperationQueue mainQueue];
    [notificationCenter addObserverForName:UIApplicationDidEnterBackgroundNotification
                                    object:nil
                                     queue:queue
                                usingBlock:^(NSNotification * _Nonnull note) {
        NSLog(@"%@", self.view);
    }];

乍一看,这代码没啥问题啊,self又没有持有通知什么的。但是通过官方文档我们会发现这个方法会将block添加到系统通知调度表中,block会被copy一份到通知中心,知道被登记的观察者被移除。问题就出在这里,系统通知中心会持有这个block,而一旦你在该block中持有了self,那么系统通知就间接的持有了self,导致self不能正常释放。

正确的使用方式是不要在block中直接使用self,把该方法返回的观察者记录下来,在不需要继续监听时,把观察者从系统通知中心中移除:

- (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);
    }];

或者对于一次性的通知,在收到通知后在block中直接把观察者从系统通知中心中移除就好了:

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];
                                   }];

四、单例的数组中add了self

例如有这样的场景:你有一个单例,在很多地方需要使用这个单例,当这个单例的某属性值发生改变时,你需要通知使用到了该单例的对象们。于是你给这个单例创建了一个数组,用于记录需要监听某属性发生改变的“代理们”。这样就很容易造成循环引用了,因为数组对添加进来的对象是强引用。即使没有形成循环引用,也会导致添加进来的对象,在从这个数组中移除前不能正常释放,你需要兼顾很多场景下如何将这些“代理们“正常的从单例的数组中移除。在我们的项目中,当时做这块功能的小伙伴,统一在这些”代理们“被pop出栈的时候从数组中移除了,在正常的流程下没有任何问题。但是随着业务的发展,当遇到这些”代理们“不是被pop出栈的情况时,就会造成内存泄露了。

遇到这种该怎么办呢?在不改变这种给所有”代理们”循环发送消息的这种方式的情况下,我们可以考虑将数组换成NSPointerArray来记录这些“代理们”。NSPointerArray是一个仿照数组功能的一个类,它可以指定添加进来的对象是强引用还是弱引用,它还能添加nil。我们这里只需创建NSPointerArray对象时指定它对数组内的对象是弱引用就好了。

NSPointerArray的简单使用举例:

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];//移除空对象
NSLog(@"count=%li", _pointArray.count);//1
for(id pointer in _pointArray){
    NSLog(@"%@", pointer);
}

对应NSDictionary和NSSet,系统也提供了NSMapTable和NSHashTable,在需要对集合中的对象指定弱引用的时候,大家可以考虑一下使用它们。

五、NSTimer导致的循环引用

我们常用NSTimer作为延迟执行代码或者定时执行代码的一种实现方式,但是很遗憾的是NSTimer会对它的target进行强引用。这也就是为什么我们往往看到很多人使用NSTimer的时候都会找个“合适的时机”手动销毁掉timer。

比如我们常常像下面这样使用timer:

_timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];

但是系统内部会强引用这个target也就是self,即使我们传入weakSelf依然不奏效,可见系统内部对target进行了一次__strong操作。

其实我们没必要在使用NSTimer的地方寻找“手动释放timer的时机”,这样是不太安全的,因为随着业务的发展,难免会导致这个释放时机不正常的问题。事实上,系统有提供NSProxy代理类来专门处理这种问题(当然不限于此),我们可以写一个继承自NSProxy类的子类,在其内部弱持有timer的target对象,然后简单实现消息转发的方法就可以了,这里就不详细说明了,网上已经有很多这个问题的介绍了,大家看看YYWeaKProxy的实现就好了。

看到这里,你一定想吐槽系统NSTimer设计的太不人性化了吧?我们想使用个定时器都这么麻烦,还要弄个代理类来解决循环引用的问题,如果不仔细看官方文档还很容易忽略这个问题。

可能苹果也意识到了这一点,从iOS10开始,系统为我们提供了新的不会造成循环引用的使用NSTimer的方式:

__weak typeof(self) weakSelf = self;
    _timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer *timer) {
        [weakSelf timerAction];
    }];

六、performSelector类的延迟执行代码导致的循环引用

官方文档中已明确说明,performSelector:withObject:afterDelay:performSelector:withObject:afterDelay:inModes:方法内部就是用NSTimer来实现的延迟操作,所以,大家在使用这类方法的时候也需注意一下是否造成循环引用问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容

  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,339评论 8 265
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,092评论 1 32
  • iOS网络架构讨论梳理整理中。。。 其实如果没有APIManager这一层是没法使用delegate的,毕竟多个单...
    yhtang阅读 5,174评论 1 23
  • 明天就要考英语四级了,我却不想去考了。 因为,我知道,这半年根本就没把英语放在心上。 根据前段时间学到的沉没成本,...
    小狮子1024阅读 516评论 0 0
  • 100天30篇文章,第12篇 《你不得不知道的4个摸得着的~高效个人管理工具!》
    曼思阅读 136评论 0 0