Block详解
Block
在OC
中占有很重要的地位,在苹果各个底层库里面也有大量运用,所以就很有必要了解它的构成、原理。Block
是开源的,这是下载地址。这里以libclosure-67为基础。
简介
在data.c
文件中,定义了6种类型的block
,分别为:
- void * _NSConcreteStackBlock[32] = { 0 };
- void * _NSConcreteMallocBlock[32] = { 0 };
- void * _NSConcreteAutoBlock[32] = { 0 };
- void * _NSConcreteFinalizingBlock[32] = { 0 };
- void * _NSConcreteGlobalBlock[32] = { 0 };
- void * _NSConcreteWeakBlockVariable[32] = { 0 };
他们是一个void *
类型的数组,占用256个字节,他们在内存中是连续分布的,值都为0。在iOS
环境中只会用到_NSConcreteStackBlock
、_NSConcreteGlobalBlock
、_NSConcreteMallocBlock
3个,其他3个是在GC
环境中用到。在编译阶段赋初值只会用到_NSConcreteStackBlock
、_NSConcreteGlobalBlock
,_NSConcreteMallocBlock
是在运行时调用_objc_retainBlock -> _Block_copy
用到的。
_NSConcreteGlobalBlock
在编译阶段基本上都会被初始化为_NSConcreteStackBlock
,只有在以下情况中,block
会初始化为_NSConcreteGlobalBlock
:
-
未捕获外部变量
在编译的时候,clang
会检查是否有引用外部变量,如果没有就被设置为_NSConcreteGlobalBlock
。 -
当需要布局(layout)的变量的数量为0
static
修饰的变量,它的layout
的数量就为0
先来定义一个最简单的block
,通过rewrite
看一下它的底层实现。
void (^log)(void) = ^(){}; log();
/*转化后*/
void (*log)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)log)->FuncPtr)((__block_impl *)log);
可以看到block
是一个函数指针变量,它的值是__main_block_impl_0
的地址。
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
再看__main_block_impl_0
,它其实就是一个结构体,初始化的时候给impl
、Desc
赋值,然后取这个新生成的结构体地址赋值给block
,同时还生成了一个__main_block_func_0
的函数,再把这个函数赋值为impl.FuncPtr
。block
的调用直接拿到FuncPtr
,然后执行__main_block_func_0
函数。
这样看起来挺详细的,也符合正常的代码逻辑,但是运行的时候真的是这样的吗?
通过hopper
查看它的可执行文件,查看定义block
的那个函数,发现block
变成___block_literal_global.52变量,执行体变成了___globalBlock_block_invoke函数,而且在调用block
之前还调用了objc_retainBlock
函数,函数体执行完后,又调了objc_storeStrong
函数。
下面所有的汇编都是通过
hopper
查看的,是x86
的架构,但是我也都有在xcode
运行查看at&t
的汇编,逻辑是一致。
_globalBlock:
...
0000000100001d48 lea rax, qword [___block_literal_global.52]
0000000100001d4f mov rdi, rax
0000000100001d52 call imp___stubs__objc_retainBlock
0000000100001d57 mov qword [rbp+var_8], rax
0000000100001d5b mov rax, qword [rbp+var_8]
0000000100001d5f mov rdi, rax
0000000100001d62 call qword [rax+0x10]
...
0000000100001d70 call imp___stubs__objc_storeStrong
...
再看下___block_literal_global.52的定义,发现它第一个值为__NSConcreteGlobalBlock
,第三个值为___globalBlock_block_invoke函数的地址,这跟block
的结构体 定义完全符合。同时也跟xcode
的反汇编一致。
objc_retainBlock
方法很简单,就是调用_Block_copy
,看它的源码会知道,如果aBlock->flags & BLOCK_IS_GLOBAL
直接把传进来的block
再返回,相当于什么都没做。
这里还有一个问题,在运行时通过xcode
查看这个block
时,发现它的isa
不是__NSConcreteGlobalBlock
,而是__NSGlobalBlock__
,这是啥呢?它从哪被改变了呢?
NSGlobalBlock
通过查找一些资料,发现是在CoreFoundation
动态库__CFInitialize
方法里面进行改变的。然后通过hopper
查看它的信息发现,__NSGlobalBlock
是一个类,继承于NSBlock
,我又查询了下__NSGlobalBlock__
,它也是个类,在libsystem_blocks.dylib
动态库里面,继承于__NSGlobalBlock
。
我再通过调式runtime
源码,看到__CFMakeNSBlockClasses
这个方法会处理__NSConcreteGlobalBlock
,再看它的汇编,它会把data.c
6种block
都处理掉,这里看下__NSConcreteGlobalBlock
的流程。
/*伪代码*/
Class __NSStackBlock = _objc_lookUpClass(“__NSGlobalBlock”);
objc_initializeClassPair_internal(__NSGlobalBlock, “__NSGlobalBlock__”, &__NSConcreteGlobalBlock, &__NSConcreteGlobalBlock+0x80);
这段代码作用就是把__NSConcreteGlobalBlock
的地址空间赋值为class
,通过参数赋值内容,其中类名是__NSGlobalBlock__
。
这样一来都清楚了,在APP
启动的时候都把对应的类赋值到他们的地址空间,所以哪怕isa
还是那6种类型,但是地址里面存的东西变成了相对应的类。
_NSConcreteStackBlock
block
被初始化成_NSConcreteStackBlock
类型,说明肯定有引用外部变量,外部变量又分为基础变量和自定义变量(对象)这2种情况,它们对block
的处理也都不同。
基础变量捕获
因为rewrite
不准,所以接下来都以汇编为例,但是它可做为参考。
直接上引用基础变量block
的汇编:
;stackBasicBlock:
...
000000010000157c lea rcx, qword [___block_descriptor_tmp.10]
0000000100001583 lea rdx, qword [___stackBasicBlock_block_invoke]
000000010000158a mov rsi, qword [__NSConcreteStackBlock_100002010]
0000000100001591 mov dword [rbp+var_4], 0xa
;开始构建block
0000000100001598 mov qword [rbp+var_38], rsi
000000010000159c mov dword [rbp+var_30], 0xc0000000
00000001000015a3 mov dword [rbp+var_2C], 0x0
00000001000015aa mov qword [rbp+var_28], rdx
00000001000015ae mov qword [rbp+var_20], rcx
00000001000015b2 mov edi, dword [rbp+var_4]
00000001000015b5 mov dword [rbp+var_18], edi
00000001000015b8 mov rdi, rax
; argument "instance" for method imp___stubs__objc_retainBlock
00000001000015bb call imp___stubs__objc_retainBlock
...
; 执行block
00000001000015ba call qword [rax+0x10]
...
; argument "value" for method imp___stubs__objc_storeStrong
00000001000015df lea rax, qword [rbp+var_10]
00000001000015e3 mov rdi, rax
; argument "addr" for method imp___stubs__objc_storeStrong
00000001000015e6 call imp___stubs__objc_storeStrong
...
前几行把block_descriptor、block_invoke、__NSConcreteStackBlock移动到寄存器,接下来开始把相关的变量移动到内存,然后调用objc_retainBlock
拷贝block
从栈上到堆上,最后再调用
objc_storeStrong
进行销毁block
。
因为基础变量是没有
_Block_descriptor_2
的,所以直接都返回了,不会再调用它的copy
方法。
自定义变量捕获
在编译阶段,block
的flags
的值一般为BLOCK_HAS_COPY_DISPOSE
、BLOCK_IS_GLOBAL
、BLOCK_HAS_SIGNATURE
,如果为BLOCK_HAS_COPY_DISPOSE
,都还会生成___copy_helper_block_:
、___destroy_helper_block_:
方法,用于拷贝和销毁捕获变量。
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime 正在 dealloc
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime 引用计数掩码,即从第 1 ~ 15 位是用来存引用计数的,第 0 位上面已经被用了
BLOCK_NEEDS_FREE = (1 << 24), // runtime 需要释放,即它现在在堆上
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler 是否有 copy / dispose 函数,copy 和 dispose 在 desc 中
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code block 有 C++ 的构造器
BLOCK_IS_GC = (1 << 27), // runtime 用了 GC,这个不用管,GC 已经被淘汰
BLOCK_IS_GLOBAL = (1 << 28), // compiler 是否处于全局区
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
// 返回值是否在栈上,如果没有签名,则它一定是 0
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler 是否有签名,签名是描述 block 的参数和返回值的一个字符串
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler 是否有扩展布局
};
这里定义一个捕获外部对象的block
,看一下相关的汇编代码,跟基础变量引用有什么不一样。
;_stackClassBlock:
...
;生成block结构
0000000100001669 lea rax, qword [___block_descriptor_tmp.16]
0000000100001670 lea rsi, qword [___stackClassBlock_block_invoke]
0000000100001677 mov rdi, qword [__NSConcreteStackBlock_100002010]
000000010000167e lea rcx, qword [rbp+var_38]
0000000100001682 add rcx, 0x20
0000000100001686 mov qword [rbp+var_38], rdi
000000010000168a mov dword [rbp+var_30], 0xc2000000
0000000100001691 mov dword [rbp+var_2C], 0x0
0000000100001698 mov qword [rbp+var_28], rsi
000000010000169c mov qword [rbp+var_20], rax
00000001000016a0 mov rax, qword [rbp+var_8]
00000001000016a4 mov rdi, rax
; argument "instance" for method imp___stubs__objc_retain
00000001000016a7 mov qword [rbp+var_40], rcx
00000001000016ab call imp___stubs__objc_retain
00000001000016b0 lea rcx, qword [rbp+var_38]
00000001000016b4 mov qword [rbp+var_18], rax
00000001000016b8 mov rdi, rcx
; argument "instance" for method imp___stubs__objc_retainBlock
; 里面会调用___copy_helper_block_ 调用objc_storeStrong 增加外部变量引用计数
00000001000016bb call imp___stubs__objc_retainBlock
00000001000016c0 mov qword [rbp+var_10], rax
00000001000016c4 mov rax, qword [rbp+var_10]
00000001000016c8 mov rcx, rax
00000001000016cb mov rdi, rcx
;调用block
00000001000016ce call qword [rax+0x10]
...
; 释放block 会调用___destroy_helper_block_方法 再次objc_storeStrong 减少外部变量引用计数
; argument "addr" for method imp___stubs__objc_storeStrong
00000001000016ff mov dword [rbp+var_44], eax
0000000100001702 call imp___stubs__objc_storeStrong
0000000100001707 xor eax, eax
0000000100001709 mov esi, eax
; 释放外部变量 减少引用计数
; argument "addr" for method imp___stubs__objc_storeStrong
0000000100001712 call imp___stubs__objc_storeStrong
0000000100001717 xor eax, eax
0000000100001719 mov esi, eax
; 释放外部变量
; argument "addr" for method imp___stubs__objc_storeStrong
0000000100001722 call imp___stubs__objc_storeStrong
...
外部对象引用变量比基础变量多了拷贝和销毁2步,而且在拷贝block
之前还retain
了外部变量,所以销毁的时候,objc_storeStrong
会走4次,1次block
,3次外部变量。
在
_objc_retainBlock
方法调用的时候,会调用Block_descriptor_2
的copy
方法,是_copy_helper_block_
方法,它是动态生成的。它会调用_objc_storeStrong
,旧值为一个地址,它的内容为0,新值为外部变量。
变量修饰符
通过上面2种情况,发现block
有2点限制,1个是block
定义后,在block
执行前改变外部变量的值,block
里面没有同步到,另外一个是在block
执行体里面不能修改外部变量的值,原因是在编译阶段,对外部变量引用是拷贝操作,一旦block
定义过了,就定死了,但是对于对象变量,可以修改对象内部空间的内容,因为对象的地址没有变,这就限制了很多使用的场景,那有没有办法解决上面2种情况呢? 答案是用static
和__block
修饰符来修饰外部变量。
static修饰符
static
修饰的变量是静态局部变量,它的初始值必须是编译期常量,如果有初始化,那么会存储在Section __data
段,如果没有初始化,那么会存储在Section __bss
段。static
修饰的变量会一直占用空间,不会释放,所以它的地址在编译过后,永远不会变,变的只是里面存储的内容。
; Section __data
; Range: [0x100002388; 0x10000238c[ (4 bytes)
; File offset : [9096; 9100[ (4 bytes)
; S_REGULAR
_stackStaticBasicBlock.num:
0000000100002388 dd 0x00000005
; Section __bss
; Range: [0x100002390; 0x1000023a0[ (16 bytes)
; No data on disk
; Flags: 0x1
; S_ZEROFILL
_stackClassBlock.per:
0000000100002390 dq 0x0000000000000000
_stackStaticClassBlock.per:
0000000100002398 dq 0x0000000000000000
在static
修饰基础变量里面可以看到,变量有初始值,放在__data
段,在修饰对象变量里面可以看到,变量没初始值,放在__bss
段,因为对象的本质就是一个结构体指针,所以修饰对象的空间都是占8个字节,在运行期,往里面存储对象alloc
出来的地址。这2个段,代码区都可以访问,所以说在block
执行体里面也可以访问,不用捕获,直接操作static
对象的地址就行,这样一来block
的那2个问题也都可以解决掉,可以在任意地方去修改,去访问最新的值。
stackStaticBasicBlock:
...
00000001000017f8 lea rax, qword [___block_literal_global.21]
00000001000017ff mov rdi, rax
; argument "instance" for method imp___stubs__objc_retainBlock
0000000100001802 call imp___stubs__objc_retainBlock
0000000100001807 mov qword [rbp+var_8], rax
000000010000180b mov dword [_stackStaticBasicBlock.num], 0xa
0000000100001815 mov rax, qword [rbp+var_8]
0000000100001819 mov rdi, rax
;执行block
000000010000181c call qword [rax+0x10]
000000010000181f xor ecx, ecx
0000000100001821 mov esi, ecx
...
; argument "addr" for method imp___stubs__objc_storeStrong
000000010000182a call imp___stubs__objc_storeStrong
...
在上面也说过,没有引用外部和static
修饰外部变量的都是_NSConcreteGlobalBlock
类型,从上面汇编也能看出来。汇编前面都很容易理解,就是最后为啥要调objc_storeStrong
来进行销毁呢?全局block
编译完成后是存储在__const
里面的,肯定不会销毁啊,我又debug
了一把,想起来block
一开始运行就变成了一个对象了,它有自定义release
方法,所以会调用自定义的方法,但是这个方法呢,是空的,直接return
了,所以说相当于啥都没干,但是呢,他把用到的栈空间给赋值为了0了。
上面的汇编是捕获基础变量的,但是跟对象是差不多的,区别是把alloc
出来的地址赋值static
变量,然后调用了一次objc_release
,把原来的变量地址内容给release
掉,不过一般里面都为0,所以也相当于什么都没做。
-[__NSGlobalBlock release]:
0000000000094390 push rbp
0000000000094391 mov rbp, rsp
0000000000094394 pop rbp
0000000000094395 ret
__block修饰符
__block
修饰符就是为了解决block
那2个场景而推出的,里面的实现也比较复杂(看汇编看了1天),其实思想挺简单,就是用Block_byref
结构对外部变量又包装了一层,然后把它的地址赋值给block
内存地址后8位,他们的内存结构都是在运行期赋值到栈空间里面的。
__block修饰基础变量
因为__block
修饰基本变量和自定义变量在编译期和运行期流程不太一样,所以这里先看基础变量,还是先上汇编代码。
_stackBlockBasicBlock:
...
;开始构建Block_byref结构
0000000100001998 mov qword [rbp+var_20], 0x0
00000001000019a0 lea rax, qword [rbp+var_20]
00000001000019a4 mov qword [rbp+var_18], rax
00000001000019a8 mov dword [rbp+var_10], 0x20000000
00000001000019af mov dword [rbp+var_C], 0x20
00000001000019b6 mov dword [rbp+var_8], 0xa
;开始构建block结构
00000001000019bd mov rcx, qword [__NSConcreteStackBlock_100002010]
00000001000019c4 mov qword [rbp+var_50], rcx
00000001000019c8 mov dword [rbp+var_48], 0xc2000000
00000001000019cf mov dword [rbp+var_44], 0x0
00000001000019d6 lea rcx, qword [___stackBlockBasicBlock_block_invoke]
00000001000019dd mov qword [rbp+var_40], rcx
00000001000019e1 lea rcx, qword [___block_descriptor_tmp.23]
00000001000019e8 mov qword [rbp+var_38], rcx
;把上面构建Block_byref的地址赋值到block的后8位
00000001000019ec mov qword [rbp+var_30], rax
;调objc_retainBlock方法
00000001000019f0 lea rdi, qword [rbp+var_50]
; argument "instance" for method imp___stubs__objc_retainBlock
00000001000019f4 call imp___stubs__objc_retainBlock
;给__block修饰变量赋值1 不是在栈上 已经copy过了,是在堆上了
00000001000019f9 mov qword [rbp+var_28], rax
00000001000019fd mov rax, qword [rbp+var_18]
0000000100001a01 mov dword [rax+0x18], 0x1
...
;调用block
0000000100001a18 call rcx
...
; argument "addr" for method imp___stubs__objc_storeStrong
0000000100001a2a call imp___stubs__objc_storeStrong
...
; argument #1 for method imp___stubs___Block_object_dispose
0000000100001a3b call imp___stubs___Block_object_dispose
...
上面总的block
结构已在xcode
上验证,如下图:
上面的重点是objc_retainBlock
这个方法,之后会调用_Block_copy
方法。这个方法在runtime.c
文件里面,在这里简单分析一下。
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
if (!arg) return NULL;
aBlock = (struct Block_layout *)arg;
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
} else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
} else {
// Its a stack block. Make a copy.
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return NULL;
memmove(result, aBlock, aBlock->descriptor->size);
// reset refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
result->isa = _NSConcreteMallocBlock;
return result;
}
}
通过flags
的与运算,判断block
的类型,如果是BLOCK_IS_GLOBAL
就直接返回。之后就是在堆上开辟aBlock->descriptor->size
大小的空间,memmove
这个方法会把从aBlock
地址开始的size
大小内容move
到result
地址的起始位置,这样就把栈上的内容拷贝到堆上面了。
_Block_call_copy_helper
方法会通过_Block_descriptor_2
拿到它的copy
方法进行调用,也就是编译时动态生成的__copy_helper_block_
方法。
___copy_helper_block_:
...
;处理参数,rdi是堆上block存储Block_byref地址的地址 rsi就是栈上Block_byref的地址
0000000100001ac8 mov edx, 0x8
0000000100001acd mov qword [rbp+var_8], rdi
0000000100001ad1 mov qword [rbp+var_10], rsi
0000000100001ad5 mov rsi, qword [rbp+var_10]
0000000100001ad9 mov rdi, qword [rbp+var_8]
0000000100001add add rdi, 0x20
...
0000000100001ae5 call imp___stubs___Block_object_assign
...
_Block_object_assign
会根据传入flags
的不同,进行不同的处理,这里的flags
表示的是引用外部变量的类型。flags
是第三个参数,对应的是rdx
,在这里是8,对应的是__block
类型,所以调用_Block_byref_copy
,参数是栈上的Block_byref
的地址,返回的是堆上的地址,再赋值给堆上block
的相应值。
enum {
//外部变量类型 flags值
BLOCK_FIELD_IS_OBJECT = 3, // id, NSObject, __attribute__((NSObject)), block, ...
BLOCK_FIELD_IS_BLOCK = 7, // a block variable
BLOCK_FIELD_IS_BYREF = 8, // the on stack structure holding the __block variable
BLOCK_FIELD_IS_WEAK = 16, // declared __weak, only used in byref copy helpers
BLOCK_BYREF_CALLER = 128, // called from __block (byref) copy/dispose support routines.
};
对外部变量的拷贝是通过_Block_byref_copy
函数处理的,这个函数的flags
的处理有点不太理解,意思知道,但是实现细节不太明白?这里不看汇编了,直接看代码。
static struct Block_byref *_Block_byref_copy(const void *arg) {
//传进的来是栈上的Block_byref地址
struct Block_byref *src = (struct Block_byref *)arg;
//引用计数为0时
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// 开辟空间
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
//看注释 引用计数是2 因为栈上的forwarding也引用它了
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;
//外部变量需要拷贝了走这个 不是Block_byref 是外部变量
if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
// Trust copy helper to copy everything of interest
// If more than one field shows up in a byref block this is wrong XXX
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;
if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
copy3->layout = src3->layout;
}
(*src2->byref_keep)(copy, src);
}
else {
//外部变量不需要拷贝 也就是基础变量 直接拷贝外部变量的值 到堆上
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
//已经在堆上 增加引用计数
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
//返回copy地址
return src->forwarding;
}
这里需要注意的是struct Block_byref
结构里面是没有外部变量的,所以在memmove
调用的时候把copy
和src
指针都加1,指向外部变量的地址,src->size - sizeof(*src)
也就是外部变量的所占空间大小。
__block修饰自定义变量
对于修饰自定义变量,在编译期,会增加4个方法,有2个Block_descriptor_2
的copy
、dispose
方法,在编译的时候会把他们指向新增加的2个方法,___copy_helper_block
和___destroy_helper_block
。还有2个是拷贝和销毁外部变量的方法,___Block_byref_object_copy_
、___Block_byref_object_dispose_
。
先来看下__block
修饰自定义变量这一行在汇编是什么样的?
_stackBlockClassBlock:
...
;开始在栈上构建__block的包装层 也就是Block_byref 占用了0x30字节
0000000100001b2b mov qword [rbp+var_30], 0x0 ;isa变量
0000000100001b33 lea rax, qword [rbp+var_30]
0000000100001b37 mov qword [rbp+var_28], rax ;forwarding变量
0000000100001b3b mov dword [rbp+var_20], 0x32000000 ;flags变量
0000000100001b42 mov dword [rbp+var_1C], 0x30 ;size变量
0000000100001b49 lea rax, qword [___Block_byref_object_copy_]
0000000100001b50 mov qword [rbp+var_18], rax ;变量copy函数地址
0000000100001b54 lea rax, qword [___Block_byref_object_dispose_]
0000000100001b5b mov qword [rbp+var_10], rax ;变量dispose函数地址
0000000100001b5f lea rax, qword [rbp+var_8]
0000000100001b63 mov rdi, qword [objc_cls_ref_Person]
; argument "instance" for method _objc_msgSend
0000000100001b6a mov rsi, qword [0x1000022f8]
; @selector(alloc), argument "selector" for method _objc_msgSend
0000000100001b71 mov rcx, qword [_objc_msgSend_100002020]
0000000100001b78 mov qword [rbp+var_78], rax
0000000100001b7c mov qword [rbp+var_80], rcx
调用alloc方法 通过_objc_msgSend
0000000100001b80 call rcx
; _objc_msgSend
0000000100001b82 mov rsi, qword [0x100002300] ; @selector(init)
0000000100001b89 mov rdi, rax
0000000100001b8c mov rax, qword [rbp+var_80]
;调用init方法 通过_objc_msgSend
0000000100001b90 call rax
;把生成自定义变量的堆地址 赋值给Block_byref 最后8位
0000000100001b92 mov qword [rbp+var_8], rax
跟修饰基础变量不一样,多了自定义变量的拷贝销毁函数的地址,而且最后8位是自定义变量的堆地址。block
的构建跟修饰基础变量一样,也是把Block_byref
它的地址,赋值到block
的后8位,占用40个字节,在Block_descriptor_1
里面有对应的字节数。
接下来的步骤跟__block
修饰基础变量一样,不过在_Block_byref_copy
方法里面,会进入if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE)
这个判断条件,然后调用___Block_byref_object_copy_
方法。
struct Block_byref_2 {
// requires BLOCK_BYREF_HAS_COPY_DISPOSE
void (*byref_keep)(struct Block_byref *dst, struct Block_byref *src);
void (*byref_destroy)(struct Block_byref *);
};
看了Block_byref_2
它的数据结构,一下子也都对应上了。
_Block_copy
_Block_copy_internal
malloc
memmove
_Block_call_copy_helper
_Block_descriptor_2
_Block_object_assign
_Block_byref_assign_copy
_Block_allocator
malloc
_Block_memmove
memmove
_Block_assign
_objc_storeStrong
_objc_release
_Block_release
_Block_call_dispose_helper
_Block_descriptor_2
_Block_object_dispose
_Block_byref_release
_Block_destructInstance
_Block_deallocator = free
_Block_object_dispose
_Block_byref_release
_Block_deallocator = free
外部变量捕获
block
是一个执行块,那么肯定有和外部变量交互的情况,这里简单改下block
,看看它的实现又有那些改变。
int num = 10;
void (^log)(int a) = ^(int x){
x = num;
};
log(5);
/*转化后*/
int num = 10;
void (*log)(int a) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, num));
((void (*)(__block_impl *, int))((__block_impl *)log)->FuncPtr)((__block_impl *)log, 5);
可以看到block
直接把外部变量num
传进去了,再看下__main_block_impl_0
结构。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int num;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0) : num(_num)
...
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int x) {
int num = __cself->num; // bound by copy
x = num;
}
__main_block_impl_0
结构体增加了一个外部变量,然后__main_block_func_0
执行体通过__cself
参数拿到变量。从这里能够看出,在定义block
的时候,已经把外部变量传进去了,再改变外部变量,block
里面外部变量的值也不会改变的。
那有没有办法解决这个问题呢?答案是外部变量用static
修饰就行了,如static int num = 10;
。
static int num = 10;
void (*log)(int a) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &num));
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *num;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_num, int flags=0) : num(_num)
...
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int x) {
int *num = __cself->num; // bound by copy
x = (*num);
}
用static
修改外部变量后,捕获变量时,赋值的是它的指针。block
执行体里面用到外部变量时,是通过地址获取它的值,所以哪怕定义block
后再改变外部变量的值,函数里面也是最新的值。
__Block修饰符
block
函数捕获外部变量后,那能改变它的值吗?正常的捕获是不能改变的,除非用修饰符来修饰外部变量,如上面介绍的static
,还有接下来介绍的__block
。
__block int num = 10;
void (^log)(int a) = ^(int x){
x = num;
num = 100;
};
num = 1;
log(5);
/*转化后*/
__attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 10};
void (*log)(int a) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344));
(num.__forwarding->num) = 1;
((void (*)(__block_impl *, int))((__block_impl *)log)->FuncPtr)((__block_impl *)log, 5);
__block
转化成了__attribute__((__blocks__(byref)))
,但是不知道它的作用是什么,网上我也没查到相关资料。
通过
gcc -dM -E - < /dev/null
命令查看,可以看到有#define __block __attribute__((__blocks__(byref)))
。
int
类型转化成了__Block_byref_num_0
,然后后面就是给它的初始化,block
捕获外部变量是__Block_byref_num_0
变量的指针。
struct __Block_byref_num_0 {
void *__isa;
__Block_byref_num_0 *__forwarding;
int __flags;
int __size;
int num;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_num_0 *num; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_num_0 *_num, int flags=0) : num(_num->__forwarding)
...
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int x) {
__Block_byref_num_0 *num = __cself->num; // bound by ref
x = (num->__forwarding->num);
(num->__forwarding->num) = 100;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src)
{
_Block_object_assign((void*)&dst->num, (void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
__Block_byref_num_0
是一个结构体,__forwarding
是一个指向本身类型的指针。
通过__main_block_impl_0
的初始化,可以看到num
变量的值是外部传过来_num
指针的__forwarding
变量。因为__forwarding
还是指向num
的地址,所以其实相当于直接传入外部变量的地址。
__block
还发生了一些改变:
1、生成了__main_block_copy_0
、__main_block_dispose_0
函数。
2、__main_block_desc_0
结构体多了copy
、dispose
函数指针,值是上面2个函数。
网友评论