iOS底层学习——block底层原理

882 阅读11分钟

上篇文章我们学习了block的分类,三种block分别为:__NSGlobalBlock____NSStackBlock____NSMallocBlock__;分析了block捕获外界变量以及block可能会引起的循环引用问题。

本篇将继续分析block的底层实现原理,栈区block是如何拷贝的堆区的,block捕获外部变量的本质,block的数据结构等内容。

1.block原理

老方法,通过clang.m文件编译成.cpp文件,查看block的本质。`

1.未捕获外部变量

案例代码:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        void (^block)(void) = ^{
            NSLog(@"hello block");
        };
        block();

        NSLog(@"%@", block);

        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

clang之后生成.cpp文件。查看编译之后的main函数:

image.png

  • block是一个结构体,该结构体定义为:__main_block_impl_0,该结构体继承自__block_impl

        struct __block_impl {
          void *isa; // isa指针
          int Flags;
          int Reserved;
          void *FuncPtr; // func
        };
    
  • 结构体中提供了一个构造函数__main_block_impl_0,该构造函数对block结构体中相关属性进行设置

    __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;
      }
    
  • 构造函数__main_block_impl_0的第一个参数为__main_block_func_0方法实现地址,在声明定义block时,将block的任务函数封装到FuncPtr属性中

    image.png

  • 调用block执行时,实际调用的是block->FuncPtr,并将block结构体作为参数传入到方法实现中

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    
  • 总结

    通过上面的分析,我们可以了解到,block是一个结构体,也可以认为是一个函数或者参数;通过函数式编程,将block任务,保存到结构体的FuncPtr属性中;函数调用block->FuncPtr()block作为隐藏参数,函数执行过程中,会持有block中的全部数据。

2.捕获未使用__block修饰的外部变量

修改一下上面的案例,让其捕获外部变量。见下面代码:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        int a = 10;
        void (^block)(void) = ^{
            NSLog(@"hello block %d", a);
        };

        block();
        NSLog(@"%@", block);  

        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

我们都知道block具有外部变量捕获功能,那么这种捕获是怎么实现的呢?查看编译后的实现:

image.png

通过上面的代码发现:

  • 当捕获外部变量时,block结构体中会多一个成员变量a,并且构造函数也会多一个参数a
  • 如果没有__block修饰,则通过值拷贝的方式,对其成员变量a进行赋值
  • 在执行block任务时,从结构体中获取对应的成员变量__cself->a,进行处理

捕获变量,在编译阶段就自动生成了相应的属性变量,来存储外界捕获的值,变量值拷贝。不能对常量进行变更,因为是值拷贝,在内部和外部会有相同的变量值,所以会导致代码歧义!编译不过!

image.png

3.捕获使用__block修饰的外部变量

再对上面的案例进行修改,外部变量添加__block。见下面代码:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        __block int a = 10;
        void (^block)(void) = ^{
            NSLog(@"hello block %d", a);
        };

        block();
        NSLog(@"%@", block);  

        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

__block修饰的外部变量又有什么区别呢,见下面编译结果:

image.png

  • 当外部变量使用__block修饰时,会封装成一个结构体__Block_byref_a_0

  • block结构体中,多出一个属性a,属性a的类型为__Block_byref_a_0

  • a的地址会赋值到__Block_byref_a_0结构体的__forwarding属性中

  • 总结:

    使用__block修饰符修饰的变量在编译时,block会为其创建一个结构体,结构体中保留了该变量的地址,地址拷贝。在使用该变量时,实际使用的是指针的方式访问,因此不管是在函数中或者是在block中改变该变量都不会互相影响。通过下图可以验证:

    image.png

4.捕获变量总结

上面分析了block捕获外部变量的逻辑,下面我们在通过两个案例来验证一下。

  • 未使用__block修饰的外部变量

    image.png

    在案例中,跟踪objc的引用计数,会发现其在捕获到block内部后由1变成了3。其实在上一篇文章# block以及循环引用问题中也有类似的案例,并且对引用计数的变化进行了分析。

    在本例中,在外层时objc被创建好以后,引用计数为1;因为objc没有使用__block进行修饰,所以是通过值拷贝的方式进行处理;于此同时,因为block捕获了外部变量,所以在运行时会从栈区拷贝到堆区,这样objc的引用计数会再次加1。最终objc的引用计数为3

  • 使用__block修饰的外部变量

    image.png

    此时的objc使用__block就行修饰,运行结果发现objc对象的引用计数一直是1。因为__block修饰的外部变量是通过指针拷贝的方式捕获到block结构体中的,所以内部和外部操作的是同一个对象。所以引用计数一直是1

2.block出处探索

block定义处设置断点:

image.png

运行程序,查看汇编,见下图:

image.png

通过汇编代码,可以发现,底层调用了objc_retainBlock方法。下面设置objc_retainBlock的符号断点,继续运行程序:

image.png

发现objc_retainBlock方法来自libobjc.A.dylib,也就是我们最熟悉的objc库。在libobjc.A.dylib库中也找到了对应的方法实现,该方法会调用_Block_copy,但是_Block_copy的实现并不在libobjc.A.dylib库中。见下图:

image.png

继续跟踪汇编,设置_Block_copy符合断点,运行程序,见下图:

image.png

可以发现_Block_copylibsystem_blocks.dylib库中。

同时,我们在clang获取的cpp文件中也可以看到block源码的出处,来自Block_private.h。见下图:

image.png

并且通过在cpp文件中block定义的结构体__block_impl和源码中Block_layout的结构体是一致的,见下图:

image.png

3.block内存变化

在上一篇文章# block以及循环引用问题中已经了解了block的三种类型。特别是捕获了外部变量的block,编译时是栈block,在运行时会copy到堆区,变成堆block。我们这里就来分析,block的这种内存变化是何时发生的,如何发生的。

引入下面的案例,该block捕获了外部变量,并设置断点进行跟踪,见下图:

image.png

通过跟踪汇编的方式,跟踪其内存变化过程。当程序运行到objc_retainBlock时,通过读取寄存器,分析block的数据状态变化。见下图:

image.png

调用objc_retainBlock方法时,此时依然是一个栈block。继续跟踪汇编,运行至_Block_copy,很显然block通过该方法完成了内存的变化,如何验证呢?汇编流程很长,在retq的地方设置断点,也就是在方法return的地方设置断点,查看其最终的处理结果,见下图:

image.png

同样通过读取寄存器x0,获取block此时为__NSMallocBlock__,并且地址发生了改变,从栈区拷贝到了堆区。

至此,可以得出结论,捕获了外部变量的block,编译时是栈block,在运行时通过_Block_copy方法会copy到堆区,变成堆block

4.Block_layout结构分析

Block_layout的源码定义如下:

    struct Block_layout {
        void * __ptrauth_objc_isa_pointer isa;
        volatile int32_t flags; // contains ref count
        int32_t reserved;
        BlockInvokeFunction invoke;
        struct Block_descriptor_1 *descriptor;
        // imported variables
    };
    
    #define BLOCK_DESCRIPTOR_1 1
    struct Block_descriptor_1 {
        uintptr_t reserved;
        uintptr_t size;
    };

    #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_layout中包括一些属性:

  • isa isa指向确定block类型
  • flags 标识码
  • reserved 保留字段
  • invoke 函数,也就是FuncPtr
  • descriptor 相关附加信息

其中flag的值来描述块对象。先看一下flag的定义:

image.png

blockBlock_descriptor_1相关属性是必然存在,其中reserved为保留字段,sizeblock的大小;但是Block_descriptor_3是可选的参数。而这里就通过flag字段来判断block是否存在Block_descriptor_3的相关属性。Block_descriptorget方法可以发现,通过地址平移的方式获取对应的值,并且在获取Block_descriptor_3时会判断Block_descriptor_2是否存在,如果不存在,就不需要添加Block_descriptor_2的地址空间。见下图:

image.png

我们可以通过lldb进行相关的验证:

image.png

获取block的内存空间,平移3*8个字节后就是Block_descriptor_1的地址。我们可以查看Block_descriptor_1之后的内存空间,进而分析Block_descriptor_2Block_descriptor_3的相关信息。

比如我们可以打印block的签名信息,见下图:

image.png

签名在Block_descriptor_3signature属性中。

5._Block_copy流程分析

_Block_copy实现源码:

// 栈 -> 堆 研究拷贝
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;
    if (!arg) return NULL;

    // Block_layout - block结构体类型
    // 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;
    }
    // 判断是是否为全局block
    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;
    // 进行block拷贝
        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; // 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; // isa修改
        return result;
    }
}
  • 首先进行block类型强转,转成Block_layout类型
  • 判断是否需要释放,如果需要则不处理
  • 判断是否为GLOBAL,如果是,也不需要进行copy操作
  • 如果不是GLOBAL,那要么是栈、要么是堆,而编译器不可能编译出一个堆区的block,所以源码中else的分支是对栈block的处理
  • 调用malloc,初始化一个内存空间,通过memmove进行相关数据的拷贝,并进行invokeflag的设置
  • 最终将blockisa设置为_NSConcreteMallocBlock

6.block捕获外部变量

block是如何捕获外部变量的呢,block三重拷贝过程是怎样的?回到cpp文件!查看__main_block_desc_0结构体的定义。见下图:

image.png

该结构体即对应源码中的Block_descriptor信息。其中reservedsize对应Block_descriptor_1的两个属性;另外,void (*copy)void (*dispose)对应Block_descriptor_2的两个方法;在copy方法的实现中,会调用_Block_object_assign,此过程即为外部变量的捕获和释放过程。

在源码中全局搜索_Block_object_assign,得到以下注释信息:

image.png

由编译器提供了辅助函数,用于Block_copyBlock_release,称为复制和处置辅助函数复制助手为基于C++堆栈的对象发出对C++ const的构造函数的调用,并为其调用运行时支持函数_Block_object_assigndispose helperC++析构函数,调用_Block_object_dispose

_Block_object_assign_Block_object_disposeflags参数设置为:

  • BLOCK_FIELD_IS_OBJECT (3),捕获Objective-C Object的情况
  • BLOCK_FIELD_IS_BLOCK (7),捕获另一个block的情况
  • BLOCK_FIELD_IS_BYREF (8),捕获__block变量的情况

其枚举定义,见下图:

image.png

我们用的最多的就是BLOCK_FIELD_IS_OBJECTBLOCK_FIELD_IS_BYREF

  • _Block_object_assign源码分析

    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:
            _Block_retain_object(object);
            *dest = object;
            break;
    
          case BLOCK_FIELD_IS_BLOCK:
            *dest = _Block_copy(object);
            break;
    
          case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
          case BLOCK_FIELD_IS_BYREF:
            *dest = _Block_byref_copy(object);
            break;
    
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
            *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:
            *dest = object;
            break;
    
          default:
            break;
        }
    }
    
    • 如果持有变量是BLOCK_FIELD_IS_OBJECT类型,即没有__block修饰,指针指向该对象,将对该对象进行持有,引用计数加1
      *dest = object;
      
    • 如果是BLOCK_FIELD_IS_BLOCK类型,捕获一个block,则进行_Block_copy操作
      *dest = _Block_copy(object);
      
    • 如果是BLOCK_FIELD_IS_BYREF,即有__block修饰,则会调用_Block_byref_copy
      *dest = _Block_byref_copy(object);
      
  • _Block_byref_copy实现源码

    static struct Block_byref *_Block_byref_copy(const void *arg) {
        struct Block_byref *src = (struct Block_byref *)arg;
    
        // __block 内存是一样 同一个家伙
        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 *src

    • 如果是BLOCK_FIELD_IS_BYREF,则会调用malloc,生成一个Block_byref *copy

    • 设置forwarding,保证block内部和外部都指向同一个对象

          copy->forwarding = copy;
          src->forwarding = copy;
      
    • Block_byrefkeep函数和destroy处理,并进行byref_keep函数的调用

      image.png

      Block_byref的设计思路和Block_layoutdescriptor流程类似,通过byref->flag标识码判断对应的属性,以此来判断Block_byref_2是否存在,Block_byref定义见下图:

      image.png

      如果用__block修饰了外部变量,编译生成的cpp文件中,Block_byref结构体中就会默认生成两个方法,即对应Block_byref_2keep方法和destory方法,见下图:

      image.png

      cpp文件中搜索这两个函数的实现,见下图:

      image.png

      此过程会再次调用_Block_object_assign函数,对Block_byref结构体中的对象进行BLOCK_FIELD_IS_OBJECT流程处理。

至此block的三重拷贝已经摸清:

  1. block的拷贝,即将栈区block,拷贝至堆区
  2. __block修饰的对象,对应的Block_byref结构体的拷贝
  3. Block_byref修饰的对象,调用_Block_object_assign函数进行修饰处理

至此,完成block捕获外部变量的本质分析,block的底层实现原理分析,block的数据结构分析等内容。