iOS循环引用问题

作者: _子墨 | 来源:发表于2017-04-28 11:19 被阅读1849次

    笔者前不久终于发布了自己的APP《小印记》,希望读者能前往App Store下载《小印记》支持一下笔者,谢谢!🙂

    《小印记》iOS源码分享--极光推送实践篇
    《小印记》iOS源码分享--自定义弹框篇
    《小印记》源码分享--极光推送服务器篇
    《小印记》iOS源码分享--网络层封装篇


    前言

    当多个对象相互持有形成一个封闭的环时,循环引用问题随之出现,导致内存泄漏。

    解决循环引用问题主要有两个办法:
    1)自己明确知道这里会存在循环引用,在合理的位置主动断开环中的一个引用(置为nil),使得对象得以回收;
    2)使用弱引用。

    弱引用的实现原理

    弱引用的实现原理是这样,系统对于每一个有弱引用的对象,都维护一个表来记录它所有的弱引用的指针地址。这样,当一个对象的引用计数为 0 时,系统就通过这张表,找到所有的弱引用指针,继而把它们都置成 nil。

    从这个原理中,我们可以看出,弱引用的使用是有额外的开销的。虽然这个开销很小,但是如果一个地方我们肯定它不需要弱引用的特性,就不应该盲目使用弱引用。举个例子,有人喜欢在手写界面的时候,将所有界面元素都设置成 weak 的,这某种程度上与 Xcode 通过 Storyboard 拖拽生成的新变量是一致的。但是我个人认为这样做并不太合适。因为:

    我们在创建这个对象时,需要注意临时使用一个强引用持有它,否则因为 weak 变量并不持有对象,就会造成一个对象刚被创建就销毁掉。
    大部分 ViewController 的视图对象的生命周期与 ViewController 本身是一致的,没有必要额外做这个事情。

    早先苹果这么设计,是有历史原因的。在早年,当时系统收到 Memory Warning 的时候,ViewController 的 View 会被 unLoad 掉。这个时候,使用 weak 的视图变量是有用的,可以保持这些内存被回收。但是这个设计已经被废弃了,替代方案是将相关视图的 CALayer 对应的 CABackingStore 类型的内存区会被标记成 volatile 类型.

    1、delegate与环

    //ClassA:
    @protocol ClssADelegate <NSObject>
    - (void)fuck;
    @end
    @interface ClassA : UIViewController
    @property (nonatomic, strong) id <ClssADelegate> delegate;
    @end
    //ClassB:
    @interface ClassB ()<ClassADelegate>
    @property (nonatomic, strong) ClassA *classA;
    @end
    @implementation ClassB
    - (void)viewDidLoad {
        [super viewDidLoad]; 
        self.classA = [[ClassA alloc] init];  
        self.classA.delegate = self;
    }
    

    如上代码,B强引用A,而A的delegate属性指向B,这里的delegate是用strong修饰的,所以A也会强引用B,这是一个典型的循环引用样例。而解决其的方式大家也都耳熟能详,即将delegate改为弱引用(weak):
    @property (nonatomic, weak) id <ClssADelegate> delegate;

    2、block与环

    @interface ClassA ()
    @property (nonatomic, copy) dispatch_block_t block;
    @property (nonatomic, assign) NSInteger tem;
    @end
    @implementation ClassA
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.block = ^{
            self.tem = 1;
        };  
    }
    

    如上代码,self持有block,而堆上的block又会持有self,所以会导致循环引用,这个例子非常好,因为xcode都能检测出来,报出警告:[capturing self strongly in this block is likely to lead to a retain cycle],当然大部分循环引用的情况xcode是不会报警告的。解决这种循环引用的常用方式如下(这种解决方式可以解决大部分block引起的循环引用,但是有一定缺陷,且看下一节):

    @interface ClassA ()
    @property (nonatomic, copy) dispatch_block_t block;
    @property (nonatomic, assign) NSInteger tem;
    @end
    @implementation ClassA
    - (void)viewDidLoad {
        [super viewDidLoad];
        __weak typeof(self) weakSelf = self
        self.block = ^{
            weakSelf.tem = 1;
        };  
    }
    

    3、结论

    如上delegate和block引起的循环引用的处理方式,有一个共同的特点,就是使用weak(弱引用)来打破环,使环消失了。所以,可以得出结论,我们可以通过使用将strong(强引用)用weak(弱引用)代替来解决循环引用。

    4、解决block循环引用的深入探索

    1)weakSelf与其缺陷

    //ClassB是一个UIViewController,假设从ClassA pushViewController将ClassB展示出来
    @interface ClassB ()
    @property (nonatomic, copy) dispatch_block_t block;
    @property (nonatomic, strong) NSString *str;
    @end
    @implementation ClassB
    - (void)dealloc {
    }
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.str = @"111";
        __weak typeof(self) weakSelf = self;
        self.block = ^{
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                NSLog(@"%@", weakSelf.str);
            });
        };
        self.block();   
    }
    

    这里会有两种情况:

    若从A push到B,10s之内没有pop回A的话,B中block会执行打印出来111。
    若从A push到B,10s之内pop回A的话,B会立即执行dealloc,从而导致B中block打印出(null)。这种情况就是使用weakSelf的缺陷,可能会导致内存提前回收。

    2)weakSelf和strongSelf

    @interface ClassB ()
    @property (nonatomic, copy) dispatch_block_t block;
    @property (nonatomic, strong) NSString *str;
    @end
    @implementation ClassB
    - (void)dealloc {
    }
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.str = @"111";
        __weak typeof(self) weakSelf = self;
        self.block = ^{
            __strong typeof(self) strongSelf = weakSelf;
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                NSLog(@"%@", strongSelf.str);
            });
        };
        self.block();   
    }
    

    我们发现这样确实解决了问题,但是可能会有两个不理解的点。

    这么做和直接用self有什么区别,为什么不会有循环引用:外部的weakSelf是为了打破环,从而使得没有循环引用,而内部的strongSelf仅仅是个局部变量,存在栈中,会在block执行结束后回收,不会再造成循环引用。
    这么做和使用weakSelf有什么区别:唯一的区别就是多了一个strongSelf,而这里的strongSelf会使ClassB的对象引用计数+1,使得ClassB pop到A的时候,并不会执行dealloc,因为引用计数还不为0,strongSelf仍持有ClassB,而在block执行完,局部的strongSelf才会回收,此时ClassB dealloc。
    这样做其实已经可以解决所有问题,但是强迫症的我们依然能找到它的缺陷:

    block内部必须使用strongSelf,很麻烦,不如直接使用self简便。
    很容易在block内部不小心使用了self,这样还是会引起循环引用,这种错误很难发觉。
    不要用NSString和NSNumber测试引用计数 最好使用自定义的class.

    3)@weakify和@strongify
    查看github上开源的libextobjc库,可以发现,里面的EXTScope.h里面有两个关于weak和strong的宏定义。

    // 宏定义
    #define weakify(...) \
        ext_keywordify \
        metamacro_foreach_cxt(ext_weakify_,, __weak, __VA_ARGS__)
    #define strongify(...) \
        ext_keywordify \
        _Pragma("clang diagnostic push") \
        _Pragma("clang diagnostic ignored \"-Wshadow\"") \
        metamacro_foreach(ext_strongify_,, __VA_ARGS__) \
        _Pragma("clang diagnostic pop")
    
    // 用法
    @interface ClassB ()
    @property (nonatomic, copy) dispatch_block_t block;
    @property (nonatomic, strong) NSString *str;
    @end
    @implementation ClassB
    - (void)dealloc {
    }
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.str = @"111";
        @weakify(self)
        self.block = ^{
            @strongify(self)
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                NSLog(@"%@", self.str);
            });
        };
        self.block();   
    }
    

    即使写了weak,strong。也得在block里面首先判断strong存在不存在,然后向下进行。
    可以看出,这样就完美解决了3中缺陷,我们可以在block中随意使用self。

    WechatIMG138.png

    相关文章

      网友评论

        本文标题:iOS循环引用问题

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