[iOS 理解] block

第一步,初步理解
网上博客千篇一律的内容,我找了一篇质量还不错的
看完链接那篇,就可以应付大厂面试了,但还缺些东西。

// 必须完成第一步再看以下内容

第二步
上面链接的文章里已经贴了大量源码了,但是,闭包源码的核心注释,他没贴
那50行注释是真的精华,本文就是围绕这点补充的。

官网链接,官网源码不是 Xcode 项目
如果想看的舒服就要自己配 Xcode 关键字高亮、点击跳转
我这有配好的链接

block 核心文件只有一个,几百行,几个函数,读者也可以自己分析(提示,原注释必须真正理解)

下面是我的分析。。// 必须先完成第一步


闭包是一个结构体,当它捕获外部变量时,一个变量要么是 __block 声明的,否则就剩:OC 对象,C++ 栈上对象,闭包对象,值类型。

关于值类型:比如 int 这类基础数据类型,指针,结构体。指针也是?指针当然是,指针本质是 long 类型。OC 对象 id 不是 objc_object* 吗?编译器可以判断出一个指针是不是 objc_object*,然后视为 OC 对象。怎么判断?我想的一个办法是取出 isa,比较 magic。。。扯远了,总之,编译器可以知道捕获的一个变量是什么类型的,就算手动欺骗编译器,最坏的后果是内存泄露,没必要。

C++ 栈上对象?堆上 new 出来的对象,因为不像是 OC 是管理引用计数的,它只需要某个位置正确的 delete,所以传给闭包时,只需要传对象的指针,这个指针和 int* 和 int 没区别,值类型。那 C++ 栈上对象不就和值类型结构体一样吗?但是,既然是对象这个概念,就要尊重对象可能的特殊行为,C++对象恰好有拷贝构造函数,所以和结构体有一点点区别,需要特殊操作一下,具体操作后文会说。

__block 声明的变量实际是一个结构体,闭包结构体里保存了这个结构体的指针。所以转移到堆的时候,必然要把变量结构体也复制到堆上。

说了这么多,到底想表达什么?
捕获的变量保存在在闭包结构体内,闭包复制到堆上时,根据变量的类型、声明方式需要不同的额外操作,或者不需要额外操作。原注释:

A Block can reference four different kinds of things that require help when the Block is copied to the heap.

  1. C++ stack based objects
  2. References to Objective-C objects
  3. Other Blocks
  4. __block variables

上面四种之外,都不需要额外操作;否则编译器会生成 copy 和 dispose。如果捕获的是 C++栈上对象,函数内会调用 const 拷贝构造,闭包释放时调用析构函数;剩下三种在 copy 和 dispose 里调用 _Block_object_assign(_Block_object_assign 后面会说)。也就是第二段注释:

In these cases helper functions are synthesized by the compiler for use in Block_copy and Block_release, called the copy and dispose helpers. The copy helper emits a call to the C++ const copy constructor for C++ stack based objects and for the rest calls into the runtime support function _Block_object_assign. The dispose helper has a call to the C++ destructor for case 1 and a call into _Block_object_dispose for the rest.

剩下这三种,都调用 _Block_object_assign,编译器具体怎么区分的?直接定一个 flag 作参数,传给 _Block_object_assign,分情况讨论呗。flag:
OBJECT(3,二进制11)
BLOCK(7,二进制111)
BYREF(8,二进制1000),BYREF 就是 __block variables

注释在这,注释下面先回顾一下闭包复制。对了,这些需要被帮的,闭包结构体的 flag 会打开标志位 BLOCK_HAS_COPY_DISPOSE

The flags parameter of _Block_object_assign and _Block_object_dispose is set to
* BLOCK_FIELD_IS_OBJECT (3), for the case of an Objective-C Object,
* BLOCK_FIELD_IS_BLOCK (7), for the case of another Block, and
* BLOCK_FIELD_IS_BYREF (8), for the case of a __block variable.
If the __block variable is marked weak the compiler also or’s in BLOCK_FIELD_IS_WEAK (16).

(为什么 只有 __block 配 __weak,变成 8 + 16,二进制 11000?
为什么 __weak 配 id 这么正常的情况,注释没写???)

回顾 block copy

假设一个闭包 block,[block copy] 就是 objc_retainBlock(block)。

打开 runtime 源码搜索:
objc_retainBlock
	_Block_copy(block)
		// 用我配好的链接可以直接点击跳转到实现
		// 跳转到 block 源码
		_Block_copy_internal
			如果是堆 block,block 的引用计数 +1
			// block 的引用计数不能通过 CFGetRetainCount 获得
			// 是自己通过 flags 管理的
			如果是全局 block,直接返回 block
			否则就是栈 block:
				malloc,memmove
				设置 flag 标志位:堆 block,引用计数置 0
				设置 isa 为堆 block
				如果 BLOCK_HAS_COPY_DISPOSE:
					descriptor->copy(dst, src),
				返回堆 block  
			

执行 descriptor->copy(dst, src),注意这是闭包的(闭包的描述符的,给闭包结构体用的),一会变量结构体也可能有,不要混了。
闭包(的描述符中)的 copy,根据前面的分析,会分情况讨论设置不同的 flag:OBJECT (3) BLOCK (7) BYREF (8或 weak24), 作为参数传给内部调用的:
void _Block_object_assign(void *destAddr, const void *object, const int flags);

