事情是这样的, 有一段代码,精简之后如下:
- (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






网友评论