iOS Block原理

一 Block的实现

1. 在main函数中声明、实现并调用一个block

block的声明、实现和调用

2. 然后我们通过clang命令将main.m转为c++文件

clang命令:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m


block的c++实现

3. block的声明和实现

void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

在block的实现中使用了__main_block_impl_0函数,并将__main_block_impl_0函数返回值的地址赋值给了我们声明的block变量“block”

4. __main_block_impl_0

__main_block_impl_0函数结构

在__main_block_impl_0的结构中我们看到一个同名的构造函数(上图红框标记的函数)。这里我们可以看出,实现block时,调用__main_block_impl_0函数,并返回了一个__main_block_impl_0结构体,并取了此结构体的地址,所以变量“block”的实质是一个__main_block_impl_0结构体的地址。

5. __block_impl

__block_impl

可以看出__block_impl结构与oc对象一样,都有一个isa指针。其中FuncPtr字段保存的就是自定义逻辑代码块转化后函数的指针。

5 __main_block_impl_0构造函数的调用

此构造函数有三个参数,其中第三个参数在构造函数声明时直接赋值为0(int flags=0),所以我们不用考虑第三个参数,主要分析另外两个参数,__main_block_func_0和&__main_block_desc_0_DATA

__main_block_impl_0构造函数的调用

6. 参数__main_block_func_0

__main_block_func_0结构

这里有个NSLog,我们分析这里就是我们实现block时的具体逻辑。我们在实现block时,将自定义逻辑的代码块封装成了__main_block_func_0函数,并将__main_block_func_0函数的地址作为参数传入了__main_block_impl_0构造函数,将__main_block_func_0存储在__main_block_impl_0结构体中。

7. 参数&__main_block_desc_0_DATA

__main_block_desc_0_DATA结构

在上图中我们看到__main_block_desc_0_DATA是对结构体__main_block_desc_0初始化后的结果。在初始化时reserved被赋值为0,Block_size中存储着__main_block_impl_0结构体占用空间的大小。

8. __main_block_impl_0构造函数的实现

__main_block_impl_0构造函数的实现

我们可以看到:

impl.isa存储着表示block类型的内容,这表明block实质上是一个OC对象。

impl.Flags中存储着0,是源码中写死的,暂时不讨论此参数。

impl.FuncPtr中存储的是block自定义逻辑代码块封装的函数地址。

Desc中存储着整个__main_block_impl_0结构体所占空间的大小。

9. 一张图总结一下block的实现过程

block实现过程

二 Block的调用

block调用

我们知道实现block时,自定义逻辑的代码块是存储在了FuncPtr中,所以调用过程实际就是找到FuncPtr并调用。

__main_block_impl_0结构体内存示意图

我们从上面__main_block_impl_0结构体内存示意图中看出,__main_block_impl_0结构体的第一个成员是impl,它的地址与__main_block_impl_0的地址其实是一样的,所以我们直接把__main_block_impl_0结构体强转为__block_impl结构体,然后直接调用其中的FuncPtr。

这样就完成了block的调用。

三 Block的变量捕获

1. 捕获局部自动变量

捕获局部自动变量

上图中变量a是一个局部自动变量(省略了auto修饰词)。从图中我们可以分析出,block中访问block外的局部值类型参数时,是通过__main_block_impl_0结构体缓存住参数值,然后再传入__main_block_func_0函数中的。

这是一个简单的变量捕获过程。

2. 捕获多种值类型变量

捕获多种值类型变量

这里声明了4个变量:a,局部自动变量;b,局部静态变量;c,全局变量;d,全局静态变量。

这四个变量被block捕获的逻辑如下:

a:通过__main_block_impl_0结构体缓存住a的值,然后通过__main_block_impl_0结构体获取到a的值

b:通过__main_block_impl_0结构体缓存住b的地址,然后通过__main_block_impl_0结构体获取到b的地址,最终获取到b的值

c:直接访问

d:直接访问

这样我们就能解释下图这种情况了

实现block修改局部变量的值

我们在实现block之后修改了变量a、b、c、d的值,在打印时发现,a的值并没有改变。这是因为在实现block的值被赋值给了__main_block_impl_0结构体内的成员,这时block中使用的a已经不是block外声明的那个a了。

