这篇文章将会涉及下面几个方面:

  1. 几个循环引用的例子, 均是从项目中直接拿出来的实际例子.
  2. 可能引起内存问题的情况总结.
    不但会谈到什么情况下会有循环引用的问题, 还会谈到什么情况下不会发生循环引用的问题.
    不但会谈到什么时候weak-strong dance有用, 还会谈到什么时候weak-strong dance没用.
  3. 如何在开发过程中实时发现内存泄露问题.
  4. 如何在开发完成后找出内存泄露问题.

1. 几个循环引用的例子

1) 一个隐蔽的循环引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@weakify(self);
[self.KVOController observe:self.viewModel.loadDataCommand
keyPath:@"result"
options:NSKeyValueObservingOptionNew
block:^(id observer, id object, NSDictionary *change) {
@strongify(self);
CommandResult *result = change[NSKeyValueChangeNewKey];
if (result.error) {
[super loadDataFailure:result.error];
} else{
_headerView.segmentIndex = self.jumptonew ? 1 : 0;
_navBarVC.barImageEntity = self.viewModel.shopHeadEntity.topbarImg;
}
}];

2) 另一个隐蔽的循环引用

1
2
3
4
5
6
7
8
9
10
for (MGJLiveRankUserView *v in self.userViews) {
[self addSubview:v];
@weakify(self);
[v bk_whenTapped:^{
@strongify(self);
if (self.delegate && [self.delegate respondsToSelector:@selector(liveRankView:didTapUser:)]) {
[self.delegate liveRankView:self didTapUser:v.userEntity.userId];
}
}];
}

3) FBKVOController的坑

1
2
3
4
5
6
7
8
@weakify(self);
[self.KVOController observe:self
keyPath:@"emojiViewStatus"
options:0
block:^(id observer, id object, NSDictionary *change) {
@strongify(self);
[self dealEmojiViewWithEmojiStatus:object];
}];

4) 某个ScanCodeViewController的dealloc方法

1
2
3
4
5
6
- (void)dealloc
{
[self stopScaning];
[_timer invalidate];
self.timer = nil;
}

5) 没有循环引用, 但仍有问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)viewDidLoad
{
[super viewDidLoad];
for (int i = 0; i < 1000; i++) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(i * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self doSomething];
});
}
}

- (void)dealloc
{
NSLog(@"dealloc");
}

- (void)doSomething {}

答案:

1.直接访问实例变量(_headerView_navBarVC), 导致weak-strong dance无效, 最终导致循环引用.
另外[super loadDataFailure:result.error]里面的super也会造成循环引用,

objc中super是编译器标示符,并不像self一样是一个对象,遇到向super发的方法时会转译成objc_msgSendSuper(…),而参数中的对象还是self,于是从父类开始沿继承链寻找- class这个方法,最后在NSObject中找到(若无override),此时,[self class]和[super class]已经等价了。 引自iOS 程序员 6 级考试

图示

解决办法:
问题1: 将 super 改为 self
问题2: 坚持使用self来访问实例变量. (使用self.headerViewself->_headerView)

2.注意[self.delegate liveRankView:self didTapUser:v.userEntity.userId]里面的v, v -> tap_block -> v 导致循环引用.
这个就比较坑了, 当时用MLeaksFinder检测出来这个有泄漏, 我看只有一行代码, 不太可能有问题啊, 是不是误报了? 后来给dealloc下断点, 发现死活调不到, 被活活打脸.
图示
解决办法: weakify(v)或者用一个临时变量保存v.userEntity.userId, 然后传这个临时变量进block.

3.KVOController中, observer observee都是self.
KVOController会retain observee, 造成 所以形成 self(observer) -> self.KVOController -> self(observee) 的循环引用, 详细可参考这个issue. 只要 observee 反过来强引用 observer 就会造成循环引用, weak-strong dance都没用, 本例中是它的一种特殊情况(observee 就是 observer).
图示
解决办法: 使用KVOController里的self.KVOControllerNonRetaining, 他不会retain observee, 或者使用 BlocksKit里的 - (NSString *)bk_addObserverForKeyPath:(NSString *)keyPath task:(void (^)(id target))task;

