本来想把标题命名为 【OC 中 Block 的本质】。
废话不多说,直接往下看。
一、Block 捕获变量的地址变了
有几句简单的代码,望君记下:
NSObject* obj;
printf("1 = %p\n", &obj);
void (^block)(void) = ^{
printf("2 = %p\n", &obj);
};
printf("3 = %p\n", &obj);
block();
printf("4 = %p\n", &obj);
接下来,为了简单方便,直接使用图片了。
场景一
除了红框框中的打印,其它的都一样。换句话说,在 block 中的 obj 的地址变了,同一个东西,地址尽然还变了,这是什么个情况???同时也要注意地址变化的位置,貌似相隔甚远呐。
场景二
是的、没有看错,相比于场景一,就多了一个 __block 修饰符。除了 block 定义之前的都变了,尤其是第3个,block 还没被执行呢,还跟着凑什么热闹,尽然也变了。同样,也看一下变化的地址。
场景三
是的、你依然没有看错,仅仅是变了一个修饰符 static。这一次就更加的厉害了,都没有变。但是,不要忘记了看看这一次的地址,与场景一、二的有什么不同。答案是长度不同,对的、可以这么回答。
说吧
看完了上面的三张图片,你就没有什么要跟我说的吗?好吧、不说的话,就直接看最后的总结吧。
本节小节
你知道这到底是为什么吗?看完了下面的内容、你应该能明白。
二、Block 的内部结构
涛声依旧,简单代码、望君记下:
// block 测试代码
- (void)testBlock {
void (^hgBlock)(void) = ^{
NSLog(@"block 中就一句打印");
};
}
很简单的代码,定义了一个 block,啥也没干,也没有调用。
接下来看看这段代码的背会到底隐藏着什么,通过如下指令转成 cpp 代码:
mxcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 HGObject.m -o HGObject.cpp
你在 cpp 文件中,应该能找到以下这些代码。
代码一:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
代码二:
static struct __HGObject__testBlock_block_desc_0 {
size_t reserved;
size_t Block_size;
} __HGObject__testBlock_block_desc_0_DATA = { 0, sizeof(struct __HGObject__testBlock_block_impl_0)};
关键的代码三:
struct __HGObject__testBlock_block_impl_0 {
struct __block_impl impl;
struct __HGObject__testBlock_block_desc_0* Desc;
__HGObject__testBlock_block_impl_0(void *fp, struct __HGObject__testBlock_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
总的来说,上面定义的一个 hgBlock 主要生成了上面核心的代码。其中 __HGObject__testBlock_block_impl_0 就是 hgBlock 的完整结构。通过其命名也能看出来:impl 主要是 block 调用时候用的,可以自行实验。Desc 就是这个 Block 的描述信息。所以每次研究 Block 的结构,主要就是看 impl 与 Desc 的变化,这两个地方是会改变的,尤其是当 Block 使用到 Block 以外的非全部变量的时候。
其实现在就可以将第一小节的各种场景都试一下,相信你能看出其中的 奥妙。
三、强/弱引用
很多时候总是会说,Block 强引用了某个指针变量、或者说 Block 弱引用了某个指针变量。这是咋回事?这个问题是不是很简单?其实不简单!
通常在 Block 中使用了一个强指针,我们就会说这个 Block 强引用了这个指针,弱指针则同理。
要想策底的说明这个问题,其实就是要验证在 Block 中使用了一个强指针之后到底发生了什么事情。凭什么说在 Block 中使用了,就有引用关系了。直接看下面的图片吧。
场景一:强引用
// block 测试代码
- (void)testBlock {
void (^hgBlock)(void) = ^{
self;
};
}
是的在 hgBlock 中仅仅是使用了一下 self 而已。
注意观察 红框框,在 impl 中多了这么一句:
HGObject *const __strong self;
一定要注意这个是 自动 生成的,换句话来说只要是在 Block 中使用了一个 强指针,那么在这个地方就会有一个对应类型的指针生成。也可以自行试一下第一小节的情况。
如果你试过了第一小节,你会发现是没有上图中(__HGObject__testBlock_block_desc_0)第二个 红框框 中的内容的。这个也很容易理解,这两个函数的出现主要是处理内存相关的。接下来简单的介绍一下关于 Block 中的内存问题。
Block 会出现在三个地方:栈区、全局区域堆区。具体的规律是:没有访问 auto 变量的都在全局区、访问了 aotu 变量的在栈区,当在栈区的 Block 调用了 copy 之后就会变成了堆区。
当你按照上面的规律试验的时候,你会发现根本不对,不会出现在栈区的情况。你可能使用的是 ARC环境了,你换成 MRC 环境试试,因为 ARC 环境会自动的将栈区的 Block 变量执行以下 copy 函数,所以就直接在堆区了。注意:我说的是变量,而不是说直接在栈区就 copy 了。可以不使用变量接收,而直接使用 NSLog 打印就知道了。
在上面也说到了一个 copy 函数,其实说的就是 Desc 中的那个 copy 了。当然了还有一个 dispose 这个也很理解,这个是在这个 Block 销毁的时候对引用的这个对象做 release 操作的。
说了这么多,别忘了,这个场景的主题是 强引用。
场景二:弱引用
// block 测试代码
- (void)testBlock {
__weak typeof(self) weakSelf = self;
void (^hgBlock)(void) = ^{
weakSelf;
};
}
是的、这是一个和常规的 weakSelf 的声明方式。看一下转成 cpp 后的结果:
果真不负众望,出现了这个:
HGObject *const __weak weakSelf;
仔细想想、其实也对。
本节小节
注意:这个小节主要介绍的是 Block 中的强/弱引用、其实我也没有打算介绍多么深奥的东西。
四、__block 隐藏在背后的秘密
这是一个很神秘的变量,具体的作用,应该没有不清楚的。具体的看代码吧:
// block 测试代码
- (void)testBlock {
__block int a = 10;
void (^hgBlock1)(void) = ^{
a = 18;
NSLog(@"hgBlock1 = %p", &a);
};
void (^hgBlock2)(void) = ^{
NSLog(@"hgBlock2 = %p", &a);
};
hgBlock1();
hgBlock2();
}
这个就有点复杂了,两个 Block 同时使用了一个 __block 变量。
转成 cpp 如下:
上图中仅仅是其中一个 Block 的结构,另一个也类似。但是新出来一个东西:__Block_byref_a_0,你仔细的话,还会发现另一个 Block 是共用__Block_byref_a_0 的。当你执行上面的代码、你还会惊奇的发现,里面的 &a 的值都是一样的。是的,他们不仅共用同一个数据结构,还共用同一个 __Block_byref_a_0 对象。在 __Block_byref_a_0 中还有一个比较有意思的指针 __forwarding, 这个指针就是当前 __Block_byref_a_0 对象的地址,可以通过转成 cpp 代码中寻找具体的逻辑。这样设计的目的是考虑到到当一个栈区的 Block 调用 copy 之后,在栈区与堆区共用同一个变量。具体可以查看下面的图:
到这里,__block 关键字背后隐藏的那些密码、也差不多介绍了 30%了,剩余的 70% 可以自行慢慢的体会。
五、typeof 引发的思考
其实在上面 弱引用 的介绍中、已经使用到 typeof 关键字了,通过上面对 强/弱引用 的介绍。请看下面的代码:
// block 测试代码
- (void)testBlock {
__weak typeof(self) weakSelf = self;
void (^hgBlock1)(void) = ^{
__strong typeof(self) self = weakSelf;
};
}
厉害了,这不是在 hgBlock 强引用 self 了么?理由是这一个 typeof(self)。确实是引用了 self 变量,但是 type 有两个特性:类型替换,并且是发生在编译期,然而 Block 对指针的引用决定于运行期,当生成 .......impl 结构体的时候,self 已经做完了类型替换了。
可以看一下 cpp 后的结果:
只有对 weakSelf 的弱引用,没有对 self 的强引用。
但是通常我是这么写的:
__strong typeof(weakSelf) self = weakSelf;
所以第一次看到这里写成 typeof(self),还是惊讶的,但是其实是没有影响的。
六、一个小实验
有这样的一个结构体:
// 结构体
struct HGStruct {
void *isa;
int a;
void (*func)(void);
};
再有这样的一个测试代码:
// 结构体测试
- (void)structTest {
struct HGStruct *s = nil;
s->func();
}
运行代码:
话不多说,直接看 Block 解开多年来的误解,这个问题我已经在很多的场合提到了。这个问题是我第一次离职、第一次面试的第二轮遇到的面试题。记忆深刻啊、经历过后才发现 找工作 不是想的那么简单。
但是仔细观察,你会发现上面的定义的这个 HGStruct 结构体与 __block_impl还相差一个成员变量,为什么 Crash 的值是一样的呢。这是我故意的,因为这里关系到一个地址对齐的技术点。
其次在上面的代码中,我是故意的将 s 设置成的 nil。这个应该是编译器的初始化问题,这种 C 结构体的指针的默认值并不是 nil,这一点不像 OC 的对象。
接下来给一到练习题吧。
练习题、也是思考题(注意区分 ARC 与 MRC)
今天无意间有发现了这样的问题,代码如下:
- (NSArray *)createBlockArray {
NSInteger i = 1;
NSInteger j = 10;
return [NSArray arrayWithObjects:^{
NSLog(@"%ld", i);
}, ^{
NSLog(@"%ld", j);
}, nil];
}
MRC 环境的运行效果是这样的:
ARC 环境的运行效果是这样的:
是的、ARC 环境也 Crash 了,但是 Block 正常执行了,最后也 Crash 了,并且地址的值 都是 0x20。
那么问题来了:
1、请分析上面导致的原因。
2、将 createBlockArray 中的数组换成 @[。。。。] 的方式,再分别在 A/MRC 环境试试会有什么不一样。
提醒一下:Block 所在哪个数据区(栈、堆与全局)?
这里也得出一个结论:不是所有的 Block 闪退都是 0x10。
七、总结
虽然也就是简单的 Block 相关介绍,但是通过上面的一些操作还是依旧可以再学习到更多的东西、更多的信息还得自行慢慢品味。