考虑下面的代码,有哪些问题,如何把他改成正确的形式?

@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的理解的更多相关文章

  1. [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. 这道 ...

  2. LRU LFU FIFO 转载

    -------------------------------------->href--------------------------> http://blog.chinaunix.n ...

  3. TYVJ计算几何

    今天讲了计算几何,发几道水水的tyvj上的题解... 计算几何好难啊!@Mrs.General....怎么办.... 这几道题都是在省选之前做的,所以前面的Point运算啊,dcmp啊,什么什么的,基 ...

  4. noip赛前小结3

    嗯,这是第三份小结. 连续三天的小结. 这几天状态逐渐回来了. 前天3道题rk8左右. 昨天上午3道题rk7,但是有一道题考后1minAC了. 昨天晚上2道题AK了. 今天也3道题rk1了. 这个趋势 ...

  5. HDU 5047

    http://acm.hdu.edu.cn/showproblem.php?pid=5047 直到看到题解,我才知道这道题考的是什么 首先交点数是Σ(16*i),区域区分的公式是 边数+点数+1=分成 ...

  6. 一套帮助你理解C语言的测试题(转)

    前言 原文链接:http://www.nowamagic.net/librarys/veda/detail/775 内容 在这个网站(http://stevenkobes.com/ctest.html ...

  7. Y2K Accounting Bug

    题目: Description Accounting for Computer Machinists (ACM) has sufferred from the Y2K bug and lost som ...

  8. block没那么难(三):block和对象的内存管理

    本系列博文总结自<Pro Multithreading and Memory Management for iOS and OS X with ARC> 在上一篇文章中,我们讲了很多关于 ...

  9. HDU-3473Minimum Sum

    Problem Description You are given N positive integers, denoted as x0, x1 ... xN-1. Then give you som ...

随机推荐

  1. Python远程连接MySQL数据库

    使用Python连接数据库首先需要安装Python的数据库驱动. 我的本地只装了Python,并没有装MySQL,当我使用命令: sudo pip install mysql-python 安装驱动( ...

  2. 深入拆解Java虚拟机视频教程

    目录: 第1节说在前面的话   00:05:07分钟   | 第3节环境搭建以及jdk,jre,jvm的关系   00:20:48分钟   | 第5节jvm再体验-jvm可视化监控工具   00:21 ...

  3. springboot中动态修改logback日志级别

    springboot中动态修改logback日志级别 在spring boot中使用logback日志时,项目运行中,想要修改日志级别. 代码如下: import org.slf4j.Logger; ...

  4. 实验吧CTF练习题---WEB---头有点大解析

    实验吧web之头有点大   地址:http://www.shiyanbar.com/ctf/29 flag值:HTTpH34der     解题步骤: 1.进入解题界面,看提示 2.说提示很多,再提示 ...

  5. 5.cookie每个参数的意义和作用以及工作原理?

    cookie主要参数有: (1)expires 过期时间 (2)path cookie存放路径 (3)domain 域名 同域名下可访问 (4)Set-Cookie: name cookie名称

  6. SqlServer2014怎样还原数据库

    场景 在SqlServer2014企业版上怎样进行数据库的还原,首先你得有一个其他数据 的备份文件. 实现 打开cmd,输入sql,打开SqlServer 2014 Management Studio ...

  7. sqoop导oracle数据到hive中并动态分区

    静态分区: 在hive中创建表可以使用hql脚本: test.hql USE TEST; CREATE TABLE page_view(viewTime INT, userid BIGINT, pag ...

  8. FJUT - OJ优先队列专题题解

    题目链接http://120.78.128.11/Contest.jsp?cid=18 题面不贴了 都是英文题,看的我心力憔悴 =7= 一.Ugly Numbers 题目说一个数的质因数只包含2.3或 ...

  9. C++输入输出常用格式(cin,cout,stringstream)

    输入格式 1.cin>>a; 最基本的格式,适用于各种类型.会过滤掉不可见字符例如空格,TAB,回车等 2.cin>>noskipws>>ch[i]; 使用了 no ...

  10. 即时聊天APP(一)

    最新写了一个即时聊天的安卓Demo,是基于Bmob后端开发的app,由于Bmob有较大局限性,因此,我并没有按照开发文档来进行开发,只是简单写了一个基本的文字聊天,以后有时间我会自己写一个带服务端的即 ...