隐藏__autoreleasing属性变量(NSError **

作者: 起个名字想破头 | 来源:发表于2017-06-08 23:14 被阅读79次

事情是这样的, 有一段代码,精简之后如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    NSError * error;
    [self fetchSyncError:&error];
    NSLog(@"%@", error);
}

//1.该方法是一个耗时的同步方法,但不考虑阻塞UI线程的问题
//2.error是一个传出参数
- (void)fetchSyncError:(NSError **)error {
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //do something
        *error = [NSError errorWithDomain:@"" code:110 userInfo:nil];
        dispatch_semaphore_signal(semaphore);
    });
    
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}

运行代码,然后就崩溃了。。。我xxx,什么鬼(其实走到这一步已经调试了许久了),然后发现问题出现在NSError上。简而言之,*error = [NSError errorWithDomain:@"" code:110 userInfo:nil];这里对*error进行了赋值,但离开dispatch作用域后就被销毁了,所以error就变成了野指针,在外部打印error时便崩溃了。

通过查询资料,发现在ARC中,编译器对引用传值做了一些隐藏的动作,参考《Transitioning to ARC Release Notes》,其中介绍了如果按照上面的代码,编译器会自动重写成如下方式:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    //如果引用传值参数没有声明为__autoreleasing,那么编译器会自动重新申请一个__autoreleasing属性的临时变量
    NSError * error;
    NSError * __autoreleasing tmp = error;  //编译器生成的临时变量
    [self fetchSyncError:&tmp];
    error = tmp;  
    NSLog(@"%@", error);
}

//1.该方法是一个耗时的同步方法,但不考虑阻塞UI线程的问题
//2.error是一个传出参数
//3.参数声明为__autoreleasing类型
- (void)fetchSyncError:(NSError * __autoreleasing *)error {
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //do something
        *error = [NSError errorWithDomain:@"" code:110 userInfo:nil];
        dispatch_semaphore_signal(semaphore);
    });
    
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}

到这里,知道了error其实是一个__autoreleasing属性的变量,经过测试,发现error对象在dispatch_async之后打印便会崩溃,所以推测在dispatch_async中有一个内部的autoreleasepool。于是乎,简化一下代码,如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSError * __autoreleasing error;//我们直接自己声明为__autoreleasing,替编译器省一步操作
    [self fetchSyncError:&error];
    NSLog(@"%@", error);
}

- (void)fetchSyncError:(NSError * __autoreleasing *)error {
      @autoreleasepool {
        *error = [NSError errorWithDomain:@"" code:110 userInfo:nil];
    }
}

运行一下代码,发现果然还是出现相同的错误。如此,该问题的原因就是__autoreleasing类型的error对象在出了autoreleasepool之后,就自动释放了,error变成了野指针。

那么,该怎么办呢?我们只要保证一个对象不是autorelease不就可以了嘛,那我们在autoreleasepool外申请一个临时变量,代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSError * __autoreleasing error;//我们直接自己声明为__autoreleasing,替编译器省一步操作
    [self fetchSyncError:&error];
    NSLog(@"%@", error);
}

- (void)fetchSyncError:(NSError * __autoreleasing *)error {
    NSError *tmp;
   @autoreleasepool {
      tmp = [NSError errorWithDomain:@"" code:110 userInfo:nil];
    }
    *error = tmp;
}

再次运行,OK!

以上问题的出现,主要有两点,一是对于引用传参,参数为__autoreleasing属性;二是有些方法会自带autoreleasepool,像子线程内部,字典或数组的block枚举方法(- (void)enumerateObjectsUsingBlock: - (void)enumerateKeysAndObjectsUsingBlock:)等。

下面两篇参考资料推荐看一下,写的更专业更深入,包括这两篇文章所参考的文章,也有许多值得学习钻研的地方。

参考资料:
探索子线程autorelease对象的释放时机
结合访问Out Parameters出现EXC_BAD_ACCESS的例子,反编译汇编解读__autoreleasing

相关文章

网友评论

    本文标题:隐藏__autoreleasing属性变量(NSError **

    本文链接:https://www.haomeiwen.com/subject/opzzfxtx.html