Block原理解析

Block是什么?

Block实际上是Objective-C对闭包的实现。

关于闭包的概念:
In programming languages, a closure is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.
闭包是包含了非本地变量(也叫作自由变量或upvalues)的函数或函数的引用,这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。

正文

文章的结构,将分为三个部分,具体如下:

一.阅读C++中的Block源码
    1.最简单的Block
    2.截取自由变量的Block
    3.使用__block的Block
二.3种Block对象类型
    1._NSConcreteGlobalBlock
    2._NSConcreteStackBloc    
    3._NSConcreteMallocBlock
三.循环引用ARC

一.阅读C++中的Block源码

Block实际上是作为极普通的C语言源代码来处理的,通过支持Block的编译器,含有Block语法的源代码转换为一般C语言编译器能够处理的源代码,并作为极为普通的C语言源代码被编译。我们可以在终端通过clang(LLVM编译器)将Objective-C的代码转换为C++源代码,具体指令为:clang -rewrite-objc 源代码文件名,即可在文件目录下生成相应的同名cpp文件。

1.最简单的Block

这是一个最简单的block,没有任何外部变量,只在block块中执行一条printf语句。我们通过clang将main.m转换为main.cpp。通过sublime text打开cpp文件,会看到将近10万行的代码,直接滑动到文件最下方,找到我们需要的学习的代码,如下图所示:

在main函数中可以看到两行关于block的代码,第一行是声明、初始化blk变量,第二行则是调用block方法。blk变量被指向了一个叫做__main_block_impl_0的结构体,结构体的构造方法中要传入两个参数,一个是void *fp,表示函数指针,另一个是__main_block_desc_0,存储了block的描述信息。函数指针指向的是__main_block_func_0,这是一个static函数,函数中只有一行代码,就是我们要执行的printf语句。可以看到__main_block_func_0函数有一个传参struct __main_block_impl_0 *__cself,表示block本身,用途类似于OC消息机制要传入self,由于blk没有引用外部变量,所以在当前的函数中没有使用到__cself。__main_block_desc_0有一个静态的结构体实例__main_block_desc_0_DATA,在初始化blk变量的时候传入的就是这个实例,在该结构体中有保留值reserved,以及block的大小Block_size。
__main_block_impl_0中包含了一个通用struct,__block_impl。所有的block都会包含这个结构体,变量FuncPtr就是函数指针,可以看到该结构体包含了isa指针,表明Block本质上也是个OC对象。图中isa赋值的_NSConcreteStackBlock就是其中一种Block类。

关于Block的结构,有如上一个导图。该结构和clang分析出来的本质是一样的,只是变量名和结构体嵌套略微不同。invoke就是函数指针,由于我们没有使用外部变量,所以不存在variables和descriptor中的copy、dispose。

1.isa指针,所有对象都有该指针,用于实现对象相关的功能。

2.flags,用于按bit位表示一些block的附加信息,block copy的实现代码可以看到对该变量的使用。

3.reserved,保留变量。

4.invoke,函数指针,指向具体的Block实现的函数调用地址。

5.descriptor,表示该Block的附加描述信息,主要是size大小,以及copy和dispose函数的指针。

6.variables,截取过来的变量,Block能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。

额外的说下,虽然结构体的嵌套有差别,但本质是一样的,结构体本身并不带有任何额外信息,下图中TestA和TestB在内存上是完全一样的:

另外再说下clang得到的cpp中关于block的调用方式:
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
这里有一个有意思的强制转换,就是将指向__main_block_impl_0的blk强转成子结构体__block_impl,然后直接调用__block_impl的FuncPtr(函数指针)。可以这样强转的原因在于__block_impl位于__main_block_impl_0的最顶部。举个例子,如下图所示:

上图两次输出的数字分别是2和1。第二次强转所获得的valueB所存储的值,实际上是TestB中的变量a,因为它是TestB最顶部的int值。结构体的本质是,我们和C语言约定了一段内存空间的长短,及其内容的安排,它和int等类型一样,都是数据类型,其他类型怎么转换,结构体就怎么转换。当把TestB强转成ValueB后,会按照ValueB的格局对该内存空间进行解释,而这段内存的第一段长度等于int类型的空间存储的是TestB的a的值,即为1。

2.截取自由变量的Block

我们这次在Block外部定义一个局部变量test_value,在Block的代码块中输出该变量。同样使用clang获取cpp文件如下:

可以看到在__main_block_impl_0结构体中多了一个叫做test_value的int值。在__main_block_func_0中,首先通过__cself->test_value获取int值,然后再进行printf输出。注意到此处的test_value是属于Block中的一块内存空间,和Block外部的test_value没有了关联。
所谓的截取自动变量值,意味着在执行Block初始化语句时,将Block所使用的自动变量值保存到Block的结构体实例中,在Block内部修改该变量值并不能影响原先的变量。
如果在Block块中对test_value执行赋值语句,并不能改变Block外部的test_value变量,实际上,编译器会进行报错。

3.使用__block的Block

对Block外部变量添加__block修饰符就可以在Block块中对变量进行修改,我们通过clang看下它的实现原理。

