iOS-block(二)-底层分析

前言 上一篇我们讲完了block的基础知识,这一篇我们就来看看block的底层原理。话不多说,我们创建一个testBlock.c的文件,输入以下代码:

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

然后对代码编译成.cpp文件,此时main函数就变成下面的样子:
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; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int a = __cself->a; // bound by copy printf("BLOCK_TEST==%d", a); }static struct __main_block_desc_0 { size_t reserved; size_t Block_size; } __main_block_desc_0_DATA = https://www.it610.com/article/{ 0, sizeof(struct __main_block_impl_0)}; int main(){ int a = 5; 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; }

使用clang指令的时候,需要注意不要使用字面量。
block分析 在对代码的分析之前我们先抛出几个问题:
  1. block的本质到底是什么
  2. block为什么需要调用block()
  3. block是如何截获外界变量的
  4. __block的实现
block本质
【iOS-block(二)-底层分析】Objective-C中,block是一个对象,从编译的结果来看,block在本质上还是一个结构体struct。而我们通常所说的block是一个匿名函数也能提现出来,比如例子中,系统默认给block分配了一个函数名称__main_block_impl_0,其参数分别是(void *)__main_block_func_0__main_block_desc_0_DATAa
调用block()
block的声明是:
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a))

实现则是:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int a = __cself->a; // bound by copy 值拷贝 printf("BLOCK_TEST==%d", a); }

使用函数就需要调用,block的最终执行调用就是block->FuncPtr(block)
捕获外界变量
通过编译结果,可以看到__main_block_impl_0这个结构体有一个int a的元素,而结构体内部的__main_block_impl_0函数有: a(_a)这么一个赋值过程。我们可以把源代码中int a相关的代码删除,重新编译,会发现得到的结果中并没有相关的参数和赋值过程。这说明block捕获外界变量的时候是自己创建一个同名变量将其进行复制操作,赋值之后,block内部的变量已经和外部变量没有了关系。所以我们在《block(一)-初探》中有一个例子:
typedef int(^myBlock)(int a, int b); int d = 10; myBlock mb = ^int(int a, int b) { return a + b + d; }; d = 5; NSLog(@"==myBlock==%d==", mb(1, 2)); // 13

