Block原理详解

作者: Joolybgo | 来源:发表于2018-10-10 16:27 被阅读43次

    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个函数。

    相关文章

      网友评论

        本文标题:Block原理详解

        本文链接:https://www.haomeiwen.com/subject/crxmaftx.html