如果 flag 是 7,就说明捕获了一个闭包对象,操作就是,对闭包执行 copy 然后 assign 到自己闭包结构体里,因为执行了 copy,所以如果闭包内调用自己会循环引用,即递归调用,解决办法就是在递归终止时让闭包指针 = 0。
(这个 assign 单词很有意思,“指定”,就是当作值类型指定,很霸道的感觉。。)

如果 flag 是 3,说明捕获了一个 OC 对象,操作是执行 retain 然后 assign。
(前面提出了一个关于 __weak id 的问题,按照上面的逻辑,flag 为 3,执行 retain???我们知道 ARC 下,闭包内使用 __weak id 是不会增加引用计数的,但是这里却???说明编译器肯定在走到闭包源码之前就做了一些工作了,有兴趣的去研究吧,本文就不提 __weak id 了)

如果 flag 是 8,好玩的来了。如果 __block id 咋办?还有 __block __weak id 呢?先看原注释(后面也有中文的):

When a __block variable is either a C++ object, an Objective-C object, or another Block then the compiler also generates copy/dispose helper functions. Similarly to the Block copy helper, the “__block” copy helper (formerly and still a.k.a. “byref” copy helper) will do a C++ copy constructor (not a const one though!) and the dispose helper will do the destructor. And similarly the helpers will call into the same two support functions with the same values for objects and Blocks with the additional BLOCK_BYREF_CALLER (128) bit of information supplied.

So the __block copy/dispose helpers will generate flag values of 3 or 7 for objects and Blocks respectively, with BLOCK_FIELD_IS_WEAK (16) or’ed as appropriate and always 128 or’d in, for the following set of possibilities:
__block id 128+3
__weak __block id 128+3+16
__block (^Block) 128+7
__weak __block (^Block) 128+7+16

flag 为 8或24,说明引用的变量是 by_ref 结构体(变量结构体),操作是调用
_Block_byref_assign_copy(destAddr, object, flags);
目的是把变量结构体复制到堆。代码在下面,接下来这段话结合代码看。复制此结构体讲道理应该很简单,开辟内存,复制内存,这对于 __block int 的确是这么简单。但是最后要判断是不是复杂的 __block id,__block C++ 对象,__block 闭包对象。

如果是这种复杂变量结构体,变量结构体也会有自己的 flag,也有标志位 BLOCK_HAS_COPY_DISPOSE,作用是,在变量结构体复制最后,如果有 BLOCK_HAS_COPY_DISPOSE,那就调用 变量结构体的 copy,copy 内调用
void _Block_object_assign(void *destAddr, const void *object, const int flags);
(如果是C++,调用非const拷贝构造)(如果flag是24,变量结构体标志为 weak)

前面见过这个函数啊,怎么又来了?再捋一捋,前面闭包复制的时候,根据 3 7 8(24) 执行不同操作,现在是 8(24) 的子操作,执行 8(24) 的时候,内部要复制变量结构体,复制最后按需执行 变量结构体的 copy

这次调用 _Block_object_assign 时,编译器给变量结构体的 flags 是都加了 128 的
比如 __block id 是 128+3 = 131,(是不是第一步里见过 131?)
__weak __block id 128+3+16
__block (^Block) 128+7
__weak __block (^Block) 128+7+16

_Block_object_assign 判断 flag 是 128 系列的,不管有没有 weak,都直接 assign,不会再 retain,因为是在声明 __block 变量,产生变量结构体时,已经 retain 过的。

_Block_byref_assign_copy

_Block_byref_assign_copy(dst*, src, flag)
	如果该变量结构体已经在堆上(通过引用计数判断)
		引用计数 +1 (有可能多个闭包捕获一个 __block 变量)
	否则复制到堆:
		malloc 
		设置 flag
		设置 forwarding
		设置 size 等值,更新 src 的 forwarding
		如果有 BLOCK_HAS_COPY_DISPOSE
			执行变量结构体的 copy(改名了,byref_keep)
		*dst = src->forwarding 

block release 逆过程

闭包引用计数 -1
如果到 0 了 且是堆上闭包:
如果有 BLOCK_HAS_COPY_DISPOSE,调用 block 的 dispose
free

dispose 内 _Block_object_dispose 或调用 C++ 析构

_Block_object_dispose:

如果是 单7,对捕获的闭包调用一次 block release
如果是 单3,release
如果是 8,__block 声明的,变量结构体,执行 _Block_byref_release(object); 总体工作是让变量结构体计数 -1。到 0 了 free、执行 by_ref 的 dispose,注意两个 dispose 区分,和上面分析 block copy 的过程逆过程。

_Block_byref_release

前面说过有可能多个闭包捕获一个 变量结构体
变量结构体有自己的引用计数

变量结构体引用计数 -1
如果计数到 0 了
	如果有 BLOCK_HAS_COPY_DISPOSE	
		执行变量结构体的 dispose (改名 byref_destroy)
	free

变量结构体的 dispose,调用 _Block_object_dispose,flag 是加了 128 的,对应什么操作呢?无操作。为什么?因为之前 _Block_object_assign 判断 flag 是 128 系列是直接 assign 的,所以释放的时候要对称,不用任何操作。

再次提醒 assign 这个单词,指定,直白的指定,像对待值类型一样。
就像是老师布置的作业叫 assignment,指给你的任务就必须做。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章