而且我们可以看到,在block调用函数的时候,还会执行一次创建临时变量赋值的操作。int a = __cself->a,这又不是同一个变量。所以当我们在block内部直接对外部的变量进行操作(赋值)的时候,其实操作的内部的同名临时变量,而不是外部的变量。所以block无法直接给外部截获的变量赋值,因为它在自己内部生成了一个同名临时变量,所有的操作都是内部的临时变量。那么我们需要在block内部处理截获的外部变量该怎么办呢?答案是使用__block
block源码
通过源码我们可以看出block在底层的结构如下:
struct Block_layout { void *isa; // isa指向 volatile int32_t flags; // contains ref count 标志状态 int32_t reserved; BlockInvokeFunction invoke; // 函数执行 struct Block_descriptor_1 *descriptor; // block的附加描述信息 如size等 // imported variables }; #define BLOCK_DESCRIPTOR_1 1 struct Block_descriptor_1 { uintptr_t reserved; uintptr_t size; };

flagsblock的状态标志位。含义如下:
enum { BLOCK_DEALLOCATING =(0x0001),// runtime 标记正在释放 BLOCK_REFCOUNT_MASK =(0xfffe),// runtime 存储引用计数的值 BLOCK_NEEDS_FREE =(1 << 24), // runtime 是否增加或减少引用计数的值 BLOCK_HAS_COPY_DISPOSE =(1 << 25), // compiler 是否拥有拷贝辅助函数 确定block是否存在Block_descriptor_2这个参数 BLOCK_HAS_CTOR =(1 << 26), // compiler: helpers have C++ code 是否有C++析构函数 BLOCK_IS_GC =(1 << 27), // runtime 是否有垃圾回收 BLOCK_IS_GLOBAL =(1 << 28), // compiler 是否是全局block 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_descriptor_3中的layout参数 };

另外还有两个可选的参数:
#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_descriptor_2需要flagsBLOCK_HAS_COPY_DISPOSE才可以,而Block_descriptor_3需要flagsBLOCK_HAS_SIGNATURE
了解了block的内部结构,我们再来看看block从全局block、栈block到堆block是怎么变化的。
首先,我们实现一个全局block:
void(^myBlock)(void) = ^ { NSLog(@"==myBlock=="); };

block处设置一个断点,进入汇编,objc_retainBlock、我们在此时读一下寄存器,继续跳转会进入,_Block_copy,在_Block_copyreturn处读一下寄存器。结果如下图所示:
iOS-block(二)-底层分析
文章图片
image 这说明调用_Block_copy使得栈block变成了堆block。下面我们来看看其源码:
// 传入的对象 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) { // 如果需要对引用计数进行处理,那就直接处理,处理完就返回 // block的引用计数是不由runtime下层处理,需要自己处理 // 这个地方处理的是堆区block latching_incr_int(&aBlock->flags); return aBlock; } else if (aBlock->flags & BLOCK_IS_GLOBAL) { // 如果是全局block 直接返回 return aBlock; } else { // Its a stack block.Make a copy // 栈区block 使用copy // 先在堆区初始化一块内存空间 struct Block_layout *result = (struct Block_layout *)malloc(aBlock->descriptor->size); if (!result) return NULL; // 将栈区的数据copy到堆区的空间 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 // 设置标志位 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); // 设置为_NSConcreteMallocBlock result->isa = _NSConcreteMallocBlock; return result; } }// 处理引用计数 static int32_t latching_incr_int(volatile int32_t *where) { while (1) { int32_t old_value = https://www.it610.com/article/*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; } } }

从代码,我们可以看出blockcopy动作主要如下:
  • 全局block不做任何事情直接返回
  • 堆区block增加引用计数然后返回
  • 栈区block
    • 在堆区申请一块内存空间
    • 将栈区的数据拷贝到堆区申请的空间
    • 给相关标志位赋值,对Block_descriptor_2copy动作,将isa设置为_NSConcreteMallocBlock
      block的引用计数是不由runtime下层处理,需要自己处理。
__block
下面我们再根据编译的代码来看看__block到底做了什么。先把下面代码进行编译:
int main(int argc, char * argv[]) { @autoreleasepool {__block NSString *name = [NSString stringWithFormat:@"%@", @"AAA"]; void(^myBlock)(void) = ^{ name = @"BBB"; NSLog(@"==name==%@==", name); }; myBlock(); return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }

我们先来看看mian函数编译之后的代码:
int main(int argc, char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; __attribute__((__blocks__(byref))) __Block_byref_name_0 name = { (void*)0, (__Block_byref_name_0 *)&name, 33554432, sizeof(__Block_byref_name_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_nw_tqjtztpn1yq6w0_wmgdvn_vc0000gn_T_main_41740c_mi_0, (NSString *)&__NSConstantStringImpl__var_folders_nw_tqjtztpn1yq6w0_wmgdvn_vc0000gn_T_main_41740c_mi_1) }; void(*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_name_0 *)&name, 570425344)); ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock); return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")))); } }

__block修饰的name对象被转化成了一个__Block_byref_name_0的结构体,其源码如下:
struct Block_byref { void *isa; struct Block_byref *forwarding; volatile int32_t flags; // contains ref count uint32_t size; }; 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; };

可以看出,__Block_byref_name_0中的(void*)0就是Block_byrefvoid *isa(__Block_byref_name_0 *)&name即为struct Block_byref *forwarding__Block_byref_id_object_copy_131即为BlockByrefKeepFunction byref_keep函数。
上面我们讲述了block从栈到堆的拷贝过程。下面再来看看__Block_byref_name_0的拷贝动作。在block里我们传入了一个&__main_block_desc_0_DATA的结构体地址,该结构体在初始化的时候传入了__main_block_copy_0方法进行拷贝操作,实现如下:
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 = https://www.it610.com/article/{ 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0}; static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { _Block_object_assign((void*)&dst->name, (void*)src->name, 8/*BLOCK_FIELD_IS_BYREF*/); }enum { BLOCK_FIELD_IS_OBJECT=3,// 截获的是对象 __attribute__((NSObject)), block, ... BLOCK_FIELD_IS_BLOCK=7,// 截获的是block变量 BLOCK_FIELD_IS_BYREF=8,// 截获的是__block修饰的对象 BLOCK_FIELD_IS_WEAK= 16,// 截获的是__weak修饰的对象 BLOCK_BYREF_CALLER= 128, // called from __block (byref) copy/dispose support routines. }; // 根据传入的对象的类型 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; } }

分析上述源码,可知,针对blockblock_byrefs截获的对象的类型进行不同的内存管理处理:
    1. 截获的变量是对象,只需要赋值,引用计数不做任何处理,因为对象的引用计数是runtime底层自己处理的。
static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default; static void _Block_retain_object_default(const void *ptr __unused) { }

