iOS 避免循环引用【译】

今天看文章发现一片关于Retain Cycle的老生常谈的问题,但是作者从开发常见场景的代理和Block分析了原因,分析的不错,加深了理解,索性小译一下,加上了一些自己的注解。欢迎转载评论,注明原文地址即可~

Avoid Strong Reference Cycles

随着ARC的引入,内存管理变得更容易了。然而,即使您不必担心何时保留和释放,但仍然有一些规则需要您知道,以避免内存问题。在这篇文章中,我们将讨论强引用循环。

什么是一个强引用循环?假设你有两个对象,对象A和对象B。如果对象A于对象B持有强引用,对象B于对象A有强引用,那么就形成了一个强引用循环。我们将讨论两种非常常见,需要注意循环引用的场景:Block和Delegate。

1
2
A->B: strong reference
B->A: strong reference

1. delegate

委托是OC中常用的模式。在这种情况下,一个对象代表另一个对象或与另一个对象协调。委派对象保留对另一个对象(委托)的引用,并在适当的时候向其发送消息。委托可以通过更新应用程序的外观或状态来响应。

(苹果的)API的一个典型例子是UITableView及其Delegate。在本例中,UITableView对其Delegate有一个引用,Delegate有一个返回UITableView的引用,按照规则,每一个都是(指向对方),保持对方活着,所以即使没有其他对象指向DelegateUITableView,内存也不会被释放。(所以需要弱引用)

1
2
3
4
5
6
7
8
9
10
11
12
13
#import <Foundation/Foundation.h>

@class ClassA;

@protocol ClassADelegate <NSObject>

-(void)classA:(ClassA *)classAObject didSomething:(NSString *)something;

@end

@interface ClassA : NSObject

@property (nonatomic, strong) id<ClassADelegate> delegate;

这将在ARC世界中生成一个保留循环。为了防止这一点,我们需要做的只是将对委托的引用更改为弱引用~

1
@property (nonatomic, weak) id<ClassADelegate> delegate;

Delegate模式

弱引用并未实现对象间的拥有权或职责,并不能使一个对象存活在内存中。如果没有其他对象指向delegate代理或者委托对象,那么delegate代理将被释放,随之delegate代理释放对委托对象的强引用。如果没有其他对象指向委托对象,则委托对象也将被释放。

2. Blocks

Block是类似于C函数的代码块,但除了可执行代码外,它们还可能包含堆栈中的变量。因此,Block可以维护一组数据,用于在执行时影响行为。因为Block保持代码的执行所需要的数据,他们是非常有用的回调。

官方文档:

BlockObjective-C对象,但是有些内存管理规则只适用于Block,而非其他Objective-C对象。

Block内对任何所捕获对象的保持强引用,包括Block自身,因此Block很容易引起强引用循环。如果一个类有这样一个Block的属性:

1
@property (copy) void (^block)(void);

在它的实现中,你有一个这样的方法:

1
2
3
4
5
6
7
- (void)methodA {
 
    self.block = ^{
 
        [self methodB];
    };
}
1
2
self->block: strong reference
block->self: strong reference

然后你就得到了一个强引用循环:对象selfblock有强引用,而block正好持有一个self的强引用。

Note: For block properties its a good practice to use copy, because a block needs to be copied to keep track of its captured state outside of the original scope.

注意:关于block的属性设置,使用copy是一个很好的方式,因为block需要被复制后用以在原始作用域外来捕获状态。

为了避免这种强引用循环,我们需要再次使用弱引用。下面就是代码的样子:

1
2
3
4
5
6
7
8
9
- (void)methodA {

ClassB * __weak weakSelf = self;

self.block = ^{

[weakSelf methodB];
};
}

通过捕获对自身的弱引用,block不会保持与对象的强引用。如果对象被释放之前的block称为weakself指针将被设置为nil。虽然这很好,因为不会出现内存问题,如果指针为nil,那么block内的方法就不会被调用,所以block不会有预期的行为。为了避免这种情况,我们将进一步修改我们的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)methodA {

__weak ClassB *weakSelf = self;

self.block = ^{

__strong ClassB *strongSelf = weakSelf;

if (strongSelf) {

[strongSelf methodB];
}
};
}

我们在block内部创建一个Self对象的强引用。此引用将属于block,只要block还在,它将存活内存中。这不会阻止Self对象被释放,我们仍然可以避免强引用循环。

并不是所有的强引用循环都很容易看到,正如示例中的那样,当您的块代码变得更复杂时,您可能需要考虑使用弱引用。

这是两种常见的模式,它们可以出现强引用循环。正如您所看到的,只要您能够正确地识别它们,就很容易用弱引用来破坏这些循环。即便ARC让我们更容易管理内存,但是你仍需要注意。

附注:翻译中,为了靠近原文意思,强引用循环就是大家经常说的循环引用。

附:Block的一点碎碎念

  1. block要用copy修饰,还是用strong

NSString、NSArray、NSDictionary 等等经常使用copy关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary;
block 也经常使用 copy 关键字,具体原因见官方文档:Objects Use Properties to Keep Track of Blocks:
block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。如果不写 copy ,该类的调用者有可能会忘记或者根本不知道编译器会自动对 block 进行了 copy 操作,他们有可能会在调用之前自行拷贝属性值。这种操作多余而低效。你也许会感觉我这种做法有些怪异,不需要写依然写。如果你这样想,其实是你日用而不知

参考

  1. Avoid strong reference cycles
  2. ChenYilong/iOSInterviewQuestions
文章作者: MichaelMao
文章链接: http://frizzlefur.com/2017/06/14/iOS_循环引用/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 MMao
我要吐槽下