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

栏目: Objective-C · 发布时间: 5年前

内容简介:文章的标题看上去有点唬人, 毕竟strong作为iOS开发中一个基础的不能再基础的语义声明, 有什么好讲的…事实上本文源自于前文在前文中, 我们在文章末尾给出了一个demo, 模拟笔者在项目中的crash.

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

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

背景

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

@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
}
@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
@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, 原因如前文所说这是没有任何问题的. 然而前文中还有一张图

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

可以看到上面给出的demo其实是对图里的过程进行了简化, 所以来看一看正常按照图上写的话demo应该是什么样子.

@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
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
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销毁的调用栈…所以上面那个结论其实是不成立的.

于是想到看一看这两种调用到底有什么差别, 把这两种写法的汇编拉出来看一看:

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

第一张图是通过getter拿到对象的写法, 可以清晰的看到这种写法多了 _objc_retainAutoreleasedReturnValue_objc_release 两个操作.

而getter方法中实际上的操作是:

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

之前笔者认为getter方法在return的时候会调用 objc_autoreleaseRturnValue , 可是这里很清楚显示调用的是 _objc_retainAutoreleaseReturnValue , 实际上, 如果没有重写getter方法的话, 这句话都不会加.

那么问题就很简单了, 看一看这几个操作的作用是什么就好了

// 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, 也就是文章标题中所说的延迟释放.

对于这样一行代码

[self.manager networkRequest];

实际上等同于

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

在我们这个例子里, 对象的持有关系是这样的:

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

回到crash上面, 前面说到上面的demo中和实际项目中的区别是demo中是同步的, 而项目中是是异步的, 网络请求回调回来的时候 networkRequest 已经走完了, 也就是 runtime 中增加的引用计数已经释放了, 那么自然在实际项目中就crash了.

只聚焦这个问题的话,其实答案已经给出了, 不过依然还有很多值得探索, 例如本文提到其它几个函数里都做了些啥, 例如什么时候在return的时候会加上 objc_autoreleaseReturnValue (通过汇编看是在return一个局部变量的时候). 如果重写demo里ViewController的manager getter方法的话, 又会有惊喜. 深入的话需要大量篇幅, 所以先用这张经典的图收尾吧…

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

总结

惯例总结一下:

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

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

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

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Lambda Calculus, Its Syntax and Semantics . Revised Edition

The Lambda Calculus, Its Syntax and Semantics . Revised Edition

H.P. Barendregt / North Holland / 1985-11-15 / USD 133.00

The revised edition contains a new chapter which provides an elegant description of the semantics. The various classes of lambda calculus models are described in a uniform manner. Some didactical impr......一起来看看 《The Lambda Calculus, Its Syntax and Semantics . Revised Edition》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具