iOS-底层原理分析之Block本质

语言: CN / TW / HK

highlight: a11y-dark

前言

写本篇文章的目的就是要搞明白以下几个问题?

  • block的底层实现原理
  • block的几种类型?
  • __block的作用是什么?有什么使用注意点?
  • block的循环引用问题解决?

什么是Block

Block是将函数及其上下文封装起来的对象。

Block的底层实现原理

Block的底层实现是一个结构体

```oc void blockTest() {     void (^block)(void) = ^{         NSLog(@"Hello");     };

block(); }

int main(int argc, char * argv[]) {     @autoreleasepool {         blockTest();     } } ```

可以通过clang命令查看编译器是如何实现Block的,进入main.m的目录,在终端输入xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m,然后会在当前目录生成main.cpp的C++文件,代码如下:

```oc ///Block的C++实现 struct __blockTest_block_impl_0 {   struct __block_impl impl;   struct __blockTest_block_desc_0 Desc;     ///构造函数   __blockTest_block_impl_0(void fp, struct __blockTest_block_desc_0 *desc, int flags=0) {     impl.isa = &_NSConcreteStackBlock;     impl.Flags = flags;     impl.FuncPtr = fp;     Desc = desc;   } };

struct __block_impl {   ///isa指针,指向一个类对象,有三种类型:_NSConcreteStackBlock、_NSConcreteGlobalBlock和_NSConcreteMallocBlock   void isa;     ///block的负载信息(引用计数和类型信息),按位存储   int Flags;     ///保留变量   int Reserved;     ///指向Block执行时调用的函数,也就是Block需要执行的代码块   void FuncPtr; };

///Block 执行时调用的函数 static void __blockTest_block_func_0(struct __blockTest_block_impl_0 __cself) {         NSLog((NSString )&__NSConstantStringImpl__var_folders_59_c8zx7n553c34b8d791v8l7ww0000gn_T_main_ab6df5_mi_0);     }

static struct __blockTest_block_desc_0 {     ///Block版本升级所需要的预留区空间   size_t reserved;     ///Block的大小=sizeof(struct __blockTest_block_impl_0)   size_t Block_size; } __blockTest_block_desc_0_DATA = { 0, sizeof(struct __blockTest_block_impl_0)};

void blockTest() {     ///block变成了一个指针,指向一个通过__blockTest_block_impl_0构造函数实例化的结构体实例     ///__blockTest_block_func_0表示Block块的函数指针     ///__blockTest_block_desc_0_DATA作为静态全局变量初始化__main_block_desc_0的结构体实例指针     void(block)(void) = ((void ()())&__blockTest_block_impl_0((void )__blockTest_block_func_0, &__blockTest_block_desc_0_DATA));     ///调用Block     ((void ()(__block_impl ))((__block_impl )block)->FuncPtr)((__block_impl *)block); }

int main(int argc, char * argv[]) {     / @autoreleasepool / { __AtAutoreleasePool __autoreleasepool;          blockTest();     } } ```

画个图来表示各个结构体之间的关系就是:

截屏2022-06-12 14.30.59.png

Block的底层数据结构也可以用一张图表示:

截屏2022-06-26 14.39.40.png

探索Block的变量捕获

Block根据其类型可以分为三类:全局区Block、栈区Block、堆区Block。然而通过上面Block的C++底层实现,可以看到__block_impl有一个属性isa,而这个isa指向的对象有三种类型,也就是这三种Block。通过这个也可以看出,Block的也是一个对象。

截获auto变量值

oc void blockTest() {     int age = 20;     void (^block)(void) = ^{         NSLog(@"Hello==%d", age);     };     block(); } 通过Clang指令生成C++代码:

```oc struct __blockTest_block_impl_0 {   struct __block_impl impl;   struct __blockTest_block_desc_0* Desc;   int age;

__blockTest_block_impl_0(void fp, struct __blockTest_block_desc_0 desc, int _age, int flags=0) : age(_age) {     impl.isa = &_NSConcreteStackBlock;     impl.Flags = flags;     impl.FuncPtr = fp;     Desc = desc;   } }; ``` 可以看到__blockTest_block_impl_0多了一个成员变量age,并且构造函数也多了一个参数age,传的仅仅是a ge的值。可以得出Block捕获的值,所以要想在Block内部修改局部变量的值是不行的。