四 Block的类型

1. block的类型

block类型

图中定义了6种情况的block:

1. 没有使用外部变量     2. 使用了局部变量            3. 使用了静态局部变量

4. 使用了全局变量        5. 使用了全局静态变量     6. 直接调用

结果可总结为:

block类型

这里我们看到block有三种类型,分别为NSGlobalBlockNSMallocBlockNSStackBlock

2. block类型间的关系与区别

从上面6个block的分析中我们可以看出当block中没用通过__main_block_impl_0结构体直接存储变量的值时,block的类型为NSGlobalBlock。

当block中通过__main_block_impl_0结构体重存储了变量的值时,block的类型为NSStackBlock。

但是这里有一个特例,block1也在__main_block_impl_0结构体重存储了变量的值,但是它的类型却是NSMallocBlock。

这是因为在ARC模式下,如果有变量引用了block,则block会自动被copy一次,将block从栈区拷贝到堆区。此时block的内存控制由引用计数控制。在MRC过程中是没有copy操作的。

由此我们可以知道,我们在ARC环境中生命block属性时,使用copy和strong关键词修饰最终的效果都是一样的,用strong修饰时block也会自动copy。我们现在还是用copy修饰block属性主要是延用MRC时代的写法。

不同类型的block存储位置

NSGlobalBlock:存储在数据段(全局区),内存在程序结束后由系统回收。

NSStackBlock:内存由编译器控制,过了作用域就会被回收。

NSMallocBlock:内存根据引用计数控制。

3. block clang后的类型

block clang后的类型

我们可以看到,block0~block5对应着__main_block_func_0~__main_block_func_5这个五个结构体,下面我们分别看下这五个结构体中的isa指针指向什么。

block clang后的类型

clang之后的block类型全部都是_NSConcreteStackBlock类型,与我们之前打印的结果不一致。我在网上找到了一些其他大神的分析,记录在这里。

1. block0,block2,block3,block4并未使用外部变量,应该是一个_NSConcreteGlobalBlock,而用clang显示的却是_NSConcreteStackBlock,唐巧的说法是:clang 改写的具体实现方式和 LLVM 不太一样。

2. 把编译后的代码copy到mm中后

((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))();

此行会crash,因为((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))指向的是impl的isa,也就是_NSConcreteStackBlock的指针。

需要改写成

struct __main_block_impl_0 impl_0 = __main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);

((void (*)())(impl_0.impl.FuncPtr))();

才能执行成功。

或许clang改写的实现跟LLVM确实不一样吧。

作者:WhiteZero链接://www.greatytc.com/p/d96d27819679来源:简书著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

4. block类型的具体实现

我们在block捕获局部自动变量a时发现,clang后的block实现自动添加了一个变量a,这个a是从哪里来的?我们需要进一步探寻block类型的底层结构。

block中自动多了一个参数a

源码简要分析

我们在源码中找到Block_private.h文件中找到Block_layout结构体,此结构体就是block的真实结构。

最后面有个注释,表示输入的变量也会保存在此结构体中,旁边的内存示意图中variables就是保存输入的变量的空间。

其中的isa就是指向block类型的(_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock)

block类型结构

五 Block捕获对象类型参数

1. 源码分析

block源码结构

搞清楚了block的源码结构之后,我们再来看看NSMallocBlock类型的block是怎么通过copy操作放进堆区的。

1. Block_copy()

我们现在知道NSMallocBlock类型的block是对NSStackBlock类型的block进行了一次copy后得到的,我们现在要来研究一下这个copy方法中做了什么操作。

我们在block源码中的Block.h中看到了这个copy方法的定义,叫做Block_copy(...)

Block_copy(...)的定义

其本质是_Block_copy()方法,这个方法的具体实现在runtime.cpp文件中

_Block_copy()方法分析
objc引用计数

如图,objc的引用计数为3,这是因为,new出来的内存区域在赋值给objc时引用计数加一,在创建stack类型block时,捕获外部变量过程中会拷贝一份,这时引用计数加一,在将block赋值给变量“block”时又执行了一次copy操作,这时objc的引用计数又加了一,所以此时objc的引用计数是3。

3. Block_descriptor_2

