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

@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. WebGL2系列之采样器对象

    前言 在WebGL1中,纹理的图片和采样信息都是写在纹理对象之中. 采样信息告诉GPU如何去读取贴图上图片的信息. 如果我们希望从同一个图片多次读取像素信息,但是每次读取的时候使用的过滤方式不一样, ...

  2. Kubernetes集群部署核心步骤

    目录 前言 一.所有节点安装docker 二.所有节点安装kubeadm 三.安装master节点 四.部署网络插件 五.安装node节点 六.运行一个demo 前言 这里使用环境:Ubuntu 18 ...

  3. DevExpress的GridView,为每行的动态绑定不同的RepositoryItemLookUpEdit

    有时需要动态为RepositoryItemLookUpEdit绑定数据源,比如联动选择的场景或者我们仅仅是需要一个下拉选择框而并不想要GridView的列与RepositoryItemLookUpEd ...

  4. 最短路问题---Dijkstra算法学习

    Dijkstra又称单源最短路算法,就从一个节点到其他各点的最短路,解决的是有向图的最短路问题 此算法的特点是:从起始点为中心点向外层层扩展,直到扩展到中终点为止. 该算法的条件是所给图的所有边的权值 ...

  5. 【Offer】[6] 【从尾到头打印链表】

    题目描述 思路分析 Java代码 代码链接 题目描述 从尾到头打印链表,将其添加到ArrayList当中输出 思路分析 递归的思路 利用栈 Java代码 public class Offer006 { ...

  6. SQL,如果碰到Json,你会怎么做?

    1.Json串如下: DECLARE @JsonInfo NVARCHAR() SET @JsonInfo=N' { "CalcPayInput":{ ", " ...

  7. Docker搭建disconf环境,三部曲之一:极速搭建disconf

    Docker下的disconf实战全文链接 <Docker搭建disconf环境,三部曲之一:极速搭建disconf>: <Docker搭建disconf环境,三部曲之二:本地快速构 ...

  8. 基于Selenium+Python的web自动化测试框架

    一.什么是Selenium? Selenium是一个基于浏览器的自动化测试工具,它提供了一种跨平台.跨浏览器的端到端的web自动化解决方案.Selenium主要包括三部分:Selenium IDE.S ...

  9. Spring Cloud(三):声明式调用

    声明式服务调用 前面在使用spring cloud时,通常都会利用它对RestTemplate的请求拦截来实现对依赖服务的接口调用,RestTemplate实现了对http的请求封装处理,形成了一套模 ...

  10. [币严BIZZAN区块链]数字货币交易所钱包对接之比特币(BTC)

    在币严BIZZAN开发数字货币交易所的过程中,一共有两大难点,一个是高速撮合交易引擎,另一个是钱包对接,这两者是我们团队以前没有接触过的.这个系列的文章主要介绍数字货币交易所钱包对接实现技术.第一个要 ...