使用static修饰变量

把上面的age改成使用static修饰 oc void blockTest() {     static int age = 20;     void (^block)(void) = ^{         NSLog(@"Hello==%d", age);     };     block(); } 通过Clang指令生成C++代码:

oc struct __blockTest_block_impl_0 {   struct __block_impl impl;   struct __blockTest_block_desc_0* Desc;   int *age;   __blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {     impl.isa = &_NSConcreteStackBlock;     impl.Flags = flags;     impl.FuncPtr = fp;     Desc = desc;   } }; 可以看出捕获的不再是变量的值,而是变量的指针地址,所以也可以在Block内部修改age的值。并且static修饰的局部变量叫作静态局部变量,是存储在静态存储区,这块内存只有在程序结束才会销毁,但是只是在声明它的代码块可见,所以传入变量的指针也不用担心变量销毁的问题。

全局变量

oc int age = 20; void blockTest() {     void (^block)(void) = ^{         NSLog(@"Hello==%d", age);     };     block(); } 通过Clang指令生成C++代码:

oc struct __blockTest_block_impl_0 {   struct __block_impl impl;   struct __blockTest_block_desc_0* Desc;   __blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int flags=0) {     impl.isa = &_NSConcreteStackBlock;     impl.Flags = flags;     impl.FuncPtr = fp;     Desc = desc;   } };

可以看出并没有把全局变量age捕获,是直接访问全局变量。

进而也可以得出如下结论:

截屏2022-06-12 14.22.43.png

__block修饰变量

如果想修改局部变量的值,可以通过__block修饰实现。

oc void blockTest() {     __block int age = 20;     void (^block)(void) = ^{         age = 26;         NSLog(@"Hello==%d", age);     };     block(); } 通过Clang指令生成C++代码:

截屏2022-06-12 15.31.40.png

__blockTest_block_impl_0多出来一个成员变量__Block_byref_age_0 *age,我们看到经过__block修饰的变量类型变成了结构体__Block_byref_age_0,block捕获的是__Block_byref_age_0类型指针。调用函数的时候先通过__forwarding找到age指针,然后去取出age值。

并且可以看到这次的C++代码多出了两个函数:

截屏2022-06-12 15.41.21.png

__blockTest_block_copy_0中调用的是_Block_object_assign__blockTest_block_dispose_0中调用的是_Block_object_dispose

截屏2022-06-12 15.42.43.png

并且这个两个函数都有个参数8,看注释说是这个枚举值BLOCK_FIELD_IS_BYREF,在Block_private.h 中可以查看到:

截屏2022-06-12 15.46.43.png

这些枚举值表示的含义分别为:

  • BLOCK_FIELD_IS_OBJECT:OC对象类型
  • BLOCK_FIELD_IS_BLOCK:是一个block
  • BLOCK_FIELD_IS_BYREF:在栈上被__block修饰的变量
  • BLOCK_FIELD_IS_WEAK:被__weak修饰的变量,只在Block_byref管理内部对象内存时使用
  • BLOCK_BYREF_CALLER:处理Block_byref内部对象内存的时候会加的一个额外标记(告诉内部实现不要进行retain或者copy)

Block copy流程

```js // 拷贝 block // 如果原来就在堆上,就将引用计数加 1; // 如果原来在栈上,会拷贝到堆上,引用计数初始化为 1,并且会调用 copy helper 方法(如果存在的话 // 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身 // 参数 arg 就是 Block_layout 对象, // 返回值是拷贝后的 block 的地址 // 运行?stack -》malloc void _Block_copy(const void arg) {     struct Block_layout *aBlock;     // 如果 arg 为 NULL,直接返回 NULL     if (!arg) return NULL;

// The following would be better done as a switch statement     // 强转为 Block_layout 类型     aBlock = (struct Block_layout )arg;     const char signature = _Block_descriptor_3(aBlock)->signature;

// 如果现在已经在堆上     if (aBlock->flags & BLOCK_NEEDS_FREE) {         // latches on high         // 就只将引用计数加 1         latching_incr_int(&aBlock->flags);         return aBlock;     }

// 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身     else if (aBlock->flags & BLOCK_IS_GLOBAL) {         return aBlock;     }

else {         // Its a stack block.  Make a copy.         // block 现在在栈上,现在需要将其拷贝到堆上         // 在堆上重新开辟一块和 aBlock 相同大小的内存         struct Block_layout *result =

(struct Block_layout *)malloc(aBlock->descriptor->size);

// 开辟失败,返回 NULL         if (!result) return NULL;

// 将 aBlock 内存上的数据全部复制新开辟的 result 上         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         // 将 flags 中的 BLOCK_REFCOUNT_MASK 和 BLOCK_DEALLOCATING 部分的位全部清为 0         result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed

// 将 result 标记位在堆上,需要手动释放;并且引用计数初始化为 1         result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1

// copy 方法中会调用做拷贝成员变量的工作         _Block_call_copy_helper(result, aBlock);

// Set isa last so memory analysis tools see a fully-initialized object.         // isa 指向 _NSConcreteMallocBlock         result->isa = _NSConcreteMallocBlock;         return result;     } } ```

