iOS Objective-C Block底层原理

2020-11-25  本文已影响0人  just东东

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内有四个变量:

1.3 Block的调用

在编译后的代码中我们可以看出Block的调用是通过block->FuncPtr(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
};

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的不同信息:

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的签名。

签名信息分析:

我们再来看看有参数有返回值的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

签名信息分析:

打印一下签名

通过[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;
    }
}

根据注释我们总结如下:

这个函数有三个参数:

对于这三个参数从__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*/);}

__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捕获外界变量的操作内存拷贝以及一些常规处理。

在分析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_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的拷贝就分析完了,总结如下:

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

源码分析:

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

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

源码分析:

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的释放相当于拷贝的反向,拷贝的东西都需要释放的
上一篇下一篇

猜你喜欢

热点阅读