从一个crash体验strong的延迟释放

文章的标题看上去有点唬人, 毕竟strong作为iOS开发中一个基础的不能再基础的语义声明, 有什么好讲的…

事实上本文源自于前文从一个crash理解weak的延迟释放写完之后与小伙伴的讨论, 既然如此我们先简单回顾一下.

背景

在前文中, 我们在文章末尾给出了一个demo, 模拟笔者在项目中的crash.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@interface ViewController ()
@property (nonatomic, strong) Manager *strongManager;
@property (nonatomic, strong) Operation *strongOperation;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
[btn setBackgroundColor:[UIColor redColor]];
[btn addTarget:self action:@selector(btnAction:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
// 模拟网络请求
self.strongManager = [Manager new];
__weak typeof(self) weakSelf = self;
self.strongManager.endBlock = ^{
weakSelf.strongManager = nil;
};

self.strongOperation = [Operation new];
[self.strongOperation setTarget:self.strongManager selector:@selector(testMethod)];
}

- (void)btnAction:(id)sender{
[self.strongOperation performCallBack]; // <- 1
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface Manager : NSObject
@property (nonatomic, copy) void(^endBlock)(void);

- (void)testMethod;
@end

@implementation Manager
- (void)testMethod{
if (self.endBlock) {
self.endBlock();
}
NSLog(@"%@", self);
}

- (void)dealloc{
NSLog(@"dealloc");
}
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@interface Operation : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign)SEL finishSelector;
- (void)setTarget:(id)target selector:(SEL)selector;

- (void)performCallBack;
@end

@implementation Operation
- (void)setTarget:(id)target selector:(SEL)selector{
self.target = target;
self.finishSelector = selector;
}

- (void)performCallBack{
NSMethodSignature *signature = [self.target methodSignatureForSelector:self.finishSelector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:self.target];
[invocation setSelector:self.finishSelector];
[invocation invoke];
}
@end

在位置1处模拟网络请求回来进行回调, 然后crash, 原因如前文所说这是没有任何问题的. 然而前文中还有一张图
野指针
可以看到上面给出的demo其实是对图里的过程进行了简化, 所以来看一看正常按照图上写的话demo应该是什么样子.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface ViewController ()

@property (nonatomic, strong) Manager *manager;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
//正常写代码的时候还是推荐使用self.debugObj这种形式, 这里是为了排除干扰因素于是全部是用ivar的方式
_manager = [Manager new];
__weak typeof(self) weakSelf = self;
_manager.testBlock = ^{
weakSelf.manager = nil;
};
//发起网络请求
[self.manager networkRequest]; //<- 2
}

@end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Manager
.h略过

@implementation Manager

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

- (void)networkRequest {
Operation *operation = [Operation new];
operation.target = self;
operation.selector = @selector(networkCallBack);
[operation performCallback];
}
// call back
- (void)networkCallBack {
if (self.testBlock) {
self.testBlock();
}
NSLog(@"%@", self);
}

@end
1
2
Operation .h .m
和文章开头demo中的operation一样

这个demo在代码2的位置模拟网络请求, 网络请求封装在manager中, 事实上, 项目代码里的网络请求也是这么写的, 唯一的不一样地方在于networkCallBack方法在这里是同步调用, 而正常项目中网络请求是异步回来的. 然后有意思的地方就来了, 可能大家都能猜到, 上面这个demo中这样并不会crash…

追根溯源

于是开启大家来找茬模式, 仔细比对两份代码不同的地方, demo其实只做了一件事,在代码位置2处通过mananger的getter方法来获取对象然后调用networkRequest方法, 而networkRequest中一系列操作会将manager销毁.
在位置2处尝试把self.manager改为_manager, 成功的触发野指针crash.
于是笔者一开始很容易的就认为, self.manager所调用的getter方法在return的时候为返回的对象加了autorelease操作, 导致manager对象在置为nil后还能存活
然鹅, 在这个demo中, manager对象dealloc的时候调用栈里并没有autorelease销毁的调用栈…所以上面那个结论其实是不成立的.
于是想到看一看这两种调用到底有什么差别, 把这两种写法的汇编拉出来看一看:
property getter
property ivar
第一张图是通过getter拿到对象的写法, 可以清晰的看到这种写法多了_objc_retainAutoreleasedReturnValue_objc_release两个操作.
而getter方法中实际上的操作是:
manager getter
之前笔者认为getter方法在return的时候会调用objc_autoreleaseRturnValue, 可是这里很清楚显示调用的是_objc_retainAutoreleaseReturnValue, 实际上, 如果没有重写getter方法的话, 这句话都不会加.
那么问题就很简单了, 看一看这几个操作的作用是什么就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Prepare a value at +0 for return through a +0 autoreleasing convention.
id
objc_retainAutoreleaseReturnValue(id obj)
{
if (prepareOptimizedReturn(ReturnAtPlus0)) return obj;

// not objc_autoreleaseReturnValue(objc_retain(obj))
// because we don't need another optimization attempt
return objc_retainAutoreleaseAndReturn(obj);
}

