原因
最近开发过程中因为自己的错误操作导致了一个很傻问题。感觉是一个基于内存管理,Block,指针的综合问题。整个理解下来,对上述概念有些了新的认识,觉得值得记录一次。首先非常鸣谢一道题考你对__autoreleasing和__block的理解的作者,本次记录的问题,基本也是基于作者提供的理解思路。
提问
我把日常的业务稍微精简了成了以下代码:
“网络请求,把error作为二级指针传入ViewModel的网络请求方法中,异步回写二级指针的值,最后回调打印error”
流程看似很简单,也很愉快。但其实下面的代码是最后的error是打印是”NULL”。
@interface ViewModel : NSObject
@end
@implementation ViewModel
- (void)getNetWorkWithError:(NSError **)error completeBlock:(void(^)(void))block {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
*error = [NSError errorWithDomain:@"domain" code:1 userInfo:nil];
block();
});
}
@end
void netWork() {
NSLog(@"Hello, World!");
NSError *error;
ViewModel *viewModel = [ViewModel new];
[viewModel getNetWorkWithError:&error completeBlock:^(){
NSLog(@"%@",error);
}];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
netWork();
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop run];
}
return 0;
}
怎么改
先说答案,有两个思路
1.第一种,error
的修饰符直接改成static
。
NSError *error;
改成static NSError *error;
2.第二种,改两处,NSError *error
用__block
修饰,block
必须先创建再传入参数。
``` 12NSLog(@”Hello, World!”); __block NSError *error;
ViewModel *viewModel = [ViewModel new];
void(^bb)(void) = ^{
1
NSLog(@"%@",error);
}; [viewModel getNetWorkWithError:&error completeBlock:bb]; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
但是不管以上哪两种方法`viewModel`的对象方法必须改成`- (void)getNetWorkWithError:(NSError * __strong *)error completeBlock:(void(^)(void))block`。
这样就可以正确打印我们想要的效果。
## 原因
1.首先第一种思路,原因不必多说了,将error提升到全部静态变量,在代码的任何地方都能轻易地访问到本尊,也就是没有我接下去讲的那么多破事了(block不capture静态变量),所以一句话带过,要是你对代码有洁癖,不想随意开辟全局空间,那就看看第二种思路。
2.首先,编译器会把``__block`` 修饰``error``对象变成一个结构体,而对于第二种写法为什么要先申明好`block`,再作为参数塞入呢,主要是因为`` void(^bb)(void) = ^{NSLog(@"%@",error);};``,`arc`环境下基本没有`stackBlock`,看到的`block`基本都是`mallocBlock`或者`globalBlock`。所以这句话因为`block`通过`copy` 捕获到的`error`上升到堆空间,所以如果开始这么写的显然是不对的。
[viewModel getNetWorkWithError:&error completeBlock:^(){ NSLog(@”%@”,error); }];
1
2
3
4
5
通过clang转换cpp可得知:
((void ()(id, SEL, NSError **, void ()()))(void )objc_msgSend)((id)viewModel, sel_registerName(“getNetWorkWithError:completeBlock:”), &(error.__forwarding->error), ((void ()())&__netWork_block_impl_0((void *)__netWork_block_func_0, &__netWork_block_desc_0_DATA, (__Block_byref_error_0 *)&error, 570425344)));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
首个参数(error的二级指针),入参时还是栈空间地址。执行到下方`block`参数时,才上升到堆空间。这么写,两个error的地址其实不是一个,所以在这里对调用的时机有了更严格的要求。
3.关于二级指针为什么默认是``__autorelease``对象,然后这里必须改成 `__strong`修饰,原因我用自己的话总结了一下,
首先第一点,我们先搞清楚,为什么在这种需求下,必须这么写。因为用了``__autorelease``修饰的外部参数,是用到了一个传递回写的机制([pass-by-writeback](https://clang.llvm.org/docs/AutomaticReferenceCounting.html#arc-ownership-restrictions-pass-by-writeback) 官方clang文档 4.3.3明确有提到), 类似于这样(引用自[iOS开发Tips:objective-c指针解引用](https://www.jianshu.com/p/1dc7c31fa06f)):
```ruby
__strong NSObject *o;
__autoreleasing NSObject *temp = o;
[self method2:&temp];
o = temp;
编译器帮我生成一个临时的变量传入方法,最后回写到我们原来的对象上。
这就是解释了,为什么采用默认写法会有问题:
1
2
3
4
- (void)getNetWorkWithError:(NSError **)error completeBlock:(void(^)(void))block {
NSLog(@"error is %p", &*error);
//...延迟操作error...
}
在方法内部和方法外部打印一下指针的地址,就一目了然了,不是一个对象指针对象。
因此默认写法肯定就没用了。
1
2
3
4
__strong NSObject *o;
__autoreleasing NSObject *temp = o;
[self method2:&temp]; //temp在方法内部延迟操作。
o = temp;//在这里赋值时temp还是nil,导致o始终是nil
所以先说结论,改成_strong
修饰二级指针之后,编译器不会再出现回写操作,我特意打了地址试了一下。
到此,我们可以知道,error
被__block
修饰以及被copy
到堆上,禁掉回写逻辑,从始至终都只有一个指针对象在传来传去,所以这段逻辑到此就没什么问题了。
后续
现在可以来讨论一下__autorelease
了,苹果为什么要这么设计。一道题考你对__autoreleasing和__block的理解 和clang原文其实已经讲得非常清楚了,但是第一次读还是觉得有点绕,用自己的话解释一下。
当方法有返回参数的时候,默认都是__autorelease
的,这个很好理解,尤其是在mrc
环境下当你return
一个对象 给外部调用时,默认应该是autorelease
状态的,当你返回出去的时候retainCount
是 + 1 ,+ 2 ,+3 …那你让谁来处理呢?这显然是不合理的。
所以,二级指针也是调用方法返回参数的其中一种方法,同样应该保持同样的性质。
而同在ARC和MRC两种环境下用这种方式互相调用的,苹果也有进一步的优化了。
我觉得这张图非常好,最近刚看到,绝对过目不忘。
鸣谢:
一道题考你对__autoreleasing和__block的理解