4.这个是由于timer使用不当, self -> self.timer -> self 循环引用, 也就是说dealloc永远调用不到. 在dealloc释放timer的操作永远执行不到.
图示
解决办法: 手工打破循环, 在viewWillDisapear时调用timer的invalidate方法, 或者使用 GCD Timer, MSWeakTimer.

5.这个不是循环引用, 但是会造成VC在1000s后才释放, 属于延迟释放问题.
虽然一般不会用dispatch_after delay 1000s, 但是在复杂的业务场景中, 可能存在复杂的dispatch_after嵌套等情况.
解决办法: weakify(self), 然后如果self已经释放, 就直接return.

1
2
3
4
5
6
@weakify(self);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(i * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
@strongify(self);
if (!self) return;
[self doSomething];
});

2. 可能引起内存问题的情况总结

1) weak-strong dance 和 block

weak-strong dance使用情况的分析:

一般来说 weak-strong dance 可以避免大部分循环引用问题, 但是也不能盲目的使用.
简单介绍下weak-strong dance, 老司机可以跳过.
原始的写法是:

1
2
__weak typeof(self) weakSelf = self;   
__strong typeof(weakSelf) strongSelf = weakSelf;

其中__weak打破了循环引用, 在self释放时, weakSelf自动置空, 至于为什么又用__strong的原因是为了防止block中的代码执行一半, self释放了, weakself也就是nil了. block中代码执行一半就半途而废了.
后来我们引入libextobjc中的 @weakify 和 @strongfiy 来简写. 其原理还是一样的.

需要注意的是, 在嵌套的blocks中, 只需@weakify(self)一次, 但必须在每个blocks里都@strongify(self), 可以参考这个issue.

前面说weak-strong dance并不是万能的, 我们从block的使用来具体分析一下.

block的使用可以分成三种: [1]

1 临时性的,只用在栈当中,不会存储起来
比如数组的enumerateObjectsUsingBlock方法
比如masonry的mas_makeConstraints直接执行block, 不曾持有block
在这些情况下, 不需要weak-strong dance.
下图可以看到mas_makeConstraints实现就是拿了block直接用, 没有持有

1
2
3
4
5
6
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}

2 需要存储起来,但只会调用一次,或者有一个完成时期
比如一个 UIView 的动画, 动画完成之后, 需要使用 block 通知外面, 一旦调用 block 之后, 这个 block 就可以释放了.
再比如网络库的successBlock, 它会在网络请求结束释放该block.
再比如GCD的相关方法, 排队执行完就释放该block.

在这些情况下, 有时需要weak-strong dance
比如网络超时设为30s, 那个有可能网络库在30s后再会释放block里的对象. 造成资源的浪费. 这时候就需要weak-strong dance. 再比如例子5也需要weak-strong dance.
比如UIView的动画, 0.3s后动画完成, 0s延迟, 就不需要weak-strong dance.

3 需要存储起来,可能会调用多次
比如按钮的点击事件,假如采用 block 实现,这种 block 就需要长期存储,并且会调用多次。调用之后,block 也不可以删除,可能还有下一次按钮的点击。

在这些情况下, 都需要weak-strong dance. 并且有可能还不够, 比如例子1和例子2

2) delegate 用 strong 修饰

虽然最最低级的错误, 几乎不会有人再犯了,

3) Toll-Free Bridging

在CF对象和NS对象之间转换时, 我们会使用__bridge来桥接, 除了__bridge_transfer会将CF对象交给ARC管理, __bridge__bridge_retained都需要手工管理CF对象.
具体可以参考Mika Ash老师的这篇文章.

4) 可能造成延迟释放的情况

  • dispatch_after:1000s
    解决办法: block里面使用weakself, 判断weakself为空就return
  • [self performSelector:@selector(method1:) withObject:nil afterDelay:1000];
    解决办法: 在dealloc中调用[NSObject cancelPreviousPerformRequestsWithTarget:self]
  • NSOperationQueue
    解决办法: 在dealloc中调用[queue cancelAllOperations]

block和performSelector等的使用一定要考虑到对象的生命周期,block等会延长对象的生命,延迟释放,由此可能会造成逻辑上时序的问题.

5) NSNotificationCenter