在_Block_copy源码中,从栈区copy到堆区的过程中,_Block_call_copy_helper(result, aBlock)的调用时为了复制栈区的Block里面的成员变量,给堆区的Block。其实最终会发现调用的是这个函数_Block_object_assign,根据参数 flags 的类型(对象、block、byref...),做了不同的处理

  • 对象类型,增加对象的引用计数;
  • block类型,会对该block执行一次block_copy操作;
  • __block修饰,会调用_Block_byref_copy;

__blockBlock类似,如果在栈区,会重新malloc一份,进行深拷贝操作,但这两个的forwarding都会指向堆区的,如果已经在堆区,只会将其引用计数+1。上面也有提到关于__block修饰变量。

Block_release 流程

js void _Block_release(const void *arg) { // 1. 将指针转换为Block_layout结构 struct Block_layout *aBlock = (struct Block_layout *)arg; if (!aBlock) return; // 2. 如果是全局block,那么直接返回 if (aBlock->flags & BLOCK_IS_GLOBAL) return; // 3. 如果不是堆block,那么直接返回 if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return; // 4. 处理堆block的引用计数 if (latching_decr_int_should_deallocate(&aBlock->flags)) { // 5. 释放block捕获的变量 _Block_call_dispose_helper(aBlock); _Block_destructInstance(aBlock); // 6. 释放堆block free(aBlock); } }

Block的内存管理

之前提到Block的类型有三种,也就是__block_impl中的isa指向的对象有三种类型:

截屏2022-06-12 15.50.27.png

那在ARC环境下,哪些情况下编译器会自动把栈区Block拷贝到堆上

截屏2022-06-12 15.53.09.png

当把Block拷贝到堆上,会有哪些变化

oc typedef void(^Block)(void); int main(int argc, char * argv[]) {     @autoreleasepool {         NSObject *obj = [[NSObject alloc]init];         Block block = ^{             NSLog(@"%p",obj);         };         block();     } } 通过Clang指令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m 转成C++代码

截屏2022-06-12 16.04.34.png

可以看到捕获的对象被强引用了,进行了copy操作,在copy函数内部的_Block_object_assign会根据对象修饰符strong或者weak而对其进行强引用或者弱引用。

总结:

  • 当Block内部访问了对象类型的auto对象时,如果Block是在栈上,将不会对auto对象产生强引用。

  • 如果Block被拷贝到堆上,会调用Block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign会根据auto对象的修饰符(__strong,__weak,__unsafe_unretained)做出相应的操作,当使用的是__strong时,将会对auto对象的引用计数加1,当为__weak时,引用计数不变。

  • 如果Block从堆上移除,会调用block内部的dispose函数,内部会调用_Block_object_dispose函数,这个函数会自动释放引用的auto对象。

解决Block的循环引用

我们知道产生循环引用的条件是相互持有,就像下面这个图画的一样

截屏2022-06-12 16.19.28.png

要想解决这个问题,就是打破这个循环,通过__weak修饰对象

截屏2022-06-12 16.21.12.png

__unsafe_unretained也可以解决循环引用,不安全,指向的对象销毁时,指针存储的地址值不变

截屏2022-06-12 16.22.55.png

__block修饰对象,不用的时候把对象置为null,也一样可以打破循环

截屏2022-06-12 16.28.30.png

以上就是最近我整理的关于Block的知识点,文章中如有纰漏,希望大家指正。