Block浅谈

776 阅读6分钟

入门分析

iOS开发的过程中免不了会用到block,今天来探究一下其底层结构和原理,接下来先上一段简单代码

image.png

使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m来将main.m文件转化为C++文件来一探究竟,由于该文件很大,故只会挑选关键内容来分析 首先找到main函数

image.png

可以看到最后一句就是我们调用block,由于其中包含很多强制转换,故简化后可看作 block->FuncPtr(block),是这样来调用的,而我们整体的block的结构是__main_block_impl_0的结构体,其中有三个结构体成员分别为:

  • __block_impl类型的:impl
  • __main_block_desc_0类型的:Desc
  • int类型的:age

image.png

__block_impl结构体如下:

image.png

可以看到其中有个成员变量FuncPtr是一个指针类型的成员变量

__main_block_desc_0结构体如下:

image.png

这里最主要的是记录block所需要的内存大小

重新看回到main函数中的第一句代码

image.png

开始调用了__main_block_impl_0结构体的构造函数来创建block,同时看到传入的第一个参数__main_block_func_0是一个函数指针

image.png

在构造函数中将这个函数指针赋予到FuncPtr中,所以之后的调用会是block->FuncPtr(block)如此调用。

结构

将上面的解释总结一下,大概能得到的一副这样的示意图

image.png

block的本质

  • block的本质也是一个OC对象,因为它内部也有一个isa指针
  • block是封装了函数调用及函数调用环境的OC对象

block底层结构如下图所示:

image.png

block的变量捕获

auto 局部变量的捕获

image.png

先来看这样一段代码,大家应该都知道答案是20,同样为了探索为什么是这样,使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m来查看

image.png

image.png

可以看到在__main_block_impl_0的构造函数中,将外部传入的age变量赋值给了结构体中的age成员变量

Tip

C++语法中构造函数会使用默认赋值,例如:__main_block_impl_0(void *fp, struct _main_block_desc_0 *desc, int _age, int flags=0) : age(_age),就是将外部传入的参数 _age 赋值给成员变量 age

static 局部变量的捕获

image.png

如果是这样,答案又该是多少呢? 同样使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m来查看

image.png

对比之前发现了略有不同,之前是直接把值传入了,这里传入了地址,接着往下看

image.png

可以看到这时候都是传递的指针,这个指针指向了age的地址,那么答案就应该是25

全局变量的捕获

image.png

使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m来查看

image.png

可以看到这时候并未进行捕获

总结

变量类型捕获到block内部访问方式
auto 局部变量值传递
static 局部变量指针传递
全局变量直接访问

block的三种类型

block的copy

ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,如以下情况:

  • block作为函数返回值时
  • block赋值给__strong指针时
  • block作为Cocoa API中方法名含有usingBlock的方法参数时(ex:NSArray的sort方法)
  • block作为GCDAPI的方法参数时

对象类型的auto变量

从开始研究的变量一直是基本数据类型,现在如果外部对象是对象类型的话会有什么变化呢

image.png

image.png

image.png

可以看到__main_block_desc_0的内部多了copydispose,这里就是方便在block对象被从栈拷贝到堆上时把外部变量需要使用_Block_object_assign对外部对象进行retain操作

那么如果是弱引用会是怎样呢? 同样使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m,但是这时候会发现爆出一个错误__weak是要使用runtime才可以的,所以这里要加新的参数进去来指定使用的runtime版本

image.png

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 main.m命令变成这样即可

image.png

总结block内部访问了对象类型的auto变量时:

  • block在栈上,不会对auto变量产生强引用

  • block被拷贝到堆上

    1. 会调用block内部的copy函数
    2. copy函数内部会调用_Block_object_assign函数
    3. _Block_object_assign函数会根据变量的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,类似于retain
  • block从堆上移除

    1. 会调用block内部的dispose函数
    2. dispose函数会调用_Block_object_dispose
    3. _Block_object_dispose会自动释放引用的auto变量,类似于release

__block修饰符

我们都知道block中若想修改捕获的外部变量并不能直接修改,直接修改会产生编译报错,例如:

image.png

那么到底为什么不能修改呢?还是看到之前分析的部分

image.png

可以看到age变量的作用域在main函数中,而改变age的值发生在__main_block_func_0函数中,并且在此函数中使用的age也并非变量age而是block记录的变量,所以我们想到用static修饰的局部变量是可以的,当然使用全局变量也可以,但是有个弊端这个变量会一直存放于内存中,所以引入了__block修饰符,我们来看一下,__block究竟做了什么

image.png

image.png

image.png

可以看到这里将age包装成了一个__Block_byref_age_0类型的对象,这个结构基本如下

image.png

__block的内存管理

  • block在栈上时,不会对__block变量产生强引用
  • block被拷贝到堆上时
    1. 会调用block内部的copy函数

    2. copy函数内部会调用_Block_object_assign函数会对__block变量形成强引用

image.png

  • block从堆中移除时
    1. 会调用block内部dispose函数
    2. dispose函数内部调用_Block_object_dispose函数
    3. _Block_object_dispose函数会自动释放引用的__block变量

image.png

对象类型的局部变量&__block变量

  • block在栈上时,对它们都不会产生强引用
  • block拷贝到堆上时,都会通过copy函数来处理
  • _Block_object_assign函数会根据所指向对象的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,形成强引用或者弱引用(仅有ARC时会retain,MRC时不会)
  • block从堆上移除时,都会通过dispose函数来释放

__block中的__forwarding指针

从之前的分析中能看到使用__block修饰的变量会被包装成一个新的对象,其中有一个__forwarding指针,当时比较奇怪为什么要访问外部变量时要通过这个指针来取得,主要原因是当block拷贝到堆上后确保__forwarding指针都访问的对象的确在堆上。

image.png

解决循环引用

循环引用的问题经常出现于使用block的场景,其本质就是互相持有导致被持有的对象都无法正常释放,接下来会分为ARC环境下和MRC环境下的解决

ARC

ARC环境下有两种解决方案:

  • __weak__unsafe_unretained

image.png

image.png

image.png

  • 使用__block解决(缺点:必须要调用block)

image.png

这个原理也很简单,因为__block修饰的变量生成的结构体会被block对象强引用,结构体对象同时会强引用self,self也同时强引用了block,形成三角关系的引用,最后的置空会打破这个三角关系,从而打破循环引用。

image.png

MRC

MRC环境下时,__block修饰的对象类型不会在block拷贝到堆上时对外部变量进行强引用,那么根据这个规则,在MRC时打破循环引用使用__unsafe_unretained__block