Block原理详解

Block详解

BlockOC中占有很重要的地位,在苹果各个底层库里面也有大量运用,所以就很有必要了解它的构成、原理。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_NSConcreteMallocBlock3个,其他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,它其实就是一个结构体,初始化的时候给implDesc赋值,然后取这个新生成的结构体地址赋值给block,同时还生成了一个__main_block_func_0的函数,再把这个函数赋值为impl.FuncPtrblock的调用直接拿到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.c6种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_descriptorblock_invoke__NSConcreteStackBlock移动到寄存器,接下来开始把相关的变量移动到内存,然后调用objc_retainBlock拷贝block从栈上到堆上,最后再调用
objc_storeStrong进行销毁block

因为基础变量是没有_Block_descriptor_2的,所以直接都返回了,不会再调用它的copy方法。

自定义变量捕获

在编译阶段,blockflags的值一般为BLOCK_HAS_COPY_DISPOSEBLOCK_IS_GLOBALBLOCK_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_2copy方法,是_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上验证,如下图:

blockaddress

上面的重点是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大小内容moveresult地址的起始位置,这样就把栈上的内容拷贝到堆上面了。

_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调用的时候把copysrc指针都加1,指向外部变量的地址,src->size - sizeof(*src)也就是外部变量的所占空间大小。

__block修饰自定义变量

对于修饰自定义变量,在编译期,会增加4个方法,有2个Block_descriptor_2copydispose方法,在编译的时候会把他们指向新增加的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结构体多了copydispose函数指针,值是上面2个函数。