iOS - 循环引用,看我就对了

5,830 阅读6分钟
原文链接: www.jianshu.com

我是一头来自北方的羊,咩-咩-咩-!
谈到循环引用,不知道你能想到什么?可能是delegate为啥非得用weak修饰,可能是block为啥总是需要特殊对待,你也可能仅仅想到了一个weakSelf,因为它能帮你解决99%的关于循环引用的事情。本文中,我将谈一谈我对循环引用的看法。

一、循环引用的产生

1、基本知识

首先,得说下内存中和变量有关的分区:堆、栈、静态区。其中,栈和静态区是操作系统自己管理的,对程序员来说相对透明,所以,一般我们只需要关注堆的内存分配,而循环引用的产生,也和其息息相关,即循环引用会导致堆里的内存无法正常回收。说起对内存的回收,肯定得说下以下老生常谈的回收机制:

  • 对堆里面的一个对象发送release消息来使其引用计数减一;
  • 查询引用计数表,将引用计数为0的对象dealloc;
    那么循环引用怎么影响这个过程呢?

2、样例分析

In some situations you retrieve an object from another object, and then directly or indirectly release the parent object. If releasing the parent causes it to be deallocated, and the parent was the only owner of the child, then the child (heisenObject in the example) will be deallocated at the same time (assuming that it is sent a release rather than an autorelease message in the parent’s dealloc method).

大致意思是,B对象是A对象的属性,若对A发送release消息,致使A引用计数为0,则会dealloc A对象,而在A的dealloc的同时,会向B对象发送release消息,这就是问题的所在。
看一个正常的内存回收,如图1:


图1


接下来,看一个循环引用如何影响内存回收的,如图2:


图2


那么推广开来,我们可以看图2,是不是很像一个有向图,而造成循环引用的根源就是有向图中出现环。但是,千万不要搞错,下面这种,并不是环,如图3:


图3

3、结论

由以上的内容,我们可以得到一个结论,当堆中的引用关系图中,只要出现环,就会造成循环引用。
细心的童鞋肯定还会发现一个问题,即是不是只有A对象和B对象这种关系(B是A的属性)才会出现环呢,且看第二部分的探究:环的产生。

二、环的产生

1、堆内存的持有方式

仔细思考下可以发现,堆内存的持有方式,一共只有两种:
方式a:将一个外部声明的空指针指向一段内存(例如:栈对堆的引用),如图4:


图4


方式b:将一段内存(即已存在的对象)中的某个指针指向一段内存(堆对堆的引用),如图5:


图5


一中所讲的B是A的属性无疑是方式b,除去这种关系,还有几种常见的关系也属于方式b,比如:block对block所截获变量的持有,再比如:容器类NSDictionary,NSArray等对其包含对象的持有。

2、方式a对产生环的影响

如图6:


图6

3、方式b对产生环的影响

如图7:


图7

4、结论

方式b是造成环的根本原因,即堆对堆的引用是产生循环引用的根本原因。
可能有的童鞋可能说,那方式a的指针还有什么用呢?当然是有用的,a的引用和b的引用共同决定了一个对象的引用计数,即,共同决定这个对象何时需要dealloc,如图8:


图8

三、如何干掉环

1、delegate与环

//ClassA:
@protocol ClssADelegate 
- (void)fuck;
@end
@interface ClassA : UIViewController
@property (nonatomic, strong) id  delegate;
@end
//ClassB:
@interface ClassB ()
@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改为弱引用。

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(弱引用)代替来解决循环引用。

四、解决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,这样还是会引起循环引用,这种错误很难发觉。

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

可以看出,这样就完美解决了3中缺陷,我们可以在block中随意使用self。

文中有两处图片有点小问题,nei cun:内存;xun huan yin yong:循环引用。