美文网首页IOSiOS程序猿程序员
如何优雅的处理循环引用(retain cycle)

如何优雅的处理循环引用(retain cycle)

作者: __block | 来源:发表于2018-06-13 11:29 被阅读266次

    什么是循环引用?

    顾名思义, 就是几个对象某种方式互相引用, 形成了"环"。由于 Objective-C 内存管理使用引用计数的架构, 而并不是 GC(garbage collector), 而在 ARC(自动引用计数) 下所有 OC 对象的内存都交由系统统一管理。在 ARC 下 retainrerleaseautoreleasedealloc 都无法被调用, 因为 ARC 要分析何处应该自动调用内存管理方法, 如果手动调用的话会干扰其工作。更多关于内存管理的内容我会在之后的文章解答
      

    两个或两个以上对象彼此强引用而形成循环应用 循环引用中只剩一个对象还引用产生循环引用的某个对象 移除此引用后 ABCD 四个对象所造成的循环引用就泄露了

      
    那么在 ARC 下经常产生循环引用的就只有三种情况了:

    Delegate:

    在声明 delegate 的时候, 使用 retainstrongcopy 等强引用属性关键字修饰时, 会导致代理方拥有被代理方的引用, 被代理方又通过 delegate 拥有了代理方的引用, 这样就造成了循环引用。
      解决方式就是在 ARC 下将关键字改为 weak 即可。

    Block:

    有几种我们常见的 block 的使用:

    1、类方法不会造成循环引用, 因为类不会持有对象

    [UIView animateWithDuration:2 animations:^{
            
    }];
    

    2、self 并没有对 block 进行引用, 只是 block 对 self 单方面引用, 所以没有造成循环引用

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
    });
    
    

    3、在某个作用域内创建对象并且的 block 回调调用 self, self 没有持有该对象, 没有造成循环引用

    TestObject *test = [[TestObject alloc] init];
    [test somethingBlock:^{
        [self doSomething];
    }];
    

    4、self 强引用了 object, object 又强引用了 block, 而在 block 回调里又调用了 self, 导致 block 强引用 self, 造成循环引用, 导致 self 无法被释放

    [self.object somethingBlock:^{
            [self doSomething];
    }];
    

    通常会做如下处理:

    // 弱引用 self 这个对象
    __weak ViewController *weakSelf = self;
    [self.object somethingBlock:^{
        // 捕获 weakSelf 这个引用(由于 __weak 修饰的都是在栈内, 有可能被系统释放, 导致 block 内使用 weakSelf 调用的代码无效)
        // 假设某 View 加载在 ViewController 上, 需要在 block 内重新布局, 而执行到 block 时 ViewController 已经销毁, 也就是说 View 没有重新布局的必要, 这种情况就不需要捕获 weakSelf 这个引用
        __strong ViewController *strongSelf = weakSelf;
        [strongSelf doSomething];
    };
    

    也可以根据不同应用场景做不同的处理:

    • 当 object 不再使用时可以主动置为 nil, 从而打破循环引用。 如果 block 声明为属性, 也可以将属性主动置为 nil, 也可打破循环引用。
    [self.object somethingBlock:^{
        [self doSomething];
        self.object = nil;
    }];
    
    • 如果某类内部将 block 作为私有属性保存并使用, 当 block 后续不会再被使用到时, 可以主动将置为 nil, 从内部打破循环引用。

    下面是某类的具体实现, 内部有一私有属性将 block 捕获, 使用 somethingBlock 做一系列事情后, 将 block 回调。

    #import "TestObject.h"
    
    @interface TestObject ()
    
    @property (nonatomic, copy) void(^somethingBlock)(void);
    
    @end
    
    @implementation TestObject
    
    - (void)somethingBlock:(void(^)(void))block {
        _somethingBlock = block;
        // 使用 _somethingBlock 做一些事情
        !_somethingBlock ? : _somethingBlock();
        _somethingBlock = nil;
    }
    
    @end
    

    于是是使用此类的代码就可以这样写:

    [self.object somethingBlock:^{
        [self doSomething];
    }];
    

    NSTimer:

    当使用 NSTimer 定时器时, 定时器会强引用 target, 等自身失效时再释放此对象。执行完相关任务后, 没有循环的定时器会自动失效, 但是如果需要循环的定时器, 则需要调用 - (void)invalidate; 使定时器失效。
      由于定时器会保留目标对象, 所有循环执行任务的时候通常会导致循环引用, 先看下面代码:

    @interface RepeatTimer ()
    
    - (void)startTimer;
    - (void)stopTimer;
    
    @end
    
    @implementation RepeatTimer {
        NSTimer *_repeatTimer;
    }
    
    - (id)init {
        return [super init];
    }
    
    - (void)dealloc {
        [_repeatTimer invalidate];
    }
    
    - (void)startTimer {
        _repeatTimer = [NSTimer scheduledTimerWithTimeInterval:5
                                                  target:self
                                                selector:@selector(doSomething)
                                                userInfo:nil
                                                 repeats:YES];
    }
    
    - (void)stopTimer {
        [_repeatTimer invalidate];
        _repeatTimer = nil;
    }
    
    - (void)doSomething {
        
    }
    

    当使用者创建了 RepeatTimer 的对象并且调用 - (void)startTimer 后, startTimer 内部实现将 RepeatTimer 的对象自身传入 NSTimer, 使得 NSTimer 保留了此对象, 而 RepeatTimer 内部有持有了 NSTimer 的对象, 造成了循环引用, 只有当使用者调用 - (void)stopTimer 时, 才可以打破循环引用。
      除非使用该类的代码完全在你的掌控之中, 否则没有办法保证其他在开发人员一定会调用 - (void)stopTimer 方法, 所以这并不是一个很好的解决方案。此外如果想在系统回收该类时令定时器无效也是没有用的, 因为 NSTimerRepeatTimer 在相互引用, 所以 RepeatTimer 的对象绝对不会被释放。 当指向 RepeatTimer 实例的最后一个外部引用移走之后, 除了 NSTimer 再无其它类在对其保持引用, 也就是说该实例已经"丢失"了, 并永远不会被释放。
      可以为 NSTimer 添加一个 category, 增加一个带有 block 的方法来解决此问题:

    @interface NSTimer (RepeatBlockTimer)
    
    + (NSTimer *)scheduledMyTimerWithTimeInterval:(NSTimeInterval)timeInterval
                                          repeats:(BOOL)repeats
                                            block:(void(^)(void))block;
    
    @end
    
    @implementation NSTimer (RepeatBlockTimer)
    
    + (NSTimer *)scheduledMyTimerWithTimeInterval:(NSTimeInterval)timeInterval
                                          repeats:(BOOL)repeats
                                            block:(void(^)(void))block {
        return [self scheduledTimerWithTimeInterval:timeInterval
                                             target:self
                                           selector:@selector(blockInvoke:)
                                           userInfo:[block copy]
                                            repeats:repeats];
    }
    
    + (void)blockInvoke:(NSTimer *)timer {
        void (^block)(void) = timer.userInfo;
        !block ? : block();
    }
    
    @end
    

    上面代码将定时器所执行的任务封装成 block, 在调用定时器的时候作为 userInfo 的参数传进去 , 传入时将 block 拷贝的堆上, 否则稍后执行它的时候, 该 block 可能已经无效。定时器现在的 targetNSTimer 的对象, 这是个单例, 所以不需要关心定时器是否会保留它。 不过此处依然有循环引用, 不过因为类对象是不需要回收的, 所以不考虑。
      然后在之前 - (void)stopTimer 里做如下修改:

    - (void)startTimer {
        __weak typeof(self) weakSelf = self;
        _repeatTimer = [NSTimer scheduledMyTimerWithTimeInterval:5
                                      repeats:self
                                        block:^(void) {
            RepeatTimer *strongSelf = weakSelf;
            [strongSelf doSomething];
        }];
    }
    

    使用 __weak 定义一个弱引用指向 self, 在 block 内部捕获这个引用。这样做的好处是保证 self 不会被定时器所引用, 保证实例(也就是捕获的引用)在执行期间持续存活。
      这样在外部指向 RepeatTimer 的引用为0时, 该实例对象就会被回收, 同时会停止定时器循环所做的操作。
      不过 iOS 在 10.0 以后系统已经提供了此方法:

    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
    

    在使用时只需下面这样就可以了

    - (void)startTimer {
        __weak typeof(self) weakSelf = self;
        _repeatTimer = [NSTimer scheduledTimerWithTimeInterval:5 repeats:self block:^(NSTimer *timer) {
            RepeatTimer *strongSelf = weakSelf;
            [strongSelf doSomething];
        }];
    }
    

    相关文章

      网友评论

      本文标题:如何优雅的处理循环引用(retain cycle)

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