美文网首页猿故I love iOS
开发中容易忽略的循环引用问题

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

作者: Code_Ninja | 来源:发表于2018-07-01 20:19 被阅读9次

    在以前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,在需要对集合中的对象指定弱引用的时候,大家可以考虑一下使用它们。

    相关文章

      网友评论

        本文标题:开发中容易忽略的循环引用问题

        本文链接:https://www.haomeiwen.com/subject/qwshuftx.html