// Accept a value returned through a +0 autoreleasing convention for use at +1.
id
objc_retainAutoreleasedReturnValue(id obj)
{
if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;

return objc_retain(obj);
}

不同版本里的runtime里对这几个函数的实现可能不太一样, 但是大概的意思是一样的, 这里摘抄的版本是objc4-709. 这里关键的函数objc_retainAutoreleasedReturnValue也就是前文中使用getter方式比ivar方式多的两部操作之一里面大概做了这么一件事, 它会与objc_autoreleaseReturnValue配合, 如果在返回值身上调用objc_autoreleaseReturnValue, 这个返回值会先被存储在TLS中, 然后外部接收方调用objc_retainAutoreleasedReturnValue时, 发现TLS中正好存了这个对象便直接返回这个对象, 从而减去存入autorelesepool的过程, 这一TLS优化在《Objective-C高级编程 iOS与OS X多线程和内存管理》一书中有一定篇幅的解释.
我们这里则是走入了没有优化的分支语句中, 也就是return objc_retain(obj);, 然后在_objc_msgSend之后调用的_objc_release. 这下就全明白了, 也就是说使用getter方法进行对象方法调用的时候, 编译器和runtime会帮我们悄咪咪的把对象引用计数+1, 在方法调用结束以后再减一. 这样一来我们这个方法在同步调用的时候就不用担心在同步的调用中因为对象被释放而引发野指针, 因为就算当前同步调用中当前对象实际持有者‘释放’了它, 在方法调用结束前还有runtime悄悄的给它+1s, 也就是文章标题中所说的延迟释放.
对于这样一行代码

1
[self.manager networkRequest];

实际上等同于

1
2
id temp = [self manager];
[temp networkRequest];

在我们这个例子里, 对象的持有关系是这样的:
持有关系
回到crash上面, 前面说到上面的demo中和实际项目中的区别是demo中是同步的, 而项目中是是异步的, 网络请求回调回来的时候networkRequest已经走完了, 也就是runtime中增加的引用计数已经释放了, 那么自然在实际项目中就crash了.
只聚焦这个问题的话,其实答案已经给出了, 不过依然还有很多值得探索, 例如本文提到其它几个函数里都做了些啥, 例如什么时候在return的时候会加上objc_autoreleaseReturnValue(通过汇编看是在return一个局部变量的时候). 如果重写demo里ViewController的manager getter方法的话, 又会有惊喜. 深入的话需要大量篇幅, 所以先用这张经典的图收尾吧…
Memory

总结

惯例总结一下:

  1. 警惕在对象持有的block中释放该对象本身这种代码
    1
    2
    3
    weakSelf.manager.testBlock = ^{
    weakSelf.manager = nil;
    };

出现这种代码的时候想一想在使用过程中有没有可能出现文中类似情况.

  1. 解释一段代码为什么没有问题可能比解释一段代码为什么有问题更加困难. 编译器和runtime以及Runloop为开发者的代码提供了很多的优化, 文中只是模拟了最简单的情况, 正常的项目环境中情况远复杂得多, 除了文中说到的引用计数增减以外还有autoreleasepool的运用, 所以开发过程中能遵守原本的内存管理语义就尽量遵守, 笔者所在的团队代码checkList中有一条是强调除了几个特定的方法中使用ivar方式访问变量, 其它都是用self.xxx的方式访问, 这样子的规范其实就避免了本文第二个demo中的crash.
  2. 最后给出一个简化的demo, 比上面那两个还要简化, 方便观察
    demo
  3. 本文中如果有说的不对的地方, 欢迎大家及时指出来.

参考

How does objc_retainAutoreleasedReturnValue work?
Friday Q&A 2011-09-30: Automatic Reference Counting

一个小广告

字节跳动西瓜视频团队真诚的邀请各路小伙伴的加入, 一起做有挑战, 有意义的事情, 欢迎大家投简历至gaobifan@bytedance.com