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

@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. CF 435B Little Pony and Harmony Chest

    Little Pony and Harmony Chest 题解: 因为 1 <= ai <= 30 所以  1 <= bi <= 58, 因为 59 和 1 等效, 所以不需 ...

  2. 树形动态规划 fjutoj-2392 聚会的快乐

    聚会的快乐 TimeLimit:1000MS  MemoryLimit:128MB 64-bit integer IO format:%lld Problem Description 你要组织一个由你 ...

  3. JOBDU 1109 连通图

    题目1109:连通图 时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:4192 解决:2224 题目描述: 给定一个无向图和其中的所有边,判断这个图是否所有顶点都是连通的. 输入: 每组数据 ...

  4. codeforces 808 D. Array Division(二分)

    题目链接:http://codeforces.com/contest/808/problem/D 题意:有一串长度为n的数组,要求选择一个数字交换它的位置使得这串数能够分成两串连续的和一样的数组. 这 ...

  5. Codeforces Round #383 (Div. 2)D. Arpa's weak amphitheater and Mehrdad's valuable Hoses(dp背包+并查集)

    题目链接 :http://codeforces.com/contest/742/problem/D 题意:给你n个女人的信息重量w和美丽度b,再给你m个关系,要求邀请的女人总重量不超过w 而且如果邀请 ...

  6. jquery中的 $(function(){ .. }) 函数

    2017-04-29 在讲解jquery中的 $(function(){ .. }) 函数之前,我们先简单了解下匿名函数.匿名函数的形式为:(function(){ ... }),又如 functio ...

  7. git bash下添加忽略文件列表

    转载自:https://blog.csdn.net/weixin_42808389/article/details/81232119 在用KEIL 5(MDK ARM)开发项目时需要用到GIT管理代码 ...

  8. Vue中如何使用less

    最近发现好多小伙伴在面试的过程中会问到vue如何使用less和scss,所以我绝对更新.复习一下less:废话不多说直接进主题: 依赖下载 1.首先使用npm下载依赖: npm install --s ...

  9. springcloud(四):应用配置中心config的安全设置

    springcloud应用配置中心config的安全设置 在springcloud应用开发中,为了方便在线管理我们的配置文件,通常会配一个配置中心config-server,这里托管着应用的一些配置文 ...

  10. linux常用命令二

    linux常用命令一 常用指令 ls        显示文件或目录 -l           列出文件详细信息l(list) -a          列出当前目录下所有文件及目录,包括隐藏的a(all ...