这回代码多了很多东西,可以看到__main_block_impl_0中的test_value变成了一个指向__Block_byref_test_value_0的指针。在main函数中,初始化test_value并不是简单的新建一个int型变量,而是构造了一个__Block_byref_test_value_0的结构体。test_value变成了一个对象,在它的结构体中,属性test_value用来存储原先的int值,__forwarding指针指向了初始化的结构体本身,即使该结构体被拷贝到Block中,__forwarding指针仍然指向最初的那个结构体,所以使用该变量的方式是test_value->__forwarding->test_value。

二.3种Block对象类型

由于Block也是Objective-C对象,所以它有相应的类。目前有三种Block类:

NSConcreteGlobalBlock,全局的静态Block,不会访问任何外部变量。

NSConcreteStackBlock,保存在栈中的Block,当函数返回时会被销毁。

NSConcreteMallocBlock,保存在堆中的Block,当引用计数为0时会被销毁。

1.NSConcreteGlobalBlock

这种情况的block是一个global类型,在通过NSLog输出为global类型,并且在clang的cpp文件中能看到impl的isa赋值成为了_NSConcreteGlobalBlock。

这种情况下的block仍然是global类型,通过NSLog输出的仍然是NSGlobalBlock。但是在clang转换的cpp中,由于clang改写的具体实现方式和LLVM不太一样,并且这里没有开启ARC,我们看到的isa指向的是stack类型。在开启ARC时,block应该是global类型。

因为不需要对自动变量进行capture截获,所以Block用结构体实例的内容不依赖于执行时的状态,因此整个程序只需要一个实例。这样就可以将Block的结构体实例放在和全局变量相同的数据区域。

判断是否为global类型的Block可以依据以下条件:

1.记述全局变量的地方有Block语法时

2.Block语法的表达式中不使用应截获的自动变量时

2.NSConcreteStackBlock

在MRC中调用了外部变量的Block就会是一个stack类型,在NSLog中可以看到。注意在ARC中已经不存在stack类型的Block了。

3.NSConcreteMallocBlock

当Block从栈上复制到堆上时,isa就会被修改为malloc类型。Block执行copy方法就会进行栈到堆的拷贝,若已经是malloc类的Block,则会对Block的引用计数+1,若是global类型执行copy则不起任何作用。

上图为MRC环境下,将stack类型的block进行拷贝得到的对象就是malloc类型。

再次使用clang获取cpp代码,可以注意到,descriptor中有copy和dispose方法,就是在Block进行copy时对__block对象也进行引用计数操作。
栈上的__block变量会被复制到堆上,这时会将成员变量__forwarding的值替换成复制堆上的__block变量的地址

什么时候会将栈上的Block复制到堆?

在以前版本的ARC中:</br>
1.调用Block的copy实例方法时</br>
2.Block作为函数返回值返回时</br>
3.将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时</br>
4.在方法中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时

现在,在ARC开启的情况下,将会只有NSConcreteGlobal和NSConcreteMallocBlock类型的block。由于ARC已经能很好的处理对象的声明周期的管理,这样所有对象都放到堆上管理,对于编译器实现来说,会比较方便。

查看以上代码生成的cpp文件,__block和descriptor中都有copy和dispose方法,descriptor中的方法用来对__block实例进行引用计数操作,__block中的方法用来对array进行引用计数操作。

上两张图的输出结果都是0,主要说下图二,Block持有了__block对象,但是__block对象无法持有__weak修饰的NSArray对象,所以执行Block方法块时NSArray对象已经被释放。

三.循环引用

执行上方的代码,Person的dealloc不会被调用,因为blk与Person实例相互引用了。

使用__block变量同样不能解决循环引用,因为Block引用了__block对象,__block对象引用了self,self引用了Block。

在Block代码块内对__block对象赋值nil可以避免循环引用,但是如果没有执行过Block或忘记赋值nil都会引起循环引用。使用__block的优点是,可控制变量的持有期。

使用__weak可以让Block无法持有Person实例,从而避免了循环引用。另外,为了方式在Block执行半途时Person实例被释放,通常在Block方法块中先创建一个__strong的Person指针对其进行持有,当Block方法块结束后,strongSelf就会被释放。

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

推荐阅读更多精彩内容

  • Block实际上是Objective-C对闭包的实现。 关于闭包的概念: In programming langu...
    chushen61阅读 340评论 0 0
  • Blocks Blocks Blocks 是带有局部变量的匿名函数 截取自动变量值 int main(){ ...
    南京小伙阅读 911评论 1 3
  • 序言:翻阅资料,学习,探究,总结,借鉴,谢谢探路者,我只是个搬运工。参考、转发资料:http://www.cnbl...
    Init_ZSJ阅读 899评论 0 1
  • 前言 Blocks是C语言的扩充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了这...
    小人不才阅读 3,757评论 0 23
  • 原创文章转载请注明出处,谢谢 这段时间重新回顾了一下Block的知识,由于只要讲原理方面的知识,所以关于Block...
    北辰明阅读 3,455评论 2 9