一道题考你对__autoreleasing和__block的理解
考虑下面的代码,有哪些问题,如何把他改成正确的形式?
@interface TestObj : NSObject
@end
@implementation TestObj
- (void)methodWillSetError:(NSError **)error group:(dispatch_group_t)group {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
*error = [NSError errorWithDomain:@"domain" code:1 userInfo:nil];
dispatch_group_leave(group);
});
}
@end
void testBlockAndAutoReleasePool() {
NSLog(@"Hello, World!");
NSError *error;
TestObj *testObj = [TestObj new];
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
[testObj methodWillSetError:&error group:group];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"error is %@", error);
});
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
testBlockAndAutoReleasePool();
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop run];
}
return 0;
}
methodWillSetError会去异步设置error的值,然后另外一个地方在error设置后去访问error的值。
实际上现在新版的Xcode已经会对
*error = [NSError errorWithDomain:@"domain" code:1 userInfo:nil];
进行警告
Block captures an autoreleasing out-parameter, which may result in use-after-free bugs
那么这个警告是什么意思呢?
实际上这个方法
- (void)methodWillSetError:(NSError * *)error group:(dispatch_group_t)group
的error,虽然没有明确指定内存的修饰符(strong, weak, autoreleasing,但是如果你直接定义NSError **error的临时变量,在arc下xcode会编译失败,要求你明确指定内存关系)但是编译器会默认转成NSError * __autoreleasing*,而在block中捕获一个__autoreleasing的out-parameter是很容易造成错误的。
为什么这么说呢?
void testAutoReleaseError(NSError **error) {
[@[@1, @2] enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (idx == 0) {
*error = [[NSError alloc] initWithDomain:@"domain" code:1 userInfo:nil];
}
}];
NSLog(@"error:%@" , *error);
}
我们用个简化例子来看一下,这个是很容易随手写下的代码。打开Xcode的Zombie,会发现在
NSLog(@"error:%@" , *error);
那一行crash掉,访问了个已经释放的对象,error已经被dealloc掉了。
为什么呢?前面说了error默认是NSError *__autoreleasing *,也就是说*error指向的对象是个__autoreleasing对象,所以
*error = [[NSError alloc] initWithDomain:@"domain" code:1 userInfo:nil];
的赋值在arc下会加个autorelease的调用变成
*error = [[[NSError alloc] initWithDomain:@"domain" code:1 userInfo:nil] autorelease];
而eumerateXXX这一系列的容器接口,里面的实现是包了一层Autorelease Pool的,所以在block运行完后Autorelease Pool被释放了附带着把*error指向的对象给释放了,*error就指向了个野指针,考虑到block运行时候外层存在是否包裹着一层Autorelease Pool的不确定性,所以clang直接把在block里捕获__autoreleasing的out-parameter给警告了。解决这个问题有两种方案,一种是指定error类型为NSError *__strong *。
把一开始的案例中的__autoreleasing修改成__strong后,会发现error打出来还是空的,这是为什么呢?
因为block捕获变量的时候是捕获变量的当前值,你对变量之后的重新赋值对block里的变量不会有影响。而在block里面也不能对变量做修改(block里的error实际上是个拷贝了当前block定义时候error的值的和block绑定的同名变量)
实际上除了error打出来是空的问题,这里还有个严重的可能会导致各种异常情况的bug,普通debug可能看不出来,但是打开Xcode的Address Sanitizer 的 Detect use of stack after return,就会发现在
*error = [NSError errorWithDomain:@"domain" code:1 userInfo:nil];
赋值这里会提示说Use of stack memory after return,因为在
void testBlockAndAutoReleasePool() {
NSLog(@"Hello, World!");
NSError *error;
TestObj *testObj = [TestObj new];
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
[testObj methodWillSetError:&error group:group];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"error is %@", error);
});
}
这一层的error是个栈变量,对其取地址得到是栈上的空间,等到dispatch_after去设置error的值的时候,栈空间由于函数已经返回了已经被销毁了,这里对error的写入会导致栈的破坏(可能某个栈变量的值就被覆盖了),导致各种奇怪crash你还无从定位。
这里就需要__block,__block的修饰可以将变量从栈空间的作用域提升到堆上。这里还有个小知识点,如果你直接加__block会发现还是在同样的地方会报出Use of stack memory after return,因为虽然__block可以将变量从栈空间的作用域提升到堆上,但它这个时机是在block被copy的时候才发生的,在我们的代码里,是先调用了methodWillSetError再调用dispatch_async,在methodWillSetError对error取地址的时候变量还在栈上,所以需要将async和methodWillSetError交换一下顺序才能保证代码正常运行。
回过头来再来看下,__autoreleasing到底是什么,为什么clang要把__autoreleasing作为默认选项,它和__strong的区别是什么?起初我们定义NSError *error的时候这里arc下不是默认是NSError * __strong error吗,把它的地址传递给个NSError * __autoreleasing *会发生什么?
https://clang.llvm.org/docs/AutomaticReferenceCounting.html
虽然clang的这篇文章是最权威也最全的文档,但是里面介绍还是很绕口的。所以这里就再说明一下。
把一个NSError *__strong*传递给一个接收NSError *__autoreleasing*参数的方法的时候,clang采用了一个pass-by-writeback的策略。
具体说来就是,在这一步骤
[testObj methodWillSetError:&error group:group];
clang 会改写成
NSError *__autoreleasing temp_error = error;
[testObj methodWillSetError:&temp_error group:group];
error = temp_error;
所以即使error的作用域已经被提升到了堆空间,但是如果error的修饰符是`NSError *__autoreleasing*,就会被转成一个在栈上的临时变量,传递到方法里异步去设置error的时候还是会造成栈的地址破坏。
那么为什么默认是__autoreleasing呢?在大部分的代码中其实往往就是一个局部变量(默认__strong类型),传递给一个out-parameter变量,这样就要经历这个__autoreleasing的转来转去的过程。
其实没啥特殊的原因,主要就是惯例,就和alloc,copy,mutableCopy和new家族的方法默认返回的是Retained return values,而其他函数通常返回的就是个Unretained return values一样。在Cocoa编程中,out-parameter返回的就是个autoreleasing的对象。(所以如果你在mrc下写一个传出out-parameter的方法,要确保这个out-parameter在离开这个方法的时候是个autoreleasing的状态,如果是个+1所有权的对象,那么就会有内存泄漏风险)。对比如果要把所有这种out-parameter的方法的参数加上个__autoreleasing的修饰,还不如直接所有的out-parameter默认就是__autoreleasing。所以这个只是一个最不坏的方案。
最后,给读者出个小问题,前面说了解决Block captures an autoreleasing out-parameter有两个办法,在我们的方法中,由于是要去异步设置error的值,所以语义上就应该是个__strong的修饰符,这是方法一,那么如果只是同步方法,又想要在block里设置这个out-parameter,应该要怎么做呢,这个就留给读者思考了。
一道题考你对__autoreleasing和__block的理解的更多相关文章
- [LeetCode] Convert Sorted Array to Binary Search Tree 将有序数组转为二叉搜索树
Given an array where elements are sorted in ascending order, convert it to a height balanced BST. 这道 ...
- LRU LFU FIFO 转载
-------------------------------------->href--------------------------> http://blog.chinaunix.n ...
- TYVJ计算几何
今天讲了计算几何,发几道水水的tyvj上的题解... 计算几何好难啊!@Mrs.General....怎么办.... 这几道题都是在省选之前做的,所以前面的Point运算啊,dcmp啊,什么什么的,基 ...
- noip赛前小结3
嗯,这是第三份小结. 连续三天的小结. 这几天状态逐渐回来了. 前天3道题rk8左右. 昨天上午3道题rk7,但是有一道题考后1minAC了. 昨天晚上2道题AK了. 今天也3道题rk1了. 这个趋势 ...
- HDU 5047
http://acm.hdu.edu.cn/showproblem.php?pid=5047 直到看到题解,我才知道这道题考的是什么 首先交点数是Σ(16*i),区域区分的公式是 边数+点数+1=分成 ...
- 一套帮助你理解C语言的测试题(转)
前言 原文链接:http://www.nowamagic.net/librarys/veda/detail/775 内容 在这个网站(http://stevenkobes.com/ctest.html ...
- Y2K Accounting Bug
题目: Description Accounting for Computer Machinists (ACM) has sufferred from the Y2K bug and lost som ...
- block没那么难(三):block和对象的内存管理
本系列博文总结自<Pro Multithreading and Memory Management for iOS and OS X with ARC> 在上一篇文章中,我们讲了很多关于 ...
- HDU-3473Minimum Sum
Problem Description You are given N positive integers, denoted as x0, x1 ... xN-1. Then give you som ...
随机推荐
- 杭电多校 hdu6627 equation
http://acm.hdu.edu.cn/showproblem.php?pid=6627 题意:解绝对值方程并统计解的个数. 解法:签到题,直接模拟小学数学学的零点分段法即可.(数据多直接cin, ...
- 牛客OI测试赛 C 序列 思维
链接:https://www.nowcoder.com/acm/contest/181/C来源:牛客网 题目描述 小a有n个数,他想把他们划分为连续的权值相等的k段,但他不知道这是否可行. 每个数都必 ...
- codeforces 14E. Camels(多维dp)
题目链接:http://codeforces.com/problemset/problem/14/E 题意:就是给出n个点要求画出t个波峰和t-1个波谷 很显然要t个波峰和t-1个波谷开始是波动一定是 ...
- .NET Core 微信小程序支付——(统一下单)
最近公司研发了几个电商小程序,还有一个核心的电商直播,只要是电商一般都会涉及到交易信息,离不开支付系统,这里我们统一实现小程序的支付流程(与服务号实现步骤一样). 目录1.开通小程序的支付能力2.商户 ...
- 解决rac错误 ORA-01102: cannot mount database in EXCLUSIVE mode
启动 Oracle 11g RAC数据库时出现以下错误.只能启动其中一个节点(rac01),另一个节点启动不了(rac02).可能是以前修改cluster_database这个参数引起的.在Orac ...
- Excel如何动态获取列名
遇到一个动态列,N行数据的求和,但是求和时需要Excel列名(A,B,C...)当时觉得这太非常难了.后来仔细研究了下Excel列名,都是从A到Z,然后AA再到AZ,以此类推. 如此的话就好弄了.通过 ...
- 【LeetCode】406-根据身高重建队列
title: 406-根据身高重建队列 date: 2019-04-15 21:13:06 categories: LeetCode tags: Java容器 比较器 贪心思想 题目描述 假设有打乱顺 ...
- IDEA导入Maven工程
1. 2.选择要导入的工程 3. 4. 5. 6.OK之后点击4图页面的next 7.点击Finish之后 完成!
- Eclipse For Mac下中文乱码解决
在Mac os 版本的eclipse下引入java项目或是源代码,经常会碰到其中中文部分都是乱码.对于这一问题,经过小试,可以解决. 1.打开eclipse 偏好设置 2.General ——> ...
- pageable多字段排序问题
Sort sort = new Sort(Sort.Direction.DESC, "createdate") .and(new Sort(Sort.Direction.AES, ...