需要注意的是 NSNotificationCenter 需要 removeObserver 不是由于循环引用的问题,
通知中心维护的是观察者是unsafe_unretained 引用, 类似于assgin, 不是weak, 不会自动置空, 使用unsafe_unretained的原因是兼容老版本的iOS系统, 所以要及时removeObserver, 否则可能造成访问野指针crash.

另外, 在VC中使用addObserverForName:object:queue:usingBlock:后, 在dealloc中调用[[NSNotificationCenter defaultCenter] removeObserver:self];是无效的, 原因是addObserverForName:object:queue:usingBlock:的observer不再是self了, 而是id observer = addObserverForName:object:queue:usingBlock:中的observer. 所以正确移除的办法是保留observer的引用然后移除. 在具体的使用中, weak-strong dance之后, 并不会造成VC的无法释放, 只会造成observer空跑, 影响不是很大.

6) WeakProxy

NSTimer 或者 [self mgj_observe:self forKeyPath:xxx]等这些会强引用observer的API, 在dealloc中去释放是没有用的, 在上面例子4已经提到了.
还有一种方法解决这个问题, 就是WeakProxy, FLAnimatedImage里面有一个实现, 用法是:

1
2
FLWeakProxy *weakProxy = [FLWeakProxy weakProxyForObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:.01 target:weakProxy selector:@selector(scanAnimation) userInfo:nil repeats:YES];

FLWeakProxy 只持有self的weak引用, 并通过OC的消息转发机制将消息转发给self处理, 这样timer就不会强引用self, dealloc里的[self.timer invalidate]就可以得到调用.

3. 使用MLeakFinder在开发过程中实时发现内存泄露问题

在我们的项目中是引入MLeakFinder第三方库, 原理是当一个 UIViewController 被 pop 时, 理论上该 UIViewController 包括它的 view,view 的 subviews 等等将很快被释放, 我们在 3s 后去 ping 这些对象, 如果这些对象还在就触发警告, 提醒开发者最近的改动引入了内存泄露. 详细的原理见这里

在我们的实践中, MLeakFinder已经成功发现了项目中多处内存泄露, 并且在开发中也多次监控到新的代码改动造成的内存泄露.
需要注意的是, MLeakFinder只默认在3s后去ping UIViewController和UIView, 其他的Object不在这个范围内, 需要手工监控, 使用MLCheck(self.otherObject)这个方法添加监控的对象.
理论上可以用runtime去拿到VC里用strong修饰的property, 然后加到MLCheck里, 不过这些property里面可能有单例需要排除一下.
另外MLeakFinder只是预警, 详细的泄露之处需要自行排查. 不过在开发过程中, 只要检查新引入的改动即可.
根据这篇介绍 MLeaksFinder2通过追踪对象的生命周期解决了延迟释放误报的问题, 在对象dealloc后, 关闭之前的警告的alert.
MLeaksFinder2还引入了FBRetainCycleDetector, 当发现嫌疑对象时, 使用FBRetainCycleDetector来找出真相. 不过在具体的测试过程中, 由于FBRetainCycleDetector问题, 查明真相并不是那么好用.

4. 使用Instruments排查现有项目的内存问题.

Instruments Leak有两种办法, 一个是Auto Snapshot, 一个是Mark Generation
实践中, 使用Auto Snapshot并没有太多的收获, 只看到一个个红叉, 找不到具体的泄露, 可能是姿势不对…
用auto snapshot只看到一个个红叉, 看泄露列表都是些无关的对象
所以我主要详细介绍Mark Generation .
Mark Generation的原理: 代追踪(Generation Tracking)是指在两次标记之间拍摄所有仍然活着的对象的快照. 当我们有一个重复的任务, 我们认为可能会内存泄露的时候, Generation Tracking是很有用的, 例如, 导航View Controller的进出.
具体的方法是手工在push VC前点Mark Generation一下, 然后pop VC后再点Mark Generation一次, 对比两代之间多出来的对象, 逐一核查, 找到泄露的对象.
Mark Generation按钮的位置
pushVC是得到GenerationB A, pop后得到 GenerationBB, 排查GenerationB
注意
instruments 打要开 dSYM 生成, 否则看不到具体的函数名
打要开 dSYM 生成

详细使用过程可参考这两个链接 link1, link2

引用

1.iOS开发中,block与代理的对比,双方的优缺点及在什么样的环境下,优先使用哪一种更为合适?