在第一篇文章中,我们探索了block的结构,现在我们来看下block是如何引用其外部变量的。
block是如何引用外部变量的
block如何捕获外部变量
首先将之前的代码修改下,让block在执行的时候需要使用到外部的变量,代码如下:
typedef void(^BlockA)(void);
void foo(int);
void runBlockA(BlockA block) {
block();
}
void doBlockA() {
int a = 128;
BlockA block = ^{
foo(a);
};
runBlockA(block);
}
然后使用相同的命令生成汇编指令,可以看到汇编指令比原来多了很多,其中最主要的是block已经不像之前那样在生成汇编的时候就已经定义好了,现在block是在汇编运行的时候动态生成的。
首先是runBlockA的汇编代码,与之前并没有改变,直接略过,下面是doBlockA的汇编指令:
_doBlockA:
@ BB#0:
push {r7, lr}
mov r7, sp
sub sp, #32
mov r0, sp
movw r1, :lower16:(___block_descriptor_tmp-(LPC1_0+4))
movt r1, :upper16:(___block_descriptor_tmp-(LPC1_0+4))
LPC1_0:
add r1, pc
movw r2, :lower16:(___doBlockA_block_invoke-(LPC1_1+4))
movt r2, :upper16:(___doBlockA_block_invoke-(LPC1_1+4))
LPC1_1:
add r2, pc
movs r3, #0
movw r9, #0
movt r9, #49152
movw r12, :lower16:(L__NSConcreteStackBlock$non_lazy_ptr-(LPC1_2+4))
movt r12, :upper16:(L__NSConcreteStackBlock$non_lazy_ptr-(LPC1_2+4))
LPC1_2:
add r12, pc
ldr.w r12, [r12]
movw lr, #128
str.w lr, [sp, #28]
str.w r12, [sp]
str.w r9, [sp, #4]
str r3, [sp, #8]
str r2, [sp, #12]
str r1, [sp, #16]
ldr r1, [sp, #28]
str r1, [sp, #20]
str r0, [sp, #24]
ldr r0, [sp, #24]
bl _runBlockA
add sp, #32
pop {r7, pc}
.p2align 1
.code 16 @ @__doBlockA_block_invoke
.thumb_func ___doBlockA_block_invoke
为了能够更好地理解这段代码,首先要注意的是sub sp, #32这个指令,表示在栈内存中分配了32个字节;然后将block_descriptor_tmp赋值到r1,将doBlockA_block_invoke赋值到r2,最后将NSConcreteStackBlock赋值到r12,但是这个时候block的整个结构体还没有确定,应该是在运行的时候动态连接出来的,也就是下面的代码执行的内容
movw lr, #128
str.w lr, [sp, #28]
str.w r12, [sp]
str.w r9, [sp, #4]
str r3, [sp, #8]
str r2, [sp, #12]
str r1, [sp, #16]
ldr r1, [sp, #28]
str r1, [sp, #20]
str r0, [sp, #24]
ldr r0, [sp, #24]
首先是将block引用的128这个数存入lr中,然后将lr中的值也就是128存入到栈内存偏移28字节的地方;r12(也就是NSConcreteStackBlock)存入栈内存的起始位置;r9也就是(49152)存入到栈内存偏移4字节的位置;r3(也就是0)存入栈内存便宜8字节的位置;r2(也就是doBlockA_block_invoke)存入栈内存便宜12字节的位置;r1(也就是block_descriptor_tmp)存入栈内存偏移16字节的位置。
结下将栈内存偏移28,也就是之前存储的128存入r1,然后将r1再写入栈内存偏移20的位置;r0写入栈内存偏移24的位置。因此到目前为止可以确定目前栈内存中地址从低到高(前24个字节)依次存储了一个block结构体和抓取到的外部变量128。最后以这个24个字节的结尾的位置赋值为r0,开始执行runblockA。
现在我们可以看到,现在运行runblockA的时候,参数不是20个字节,而是24个字节,因为多了一个捕获的外部变量;而且从___doBlockA_block_invoke的汇编中也可以看到,在最终执行_foo之前,r0只取了最后的4个字节。
___doBlockA_block_invoke:
@ BB#0:
push {r7, lr}
mov r7, sp
sub sp, #8
str r0, [sp, #4]
mov r1, r0
str r1, [sp]
ldr r0, [r0, #20]
bl _foo
add sp, #8
pop {r7, pc}
如果引用的是对象类型呢
刚才的例子block捕获的是值类型,如果改成引用类型呢:
#import <Foundation/Foundation.h>
typedef void(^BlockA)(void);
void foo(NSString *);
void runBlockA(BlockA block) {
block();
}
void doBlockA() {
NSString *a = @"A";
BlockA block = ^{
foo(a);
};
runBlockA(block);
}
使用同样的命令生成汇编后,会发现差异最大的是___block_descriptor_tmp,多了__copy_helper_block和__destroy_helper_block,分别对应第一篇中提到的Block_descriptor中的copy和dispose方法;
___block_descriptor_tmp:
.long 0 @ 0x0
.long 24 @ 0x18
.long ___copy_helper_block_
.long ___destroy_helper_block_
.long L_.str.1
.long 256 @ 0x100
这两个方法应该是在block进行拷贝和销毁的时候对其捕获到的引用类型的对象执行相应的操作。
block的类型
到目前为止,从汇编的代码来看,已经确认了两种block类型,一种是在编译的时候已经确定好的___block_literal_global类型,另一种是在运行的时候动态链接的L__NSConcreteStackBlock类型。
其实block一共有三种类型:
_NSConcreteGlobalBlock:就是第一篇文章中的block类型,在编译的时候就已经全局存在,而且也不需要获取任何外部变量
_NSConcreteStackBlock:就是这篇文章中的block类型,分配在栈中。几乎所有堆中的block都是在栈中先生成然后再拷贝到堆中去,也就是这种类型的block是堆中block的前身。
_NSConcreteMallocBlock:分配在堆中的block,当对block执行拷贝操作的时候,他们会被分配到堆中,并根据引用计数来确定要不要销毁。
网友评论