美文网首页
iOS Objective-C Block底层原理

iOS Objective-C Block底层原理

作者: just东东 | 来源:发表于2020-11-25 14:25 被阅读0次

    iOS Objective-C Block底层原理

    在上一篇文章中我们对Block做了简单的介绍,下面我们通过这篇文章对Block的底层原理进行探索。

    首先提出问题:

    1. Block的本质是什么?
    2. Block为什么需要调用block()
    3. Block是如何截获外界变量的?
    4. __block是如何实现的?

    1. 通过Clang查看Block的底层实现

    1.1 编译后的代码简单分析

    要想知道Block的底层实现,我们首先想到的就是通过Clang编译一下Block代码,然后看看其内部的实现。我们创建一个block.c的文件,内部代码如下:

    #include "stdio.h"
    
    int main(){
        
        void(^block)(void) = ^{
            printf("hello block");
        };
        
        block();
        return 0;
    }
    

    通过xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc block.c命令,将.c文件编译成.cpp文件,我们找到main函数进行查看,编译后的形式如下:

    int main(){
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        return 0;
    }
    

    去除掉类型强转,可以将编译后的代码简化成如下形式:

    int main(){
        void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
    
        block->FuncPtr(block);
        return 0;
    }
    

    通过简化后的代码我们可以看出Block等于__main_block_impl_0函数,该函数有两个参数,其中第一个参数__main_block_func_0就是我们在Block代码块中写的代码。其编译后的实现如下:

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        printf("hello block");
    }
    

    1.2 __main_block_impl_0

    我们在编译后的.cpp文件内搜索__main_block_impl_0便可找起实现,下面我们来看看其实现:

    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;
      }
    };
    

    可以看到__main_block_impl_0是一个结构体,在该结构体中第一个参数是一个__block_impl类型的imp__block_impl源码如下:

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    

    __block_impl内有四个变量:

    • isa: 类似于OC对象的isa,在这个例子中指向_NSConcreteStackBlock,实际就是指向那种类型的Block,相当于指向类
    • Flags:这里是0
    • Reserved: 保留字段
    • FuncPtrBlock代码块的函数指针,通过该指针调用block

    1.3 Block的调用

    在编译后的代码中我们可以看出Block的调用是通过block->FuncPtr(block)来进行的。

    • 可以看出block内部声明了一个__main_block_func_0的函数;
    • __main_block_impl_0中传入的第一个参数就是__main_block_func_0
    • 在其内部用fp表示,然后赋值给implFuncPtr属性;
    • 所以我们可以可以通过block->FuncPtr(block)来进行调用Block

    通过对Clang编译的源码进行查看,在block内部并不会自动调用,所以我们需要调用底层生成的函数__main_block_func_0,才能实现block的调用

    1.4 Block捕获外界变量

    1.4.1 仅使用变量

    上面我们分析了一个最简单的Block,没有任何的与外界交互,如果与外界交互时,我们的Block又会是什么样呢?

    这里我们同样使用Clang去编译一个可以捕获外界变量的Block,实现代码如下:

    #include "stdio.h"
    
    int main(){
        int a = 123;
        void(^block)(void) = ^{
            printf("hello block a = %d",a);
        };
        
        block();
        return 0;
    }
    

    编译后的结果:

    int main(){
    
        int a = 123;
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
    
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        return 0;
    }
    
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int a = __cself->a; // bound by copy
    
        printf("hello block a = %d",a);
    }
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int a;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    通过以上编译后的代码我们可以看出,Block如果想要捕获外界变量,就会在其内部创建一个同名变量来存储从外界捕获的变量。并在__main_block_func_0中取出捕获的变量,以供函数调用的时候使用。

    1.4.2 修改变量 (__block)

    如果我们使用__block修饰外界变量,并在Block中修改了变量是什么样子呢?

    我们修改代码为如下,然后通过Clang去编译:

    #include "stdio.h"
    
    int main(){
        
        __block int a = 123;
        void(^block)(void) = ^{
            a = 10;
            printf("hello block a = %d",a);
        };
        
        block();
        return 0;
    }
    

    编译后的结果:

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    int main(){
    
        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 123};
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
    
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        return 0;
    }
    

    通过以上编译后的代码我们可以看到,对于__block修饰的变量在底层被编译成了__Block_byref_a_0类型的结构体:

    struct __Block_byref_a_0 {
      void *__isa;
    __Block_byref_a_0 *__forwarding;
     int __flags;
     int __size;
     int a;
    };
    

    在这个结构体中我们可以通过一个叫做__forwarding的成员变量来间接访问我们定义的变量。

    在此处生成的__main_block_impl_0结构体中,变量a也是取的__Block_byref_a_0类型的结构体指针。生成代码如下:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_a_0 *a; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    对于__main_block_func_0中的变量a也同样是取的a的地址进行修改其中的值。代码如下:

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        __Block_byref_a_0 *a = __cself->a; // bound by ref
    
        (a->__forwarding->a) = 10;
        printf("hello block a = %d",(a->__forwarding->a));
    }
    

    综上所述对于使用__block修饰的变量,在通过Clang编译后又如下结论:

    1. 对于外界的变量会编译为__Block_byref_name_0的结构体,其中name是外界变量的名称
    2. 结构体内会保存外界变量的指针
    3. 通过结构体内的__forwarding的成员变量来间接访问我们定义的变量。
    4. 对于编译后的block结构体__main_block_impl_0内部也会存储一个外界变量的__Block_byref_name_0类型的结构体指针
    5. 通过block结构体作为参数传递给生成的__main_block_func_0对外界变量进行访问。

    所以此处并不是像2.1中的那样只是创建了一个同名的变量那样简单,在这两节中分别使用了值拷贝和指针拷贝两种方法:

    • 值拷贝:也就是浅拷贝,只拷贝数值,且拷贝的值不可更改,指向不同的内存空间
    • 指针拷贝:也就是深拷贝,生成的对象与原对象指向同一片内存空间,在一处修改的时候另一处也会被修改

    1.5 小结

    通过上面的分析我们可以得出如下结论:

    1. Block在底层是一个结构体,同样也可以使用%@打印,所以也可以理解为对象
    2. Block需要调用是因为Block代码块在底层是一个函数,要想让其执行,所以需要调用
    3. Block捕获外界变量时,会自动生成一个同名属性
    4. Block捕获并修改外界变量时,会生成一个__Block_byref_name_0的结构体,并通过一个叫做__forwarding的成员变量来间接访问我们定义的变量
    5. 所以__block的原理是生成响应的结构体,保存原始变量的指针和值,传递一个指针地址给Block

    2. Block底层探索

    2.1 查找Block的底层实现

    通过以上对于Clang编译后Block的探索后我们对Block有了初步的了解,但是我们还是想知道Block在底层的真正的实现,以及找一份开源代码进行研究,下面我们通过汇编去寻找一下Block的底层实现和实现库的位置。

    我们创建一个iOS工程编写一段Block代码,并添加如下断点,然后开启汇编调试Debug->Debug Workflow->Always Show Disassembly

    16061153947068.jpg

    运行程序后我们发现一个符号symbolobjc_retainBlock这里的汇编代码是call说明调用了这个符号,我们在这行汇编代码处添加断点,如下图:

    16061153523231.jpg

    过掉原本的断点,来到上面这行处,然后按住command鼠标点击断点处的向下的小箭头来到如下图所示的汇编代码处:

    16061153729653.jpg

    通过上面的图片我们可以知道此处又继续调用了_Block_copy,然后我们添加_Block_copy符号断点。过掉上面的断点来到如下图所示的汇编处:

    16061164559120.jpg

    通过上面这张图片我们可以看到_Block_copy实现于libsystem_blocks.dylib源码中。

    我们可在Apple Opensource中下载各个版本的libclosure源码。这里推荐一下LGCooci老师的libclosure-74-KCBuild,可以编译运行的libclosure,可以运行并断点调试Block底层的libclosure-74源码。

    2.2 Block_layout

    2.2.1 Block_layout源码及分析

    首先我们全局搜索_Block_copy找到它的源码如下:

    _Block_copy源码:

    // Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
    void *_Block_copy(const void *arg) {
        struct Block_layout *aBlock;
    
        if (!arg) return NULL;
        
        // The following would be better done as a switch statement
        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 =
                (struct Block_layout *)malloc(aBlock->descriptor->size);
            if (!result) return NULL;
            memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
    #if __has_feature(ptrauth_calls)
            // Resign the invoke pointer as it uses address authentication.
            result->invoke = aBlock->invoke;
    #endif
            // 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;
        }
    }
    

    从该函数的第一行代码中我们看到了个Block_layout,那么我们首先来看看Block_layout这个结构体是什么,其实这就是我们block底层的真正实现,源码如下:

    struct Block_layout {
        void *isa;
        volatile int32_t flags; // contains ref count
        int32_t reserved;
        BlockInvokeFunction invoke;
        struct Block_descriptor_1 *descriptor;
        // imported variables
    };
    
    • isa:指向block类型的类,就是那几种block
    • flags:标识符,是一种位域结构,按位表示block的一些信息
    • reserved:保留字段
    • invoke:函数指针,指向具体的block实现的调用地址
    • descriptorblock的附加信息(其实还有Block_descriptor_2Block_descriptor_3

    2.2.2 flag 分析

    _Block_copy函数中我们可以看到aBlock->flags & BLOCK_NEEDS_FREE,说明flagBLOCK_NEEDS_FREE相关,我们跳转到BLOCK_NEEDS_FREE找到如下枚举代码:

    enum {
        BLOCK_DEALLOCATING =      (0x0001),  // runtime
        BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
        BLOCK_NEEDS_FREE =        (1 << 24), // runtime
        BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
        BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
        BLOCK_IS_GC =             (1 << 27), // runtime
        BLOCK_IS_GLOBAL =         (1 << 28), // compiler
        BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
        BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
        BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
    };
    

    通过以上枚举代码我们可以看到这些枚举值分别带表着block的不同信息:

    • 第1位BLOCK_DEALLOCATING:释放标记,一般常用 BLOCK_NEEDS_FREE按位与操作,一同传入 Flags,表示该block是否可以释放。
    • 第16位BLOCK_REFCOUNT_MASK:存储引用计数的值,是一个可选用的参数
    • 第24位BLOCK_NEEDS_FREE:低16位是否有效的标志,程序根据它来决定是否增加或较少引用计数位的值
    • 第25位BLOCK_HAS_COPY_DISPOSE:是否拥有拷贝辅助函数a copy helper function
    • 第26位BLOCK_HAS_CTOR:是否拥有block析构函数
    • 第27位BLOCK_IS_GC:标志是否有垃圾回收,应用于OS X
    • 第28位BLOCK_IS_GLOBAL:标志是否是全局Block
    • 第29位BLOCK_USE_STRET:与30位相反,判断当前Block是否拥有一个签名,用于runtime时动态调用。
    • 第30位BLOCK_HAS_SIGNATURE:与29位相反,判断当前Block是否拥有一个签名,用于runtime时动态调用。
    • 第31位:BLOCK_HAS_EXTENDED_LAYOUT:标志block是否有扩展

    2.2.3 descriptor 分析

    descriptorblock的附加信息,首先在``中看到的是Block_descriptor_1,我们跳转过去可以看到如下代码:

    #define BLOCK_DESCRIPTOR_1 1
    struct Block_descriptor_1 {
        uintptr_t reserved;// 保留信息
        uintptr_t size;// block大小
    };
    
    #define BLOCK_DESCRIPTOR_2 1
    struct Block_descriptor_2 {
        // requires BLOCK_HAS_COPY_DISPOSE
        BlockCopyFunction copy;//拷贝函数指针
        BlockDisposeFunction dispose;// 销毁
    };
    
    #define BLOCK_DESCRIPTOR_3 1
    struct Block_descriptor_3 {
        // requires BLOCK_HAS_SIGNATURE
        const char *signature;// 签名
        const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT 依赖于block扩展布局
    };
    

    这里的Block_descriptor_1是必选的Block_descriptor_2Block_descriptor_3不是必选的:

    Block_descriptor_2需要flags是:BLOCK_HAS_COPY_DISPOSE才会存在,Block_descriptor_3需要flagsBLOCK_HAS_SIGNATUREBLOCK_HAS_EXTENDED_LAYOUT才会存在。

    我们在Block_layout中只看到Block_descriptor_1那么是怎么访问Block_descriptor_2Block_descriptor_3的呢?我们可以在其构造方法中找到答案,就是经过内存平移访问的,源码如下:

    /****************************************************************************
    Accessors for block descriptor fields
    *****************************************************************************/
    #if 0
    static struct Block_descriptor_1 * _Block_descriptor_1(struct Block_layout *aBlock)
    {
        return aBlock->descriptor;
    }
    #endif
    
    // Block 的描述 : copy 和 dispose 函数
    static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
    {
        if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
        uint8_t *desc = (uint8_t *)aBlock->descriptor;
        desc += sizeof(struct Block_descriptor_1);
        return (struct Block_descriptor_2 *)desc;
    }
    
    // Block 的描述 : 签名相关
    static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock)
    {
        if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
        uint8_t *desc = (uint8_t *)aBlock->descriptor;
        desc += sizeof(struct Block_descriptor_1);
        if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
            desc += sizeof(struct Block_descriptor_2);
        }
        return (struct Block_descriptor_3 *)desc;
    }
    

    2.2.4 查看Block签名

    在面试中经常会提到Block签名的问题,那么我们的Block签名到底是什么呢?在上一节中我们可以看到我们的Block签名存储在Block_descriptor_3中,下面我们就定义一个block通过读取内存的方式看一看Block的签名长什么样。

    Block 代码:

    void (^block)(void) = ^{
        NSLog(@"test_block");
    };
    block();
    
    16061993231947.jpg

    以下这段话很重要!!!!!

    这里是紧密的根据上一节的内容来的,因为block在底层本质是Block_layout,其参数中isa占8字节,flags占4字节,reserved占4字节,invoke占8字节,descriptor占8字节,所以我们读取的第一个4段内存中的第4个就是descriptor的地址,根据上一节中我们的分析,知道了Block_descriptor_3的地址是由Block_descriptor_1偏移进行读取的,根据_Block_descriptor_3函数中的代码,我们的Block是否需要销毁通过BLOCK_HAS_COPY_DISPOSE进行判断,在读取的第一个四段内存中的第二段0x50000000与上flags中的BLOCK_HAS_COPY_DISPOSE也就是1 << 25结果为0,所以只需要偏移Block_descriptor_1这个结构体的内存大小,Block_descriptor_1有两个属性,分别都是uintptr_t类型,uintptr_t实际就是long占8字节,两个就是16字节,所以第二个四段内存中的第三个就是Block_descriptor_3的首地址,也就是signature签名信息的地址,是个char *类型,占8字节。打印结果为v8@?0,所以这就是我们当前Block的签名。

    签名信息分析:

    • v:返回值void
    • 8:占8位,也就是block本身占用的内存空间
    • @?:block签名
    • 0:起始位置为0

    我们再来看看有参数有返回值的Block的签名:

    代码:

    NSString* (^block1)(int a, int b) = ^(int a, int b){
        return [NSString stringWithFormat:@"%d---%d", a, b];
    };
        
    NSString * str = block1(1,2);
        
    NSLog(@"字符串的值是:%@", str);
    

    内存读取结果:

    16062009165973.jpg

    此时的签名变成了@"NSString"16@?0i8i12

    签名信息分析:

    • @"NSString":返回值为OCNSString
    • 16:占用16字节
    • @?:block的签名
    • 0i8i12:起始位置为0,block,i为分隔符,8是第一个参数的起始位置也就是int a,12 是第一个参数的起始位置也就是int b

    打印一下签名

    通过[NSMethodSignature signatureWithObjCTypes:"@?"]

    16062016572302.jpg

    通过打印我们可以看到isBlock

    PS:其实我们直接po 打印也可以看到block的签名:

    16062106117043.jpg 16062106674098.jpg

    结论:

    block的签名为@?

    2.3 Block 三层拷贝 捕获外界变量并修改的底层实现(__block)

    1.4.2中我们编译后的代码中多了两个函数__main_block_copy_0__main_block_dispose_0,在那一节我们并没有详细的分析,下面我们就通过这两个函数来详细的说说在底层__block是个啥。

    首先我们在__main_block_copy_0函数中可以看到其在内部调用了_Block_object_assign函数,那么我们就去libclosure中搜索一下这个函数:

    2.3.1 _Block_object_assign

    /*******************************************************
    
    Entry points used by the compiler - the real API!
    
    
    A Block can reference four different kinds of things that require help when the Block is copied to the heap.
    1) C++ stack based objects
    2) References to Objective-C objects
    3) Other Blocks
    4) __block variables
    
    In these cases helper functions are synthesized by the compiler for use in Block_copy and Block_release, called the copy and dispose helpers.  The copy helper emits a call to the C++ const copy constructor for C++ stack based objects and for the rest calls into the runtime support function _Block_object_assign.  The dispose helper has a call to the C++ destructor for case 1 and a call into _Block_object_dispose for the rest.
    
    The flags parameter of _Block_object_assign and _Block_object_dispose is set to
        * BLOCK_FIELD_IS_OBJECT (3), for the case of an Objective-C Object,
        * BLOCK_FIELD_IS_BLOCK (7), for the case of another Block, and
        * BLOCK_FIELD_IS_BYREF (8), for the case of a __block variable.
    If the __block variable is marked weak the compiler also or's in BLOCK_FIELD_IS_WEAK (16)
    
    So the Block copy/dispose helpers should only ever generate the four flag values of 3, 7, 8, and 24.
    
    When  a __block variable is either a C++ object, an Objective-C object, or another Block then the compiler also generates copy/dispose helper functions.  Similarly to the Block copy helper, the "__block" copy helper (formerly and still a.k.a. "byref" copy helper) will do a C++ copy constructor (not a const one though!) and the dispose helper will do the destructor.  And similarly the helpers will call into the same two support functions with the same values for objects and Blocks with the additional BLOCK_BYREF_CALLER (128) bit of information supplied.
    
    So the __block copy/dispose helpers will generate flag values of 3 or 7 for objects and Blocks respectively, with BLOCK_FIELD_IS_WEAK (16) or'ed as appropriate and always 128 or'd in, for the following set of possibilities:
        __block id                   128+3       (0x83)
        __block (^Block)             128+7       (0x87)
        __weak __block id            128+3+16    (0x93)
        __weak __block (^Block)      128+7+16    (0x97)
            
    
    ********************************************************/
    
    //
    // When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
    // to do the assignment.
    //
    void _Block_object_assign(void *destArg, const void *object, const int flags) {
        const void **dest = (const void **)destArg;
        switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
          case BLOCK_FIELD_IS_OBJECT:
            /*******
            id object = ...;
            [^{ object; } copy];
            ********/
    
            _Block_retain_object(object);
            *dest = object;
            break;
    
          case BLOCK_FIELD_IS_BLOCK:
            /*******
            void (^object)(void) = ...;
            [^{ object; } copy];
            ********/
    
            *dest = _Block_copy(object);
            break;
        
          case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
          case BLOCK_FIELD_IS_BYREF:
            /*******
             // copy the onstack __block container to the heap
             // Note this __weak is old GC-weak/MRC-unretained.
             // ARC-style __weak is handled by the copy helper directly.
             __block ... x;
             __weak __block ... x;
             [^{ x; } copy];
             ********/
    
            *dest = _Block_byref_copy(object);
            break;
            
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
            /*******
             // copy the actual field held in the __block container
             // Note this is MRC unretained __block only.
             // ARC retained __block is handled by the copy helper directly.
             __block id object;
             __block void (^object)(void);
             [^{ object; } copy];
             ********/
    
            *dest = object;
            break;
    
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
            /*******
             // copy the actual field held in the __block container
             // Note this __weak is old GC-weak/MRC-unretained.
             // ARC-style __weak is handled by the copy helper directly.
             __weak __block id object;
             __weak __block void (^object)(void);
             [^{ object; } copy];
             ********/
    
            *dest = object;
            break;
    
          default:
            break;
        }
    }
    

    根据注释我们总结如下:

    • 在将块复制到堆时,块可以引用四种不同类型的需要帮助的东西。
      • 1)基于c++栈的对象
      • 2)引用Objective-C对象
      • 3)其他模块
        1. __block变量
    • blockBlock_byrefs持有对象时,它们的复制例程助手就会使用这个入口点
    • 所以这个函数并不仅仅用于__block,对于很多从栈区拷贝到堆区的操作可能都会用到此函数

    这个函数有三个参数:

    • void *destArg :捕获对象的地址、
    • const void *object:捕获对象
    • flags: flag标志

    对于这三个参数从__block处分析,我们可以从1.4.2中的如下代码:

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    • destArg:&dst->a
    • object:src->a
    • flags:8

    __main_block_impl_0和源码:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_a_0 *a; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    struct __Block_byref_a_0 {
      void *__isa;
    __Block_byref_a_0 *__forwarding;
     int __flags;
     int __size;
     int a;
    };
    

    在上一遍源码,其他的就不多说了,已经很显而易见了,下面我们在看看_Block_object_assign函数,该函数的核心就是通过flags中的值去找出各种外界变量种类组合,种类代码如下:

    // Runtime support functions used by compiler when generating copy/dispose helpers
    
    // Values for _Block_object_assign() and _Block_object_dispose() parameters
    enum {
        // see function implementation for a more complete description of these fields and combinations
        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.
    };
    
    1. BLOCK_FIELD_IS_OBJECT:对象
    2. BLOCK_FIELD_IS_BLOCK:block变量
    3. BLOCK_FIELD_IS_BYREF__block 修饰的变量
    4. BLOCK_FIELD_IS_WEAK:__weak 修饰的变量
    5. BLOCK_BYREF_CALLER:处理Block_byref内部对象内存的时候会加的一个额外标记,配合上面的枚举一起使用

    此处我们看看BLOCK_FIELD_IS_BYREF也就是对应__block时在函数内部是怎么处理的:

    case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
    case BLOCK_FIELD_IS_BYREF:
    /*******
     // copy the onstack __block container to the heap
     // Note this __weak is old GC-weak/MRC-unretained.
     // ARC-style __weak is handled by the copy helper directly.
     __block ... x;
     __weak __block ... x;
     [^{ x; } copy];
     ********/
    
    *dest = _Block_byref_copy(object);
    break;
    

    我们可以看到,其内部调用的是_Block_byref_copy函数

    2.3.2 _Block_byref_copy

    _Block_byref_copy源码:

    // Runtime entry points for maintaining the sharing knowledge of byref data blocks.
    
    // A closure has been copied and its fixup routine is asking us to fix up the reference to the shared byref data
    // Closures that aren't copied must still work, so everyone always accesses variables after dereferencing the forwarding ptr.
    // We ask if the byref pointer that we know about has already been copied to the heap, and if so, increment and return it.
    // Otherwise we need to copy it and update the stack forwarding pointer
    static struct Block_byref *_Block_byref_copy(const void *arg) {
        struct Block_byref *src = (struct Block_byref *)arg;
    
        if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
            // src points to stack
            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
            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;
    
            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 {
                // Bitwise copy.
                // This copy includes Block_byref_3, if any.
                memmove(copy+1, src+1, src->size - sizeof(*src));
            }
        }
        // already copied to heap
        else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
            latching_incr_int(&src->forwarding->flags);
        }
        
        return src->forwarding;
    }
    

    这个函数是__block捕获外界变量的操作内存拷贝以及一些常规处理。

    • 这里首先出初始化一个局部变量src存储传入的外部变量
    • 然后判断block的引用计数是否为0
    • 如果不为0说明不是第一次拷贝,进入另一个分支判断是否需要释放(free),如果需要则调用latching_incr_int函数增加引用计数
    • 如果以上都不满足直接返回src->forwarding
    • 如果是0就说明是第一次拷贝
      • 首先创建一个一样大小的Block_byref变量copy
      • copy赋一些值,这里有一处重要的操作就是通过对copyforwardingsrcforwarding同时指向copy来达到变量的指针统一,以达到修改变量值时,达到同时修改的目的。
        • 下面判断该block是否需要销毁,如果需要就进行一些赋值操作
        • 还会判断block是否有扩展信息,如果有也会进行一些赋值操作
        • 最后调用src2->byref_keep,那么这个byref_keep是什么呢?我们进一步分析

    在分析byref_keep前我们先看看latching_incr_int函数,源码如下:

    latching_incr_int源码:

    static int32_t latching_incr_int(volatile int32_t *where) {
        while (1) {
            int32_t old_value = *where;
            if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
                return BLOCK_REFCOUNT_MASK;
            }
            if (OSAtomicCompareAndSwapInt(old_value, old_value+2, where)) {
                return old_value+2;
            }
        }
    }
    

    latching_incr_int函数中的判断就不多说了,这里说一下为啥要加2,因为记录引用计数是在flag的第二位中,第一位是记录block是否释放的,所以加2想当于第二位加1.

    byref_keep:

    由于byref_keep是一个BlockByrefKeepFunction函数指针类型的属性,所以byref_keep并不是函数名,byref_keep所在的结构体:

    //__Block 修饰的结构体
    struct Block_byref {
        void *isa;
        struct Block_byref *forwarding;
        volatile int32_t flags; // contains ref count
        uint32_t size;
    };
    
    //__Block 修饰的结构体 byref_keep 和 byref_destroy 函数 - 来处理里面持有对象的保持和销毁
    struct Block_byref_2 {
        // requires BLOCK_BYREF_HAS_COPY_DISPOSE
        BlockByrefKeepFunction byref_keep;
        BlockByrefDestroyFunction byref_destroy;
    };
    
    struct Block_byref_3 {
        // requires BLOCK_BYREF_LAYOUT_EXTENDED
        const char *layout;
    };
    

    那么到这里就算断了吗?那肯定不是的,我们去1.4.2Clang编译后的代码中去寻找__Block_byref_a_0这个结构体中看看,这里的第五个参数为123,因为这个是int类型的外部变量,我们在外部赋值的时候为123。下面我们换个字符串试试。

    OC代码:

    __block NSString *block_test = [NSString stringWithFormat:@"block_test"];
    void (^block)(void) = ^{
        block_test = @"block_test_block";
        NSLog(@"LG_Block - %@",block_test);
    };
    block();
    

    编译后__Block_byref_block_test_0部分

    __attribute__((__blocks__(byref))) __Block_byref_block_test_0 block_test = {
    (void*)0,
    (__Block_byref_block_test_0 *)&block_test,
    33554432, 
    sizeof(__Block_byref_block_test_0),
    __Block_byref_id_object_copy_131,
    __Block_byref_id_object_dispose_131,
    ((NSString * _Nonnull (*)(id, SEL, NSString * _Nonnull, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"),
    sel_registerName("stringWithFormat:"),
    (NSString *)&__NSConstantStringImpl__var_folders_0r_7cq1c39116927bt9x0bjsbtm0000gn_T_main_0ca87c_mi_0)};
    

    这时我们看到__Block_byref_block_test_0第五个参数为__Block_byref_id_object_copy_131,也就是对应byref_keep的位置因为Block_byref有四个属性,所以Block_byref_2的第一属性就对应着这里面的第五个参数。

    我们在Clang编译后的代码中搜索__Block_byref_id_object_copy_131,其实现如下:

    static void __Block_byref_id_object_copy_131(void *dst, void *src) {
     _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
    }
    

    我们发现__Block_byref_id_object_copy_131内部也是调用的_Block_object_assign函数,但是参数确是偏移了40位的,我们知道这里是传入的参数是捕获的外界变量生成的结构体,对于这次编译生成的结构体源码如下:

    struct __Block_byref_block_test_0 {
      void *__isa;
    __Block_byref_block_test_0 *__forwarding;
     int __flags;
     int __size;
     void (*__Block_byref_id_object_copy)(void*, void*);
     void (*__Block_byref_id_object_dispose)(void*);
     NSString *block_test;
    };
    

    由以上代码我们可以看出前面的地址和是8+8+4+4+8+8 = 40,所以偏移40位后就是block_test的地址,也就是取的外界变量的值。所以这就是将外界捕获的变量在通过_Block_object_assign进行拷贝处理一次。也验证了我们一开始时说_Block_object_assign并不仅仅是处理__block的。

    2.3.3 _Block_copy

    对于block类型的变量会调用_Block_copy函数进行处理,下面我们就看看_Block_copy函数,源码如下:

    // Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
    void *_Block_copy(const void *arg) {
        struct Block_layout *aBlock;
    
        if (!arg) return NULL;
        
        // The following would be better done as a switch statement
        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 =
                (struct Block_layout *)malloc(aBlock->descriptor->size);
            if (!result) return NULL;
            memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
    #if __has_feature(ptrauth_calls)
            // Resign the invoke pointer as it uses address authentication.
            result->invoke = aBlock->invoke;
    #endif
            // 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;
        }
    }
    

    此处源码我们在一开始就上过,那时只是由该函数引入了对Block底层结构的分析,现在我们分析一下该方法:

    • 首先判断是需要释放的,也就是堆区block则调用latching_incr_int函数直接进行引用计数的增加,该函数在上面分析过,这里就不多说了
    • 然后判断是不是全局block,如果是就直接返回
    • 最后也就是栈区block
      • 根据block的大小申请一块堆区空间
      • 将栈区block移动到堆区申请的空间
      • invoke进行赋值
      • flags进行赋值,是否需要释放,引用计数等
      • 调用_Block_call_copy_helper函数处理Block_descriptor_2copy动作
      • isa设置为_NSConcreteMallocBlock也就是堆block

    _Block_call_copy_helper源码:

    static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
    {
        struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
        if (!desc) return;
    
        (*desc->copy)(result, aBlock); // do fixup
    }
    

    2.3.4 小结

    至此我们对Block的拷贝就分析完了,总结如下:

    • 首先会调用_Block_copy函数将栈区block拷贝到堆区(一层)
    • 对于使用__block修饰的外界变量底层会生成一个__Block_byref_xxx_0的结构体
    • 对于该结构体首先会调用_Block_object_assign函数对齐flags判断进入不同分支处理,这里就是BLOCK_FIELD_IS_BYREF对应__block
    • 在分支中会调用_Block_byref_copy函数,函数内部会拷贝一个一样大小的结构体,并且将变量指针指向同一区域,已达到修改值时相同的目的(二层)
    • 最后会通过Block_byref_2中的byref_keep属性记录的函数指针内调用_Block_object_assign函数,传入__Block_byref_xxx_0的结构体的外界变量的值进行又一次拷贝,这个值是通过指针偏移找到的(三层)
    • 如果外界变量不是对象时,例如int则直接记录其值,不会进行最后一次拷贝操作
    • 对于_Block_copy函数内对block类型的拷贝:
      • 全局block不要拷贝
      • 栈区block需要拷贝到堆区
      • 堆区block增加引用计数即可

    2.4 Block的释放

    在上一节中我们提到,编译后的代码中会多出两个函数,其中我们分析了__main_block_copy_0,还剩下一个__main_block_dispose_0,下面我们就来看看__main_block_dispose_0都做了什么?

    __main_block_dispose_0源码:

    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->block_test, 8/*BLOCK_FIELD_IS_BYREF*/);}
    

    __main_block_dispose_0函数我们可以看出其内部调用了_Block_object_dispose函数,所以我们就来到libclosure源码中搜索一下这个函数,源码如下:

    // When Blocks or Block_byrefs hold objects their destroy helper routines call this entry point
    // to help dispose of the contents
    void _Block_object_dispose(const void *object, const int flags) {
        switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
          case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
          case BLOCK_FIELD_IS_BYREF:
            // get rid of the __block data structure held in a Block
            _Block_byref_release(object);
            break;
          case BLOCK_FIELD_IS_BLOCK:
            _Block_release(object);
            break;
          case BLOCK_FIELD_IS_OBJECT:
            _Block_release_object(object);
            break;
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
            break;
          default:
            break;
        }
    }
    

    通过源码我们可以看到它与_Block_object_assign的实现方式是一致的,都是通过一个switch函数来进行匹配不同的情况:

    2.4.1 释放__block 修饰的变量(BLOCK_FIELD_IS_BYREF)

    当需要释放__block修饰的变量时会调用_Block_byref_release函数,源码实现如下:

    static void _Block_byref_release(const void *arg) {
        struct Block_byref *byref = (struct Block_byref *)arg;
    
        // dereference the forwarding pointer since the compiler isn't doing this anymore (ever?)
        byref = byref->forwarding;
        
        if (byref->flags & BLOCK_BYREF_NEEDS_FREE) {
            int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK;
            os_assert(refcount);
            if (latching_decr_int_should_deallocate(&byref->flags)) {
                if (byref->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
                    struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
                    (*byref2->byref_destroy)(byref);
                }
                free(byref);
            }
        }
    }
    

    源码分析:

    • 首先获取到要释放的变量,并取消引用转发的指针,因为在赋值的时候是与外界变量指向同一空间的
    • 判断是否需要释放,如果不需要就执行完毕了
    • 如果需要释放
      • 获取引用计数
      • 调用latching_decr_int_should_deallocate函数判断是否应该释放
        • 应该释放的话就判断flags中是否有拷贝/释放辅助函数
          • 如果有的话就获取一个临时变量byref2调用byref_destroy属性保存的函数
        • 最后释放byref

    关于byref_destroy保存的函数,实现原理与byref_keep是一致的,我们来到编译后的eC++文件中查看,byref_destroy属性保存的函数是__Block_byref_id_object_dispose_131,其代码如下:

    static void __Block_byref_id_object_dispose_131(void *src) {
     _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
    }
    

    通过__Block_byref_id_object_dispose_131的代码我们可以看出,再其内部调用的是_Block_object_dispose函数,同样也是偏移了40,如果不是对象类型,比如int是没有保存这个函数的,原理同拷贝原理,拷贝的时候分三层拷贝,释放的时候也就要三层释放。

    上面提到的latching_decr_int_should_deallocate函数返回当前block是否应该释放,该函数的源码如下:

    latching_decr_int_should_deallocate源码:

    static bool latching_decr_int_should_deallocate(volatile int32_t *where) {
        while (1) {
            int32_t old_value = *where;
            if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
                return false; // latched high
            }
            if ((old_value & BLOCK_REFCOUNT_MASK) == 0) {
                return false;   // underflow, latch low
            }
            int32_t new_value = old_value - 2;
            bool result = false;
            if ((old_value & (BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING)) == 2) {
                new_value = old_value - 1;
                result = true;
            }
            if (OSAtomicCompareAndSwapInt(old_value, new_value, where)) {
                return result;
            }
        }
    }
    
    • 这里通过一个while循环,不断的判断block是否应该释放
    • 首先判断flags与上BLOCK_REFCOUNT_MASK等于BLOCK_REFCOUNT_MASK,则返回false
    • 然后判断两个相与是否等于0,等于的话也会返回false
    • 创建一个新new_value = old_value - 2,并定义一个boolresult等于false
    • 判断旧flags与上(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING)等于2,则记录result等于true
    • 最后调用OSAtomicCompareAndSwapInt函数将新旧值比对交换,如果交换成则返回result,否则进入新的循环。

    2.4.2 释放Block变量(BLOCK_FIELD_IS_BLOCK)

    当需要释放block变量时,需要调用_Block_release函数,其源码实现如下:

    // API entry point to release a copied Block
    void _Block_release(const void *arg) {
        struct Block_layout *aBlock = (struct Block_layout *)arg;
        if (!aBlock) return;
        if (aBlock->flags & BLOCK_IS_GLOBAL) return;
        if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;
    
        if (latching_decr_int_should_deallocate(&aBlock->flags)) {
            _Block_call_dispose_helper(aBlock);
            _Block_destructInstance(aBlock);
            free(aBlock);
        }
    }
    

    源码分析:

    • 首先创建一个局部的的block变量,如果为空直接return
    • 如果block是全局的,则直接return
    • 如果block不需要释放也直接return
    • 如果都不是则调用latching_decr_int_should_deallocate判断是否能够释放,函数分析在上一节已经分析过了
      • 如果可以释放就调用_Block_call_dispose_helper函数,获取descriptor_2dispose存储的是否函数进行调用
      • 然后还会调用_Block_destructInstance,这里没有相关实现,应该是没开源吧
      • 最后free局部变量

    3. 总结

    1. Block是一个匿名函数,也是一个对象,在底层是一个Block_layout
    2. Block需要调用是因为Block代码块在底层是一个函数,要想让其执行,所以需要调用
    3. Block捕获外界变量的时候会生成一个同名的中间变量,取获取到的时候的值
    4. Block使用外界变量的时候会生成一个__Block_byref_xxx_0的结构体
    5. Block的签名是@?
    6. Block通过__block访问外界变量的时候会有三层拷贝
      1. 首先是block从栈拷贝到堆
      2. 将修饰的对象转话为一个结构体,将其拷贝到堆内存
      3. 将修饰的对象的内存地址也进行拷贝
    7. Block的释放相当于拷贝的反向,拷贝的东西都需要释放的

    相关文章

      网友评论

          本文标题:iOS Objective-C Block底层原理

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