    1. 如果截获的变量是block对象,调用_Block_copy方法。
void *_Block_copy(const void *arg) { // 详见上面的分析 }

  1. 如果截获的变量是__block对象,需要重新申请一块堆内存,然后将截获的对象也就是上述例子中的__Block_byref_name_0结构体赋值给新的结构体,并将它们的forwarding指针都指向新生成的结构体。其实也就是对__block修饰的对象做了一次拷贝动作,然后让他们都指向同一块内存区域达到修改其中一个两个都改变的目的。
static struct Block_byref *_Block_byref_copy(const void *arg) { // 创建一个临时变量 struct Block_byref *src = https://www.it610.com/article/(struct Block_byref *)arg; if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) { // src points to stack // 1.申请堆内存空间 struct Block_byref *copy = (struct Block_byref *)malloc(src->size); // 2. 给新申请的空间赋值 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的对象和源对象都指向堆内存的拷贝地址 copy->forwarding = copy; // 堆拷贝指向自己 src->forwarding = copy; // 栈拷贝指向堆内存 copy->size = src->size; if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) { // 处理desc2 内存偏移取值 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) { // 处理desc3 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); }return src->forwarding; }

在新生成的结构体__Block_byref_name_0中,还有一个名为__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_object_assign方法,不过是传递的参数不同,是目标对象偏移了40字节,由于此时目标对象就是__Block_byref_name_0,偏移40字节正好是NSString *name。也就是说此时又是对name这个对象的内存地址做了一次拷贝。
struct __Block_byref_name_0 { void *__isa; // 8 __Block_byref_name_0 *__forwarding; // 8 int __flags; // 4 int __size; // 4 void (*__Block_byref_id_object_copy)(void*, void*); // 8 void (*__Block_byref_id_object_dispose)(void*); // 8 NSString *name; };

所以,我们可以得出结论:__block修饰的外部变量,在block内部能够修改的主要原因在于3次拷贝。
    1. block的拷贝,从栈内存到堆内存。
    1. 对新生成的结构体的拷贝。__block修饰的变量会生成一个名为__Block_byref_XXX_0结构体,将原来的进行了封装,然后把整个结构体地址指针传入block内部。
    1. 对原来的对象的内存的拷贝。
block的释放
上面我们讲过了block的持有过程,既然有持有,那就肯定有释放。下面我们来看看block的释放过程。
&__main_block_desc_0_DATA的定义中,会传入__main_block_dispose_0这样一个函数与void (*dispose)(struct __main_block_impl_0*)方法相对应。其函数实现如下:
static void __main_block_dispose_0(struct __main_block_impl_0*src) { _Block_object_dispose((void*)src->name, 8/*BLOCK_FIELD_IS_BYREF*/); }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: _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; } }

    1. 需要释放的如果是__block修饰的对象,判断如果应该释放,则调用方法销毁创建的__block结构。
static void _Block_byref_release(const void *arg) { struct Block_byref *byref = (struct Block_byref *)arg; 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); } } }

    1. 需要释放的如果是block,判断是否该释放,如果应该释放则调用释放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); } }

    1. 需要释放的如果是对象,则什么都不用做,ARCruntime底层自己处理。
static void (*_Block_release_object)(const void *ptr) = _Block_release_object_default; static void _Block_release_object_default(const void *ptr __unused) { }

block的签名
我们在讲述block的本质的时候说了,block是匿名函数,那么作为一个函数,block是否也有自己的签名。答案是肯定,上面我们的打印结果也体现出来了。
我们在讲述block的源码中提到block有两个可选的参数Block_descriptor_2Block_descriptor_3。而block的签名信息就放在Block_descriptor_3中,一个名为signature的元素。
struct Block_descriptor_3 { // requires BLOCK_HAS_SIGNATURE const char *signature; const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT };

从上面的打印结果可以看出block的签名样子如下:
signature: "v8@?0"

其中v表示返回值是void@?表示未知的对象,即为block。这和方法签名是有所不同的,方法签名一般是v@:这样的形式(此处只说返回值为void的场景),:表示SEL
总结
  1. block的本质是一个匿名函数,也是一个对象,其底层实现是结构体。
  2. block既有声明,也有调用。
  3. block也有签名,和方法签名略有不同。
  4. block截获外部变量是在定义的时候生成一个同名的中间变量,该变量的初值就是外部变量在被截获的时候的值,之后就与外部变量没有关系。
  5. __block修饰的外部变量在block内部能够修改的原因在于3次拷贝:
    • block的拷贝,从栈内存到堆内存
    • 将修饰的对象转化为一个结构体,将其拷贝到堆内存。
    • 将修饰的对象的内存地址也进行了拷贝用以修改。
参考文献:
苹果官方文档
Objective-C高级编程 iOSOS X多线程和内存管理》

    推荐阅读