我们从上边_Block_copy()方法分析中看到,在copy过程中调用了_Block_call_copy_helper函数,在这个函数中判断了是否有Block_descriptor_2结构体,如果有的话我们将调用其中的copy函数。

我们先来看下Block_descriptor_2结构体是什么样的。

Block_descriptor_2结构

其中copy和dispose都是一个函数的指针,而这两个函数具体在哪里实现的呢?

我们构造一个block,不能block中捕获一个OC对象的变量,clang之后就可以看到这两个函数。

NSMallocBlock类型block结构

我们在__main_block_desc_0_DATA结构体中看到,多了两个字段,而且在初始化时将__main_block_copy_0和__main_block_dispose_0这两个函数的地址传了进去。

我们接下来分析这两个函数。

4. __main_block_copy_0

__main_block_copy_0函数的本质是_Block_object_assign函数,我们分析一下_Block_object_assign函数。

_Block_object_assign()函数分析

5. __main_block_dispose_0

__main_block_copy_0函数的本质是_Block_object_dispose函数,我们分析一下_Block_object_dispose()函数。

_Block_object_dispose()函数

6. 总结一下

(1)在实现block时,如果block引用了外部局部变量,且block被引用时,block的类型被设置为NSMallocBlock类型

(2)对象的传递与值类型一样也是在__main_block_impl_0结构体中新增一个字段保存

(2)在block中捕获OC对象的参数后看到,系统自动构造出了__main_block_copy_0和__main_block_dispose_0两个函数

(3)__main_block_copy_0和__main_block_dispose_0这两个函数保存在__main_block_desc_0_DATA结构体中

(4)NSMallocBlock是NSStackBlock copy得来的,在copy的过程中通过__main_block_copy_0和__main_block_dispose_0这两个方法对捕获的对象进行内存管理。

六 __block修饰符

1. __block修饰符源码分析

当我们希望在block中修改变量的影响范围扩展到block之外,我们需要在声明变量时使用__block修饰符,那这个修饰符具体是怎么样实现这一功能的?我们先clang一下,看下具体实现是怎样的。

__block底层实现

可以看到,用__block修饰的变量被构造成了__Block_byref_objc_0类型的结构体,在构造函数__main_block_impl_0初始化时,对象objc被赋值为__Block_byref_objc_0结构体的__forwarding字段。

我们先来看下__Block_byref_objc_0结构体的结构。

__Block_byref_objc_0结构

这里看到前面赋值给objc字段的__forwarding也是一个__Block_byref_objc_0结构体,还是看不出来__block是如何实现的。

2. __block修饰符原理

我们先将main.h文件修改到mrc模式,修改方式如下

mrc模式

修改到mrc模式之后实现下面的代码

__bllock修饰原理

打印结果如下

__block修饰原理打印结果

在copy的过程中,有__block修饰的OC对象,会被构造在__Block_byref_object_0结构体的__forwarding字段中。

在栈block中,__forwarding指向的是自己(栈中的__Block_byref_object_0结构体),栈block经历了copy操作之后,将__Block_byref_object_0结构体拷贝到了堆中。

堆中__Block_byref_object_0结构体的__forwarding指向的也是自己(堆中的__Block_byref_object_0结构体),

但是此时栈中__Block_byref_object_0结构体的__forwarding指向的不是自己了(指向了堆中的__Block_byref_object_0结构体)。

我们看看此时block中使用objc的具体实现

使用__block修饰的对象

在使用objc时通过__forwarding重新指向了对象的存储位置,这样copy之后无论是在栈block中还是堆block中访问的都是同一个objc对象了。

最后我们还可以自己尝试一下,在block copy之后在block外直接打印objc,此时objc指针位置也与其他block中的objc同步了。

__forwarding的操作是在__main_block_copy_0函数当中的__Block_byref_copy()函数中实现的。

参考文献

iOS底层原理探索— block的本质(一)

史上最详细的Block源码剖析

Block 签名信息的使用

公众号

关注公众号,回复26H49获取block源码

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,966评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,170评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,909评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,959评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,851评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,583评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,956评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,590评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,878评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,892评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,719评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,501评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,957评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,124评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,440评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,003评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,211评论 2 339