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 中解决循环引用的问题

    如何在 iOS 中解决循环引用的问题 如何在 iOS 中解决循环引用的问题

  • iOS循环引用问题

    检测循环引用方法 引入开源库MLeaksFinder 在Dealloc中打log,判断是否销毁 Instrumen...

  • iOS循环引用问题

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

  • iOS循环引用问题

    OC的内存管理应用了计数的方式来管理内存,这种方式虽然很简单,但是有一个比较大的瑕疵,就是它不能很好的解决循环引用...

  • iOS闭包循环引用精讲

    iOS闭包循环引用精讲 iOS闭包循环引用精讲

  • NSTimer的循环引用问题解决方案

    iOS开发中,针对循环引用的问题,会有很多方面,block,代理,自循环,多循环,还有一个就是Timer的循环引用...

  • weak-strong dance探究

    循环引用 循环引用是iOS开发常见的问题,虽然现在普遍是ARC工程,但是这个问题仍然无可避免。一般都是两个强引用对...

  • iOS 内存管理 部分二

    主要讲解CADisplayLink 和 NSTimer 的循环引用问题 iOS 内存管理 部分一iOS 内存管...

  • 浅谈OC中的循环引用问题

    Everyone has his dream, but ... ... 谈到循环引用这个问题,相信很多iOS的童鞋...

  • 一个奇怪且无聊的检测Block的想法

    在大多数iOS应用开发过程中, 循环引用一直都是最常见的iOS开发问题之一。通常情况下, 最常见的循环引用问题就是...

网友评论

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

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