1 背景 - 从函数指针到block
1.1 Block的引入
block是Objective-C中常用的语法成分, 典型的使用场合是传递行为(如回调).
但实际上, C语言中已经有能够传递行为的语法成分了, 就是函数指针. 它指向的是内存中的可执行代码. 对函数指针的解引用将会唤起对应的函数, 对这个函数的调用和普通的函数调用一样可以传入参数. 相较于"直接地"从确定的函数名引导到可执行代码, 函数指针通过从一个变量(也就是函数指针的值)"间接地"来调用函数, 这种间接的调用方式使它可以在运行时根据函数指针的值动态地选择对应的函数来执行.
函数指针的优势在于可以将一组对数据的操作(behavior, or rather, operations on data)也抽象成了数据(data), 这比较贴合面向对象的设计理念, 但它离真正的封装还有一定的距离——因为函数指针并不能真正地包含数据.
函数指针对外部数据的包含是通过参数列表来完成的, 这种方法需要程序的编写者自行维护, 灵活性差的同时, 维护成本和复用难度也高.
于是, 我们就想能不能创造这样一种语法, 它和函数指针一样包含了一组可执行代码, 但能够自动地捕获所用到的变量.
在Objective-C中, Block提供了这种能力.
1.2 Block的简单介绍
Block的使用方法大致如下:
在这里, 我们可以看到, Block和函数指针的声明很类似, 只是将星号(*)改成了脱字符(^). 实际上, Apple对C做Block这一拓展时, 也提到了它们的声明只有这一区别[2].
实际上, Block语法为C语言引入了4个新的概念[3]:
- 一种新的复合类型, 也就是Block引用;
- Block的字面量;
- 一种新的存储类型及其所对应的修饰符
__block; - 语言原语
Block_copy和Block_release.
本文不打算直接开始讨论这四个新的概念与成分, 而是从设计者的角度, 从"程序开发者需要什么"入手, 设计一个类似于Block的语法成分——GuBlock, 在这个过程中带领大家更好地理解Block"做了什么", "为什么要这样做"和"这样做的好处体现在哪里"这三个方面的问题.
1.3 GuBlock = 函数指针 + 变量捕获 + 生命周期管理
我们基于函数指针"传递一段可执行代码"的能力, 在它的基础上添加在其声明所在的作用域中自动捕获其所使用的变量的功能.
这样就有:
但实际上, 如[1]中所说: 函数指针比普通指针更不容易出错的原因是——它不需要程序的编写者手动管理内存——因为它没有可供管理的内存, 而一旦GuBlock为函数指针添加上了属于它的变量, 那么就需要考虑生命周期的问题了. 因此这个"等式"应该被修改为如下添加过"副作用"的样子:
这是我们设计GuBlock的出发点和准则.
2 GuBlock的概念设计和代码实现
从上面的讨论中, 我们可以得出这样的结论: 在C/C++中函数指针的基础上, 为其添加它所使用到的变量.
函数指针代表着行为, 解引用它就代表着触发相关行为. 在此基础上, 我们将它所使用到的数据使用结构体来打包.
我们同样将GuBlock的形式声明为使用脱字符(^)这一一元构造符来创建GuBlock类型的对象.
// main.m
int main()
{
int a = 1;
void (^myGuBlock)(void) = ^ { printf("a = %d", a); };
myGuBlock();
return 0;
}
编译器会将myGuBlock重写成一个结构体, 其中包含着一根函数指针, 它能完成{ printf("a = %d", a); }这个函数体的行为. 同时, 由于这个函数体{ printf("a = %d", a); }中包含了与myGuBlock同作用域的变量a, 根据上面的设计, 这个结构体中会有这个数据a的值.
于是,main.m被重写后有这样的结果:
// main.cpp
struct rewritten_myGuBlock {
void (*funcPtr)(struct rewritten_myGuBlock *); // 实际的函数指针.
// 参数较原函数需要添加一个同类型的结构体指针.
// 思路仿照了C++的类成员函数, (隐含的)第一个参数总为this.
int capturedA; // 捕获的变量A.
};
对它的赋值和使用应当如下示:
// main.cpp
void function_of_myGuBlock_in_main(struct rewritten_myGuBlock * _cself)
{
printf("a = %d", _cself->capturedA);
}
int main()
{
int a = 1;
struct rewritten_myGuBlock rewrittenMyGuBlock = { function_of_myGuBlock_in_main, a };
rewrittenMyGuBlock->function_of_myGuBlock_in_main(rewrittenMyGuBlock);
}
也就是:
因此, 我们现在获得了一个简易的复合类型——GuBlock. 编译器将它重写成一个结构体, 这个结构体中包含着它对应的代码块所在的函数指针, 和该代码块中所捕获的数据.
此时, Block的设计稿如下示:
2.1 GuBlock针对变量作用域的优化
下图是GuBlock的初步设计, 对捕获到的不同类型的变量都以相同的方式打包.
这是一种很浪费的方式, 因为并不是每一个被捕获的变量都需要用这么复杂的方式存储.
2.1.1 自动变量的处理
自动变量, 或称局部变量, 它们的作用域局限在它们所处的代码块内, 因此具有块作用域. GuBlock中的函数指针所指向的代码块显然和局部变量的作用域不同:
因此, 我们需要在重写后的"GuBlock"这一结构体中存储一份局部变量的拷贝.
2.1.2 全局变量的处理
全局变量是文件级别的作用域, 因此, 在GuBlock的函数指针所指向的代码块中, 实际上是天然地可以访问到全局变量的, 因此并不需要专门地存储在"重写后的GuBlock"这一结构体中.
静态全局变量也是一种全局变量, 因此和全局变量的处理完全一致.
2.1.3 静态局部变量的处理
最后是静态局部变量.
静态局部变量的标识符仅在其所声明的代码块中可见, 但即使该代码块结束, 它的内存空间也不会被销毁归还. 由于在其所声明的代码块结束后, 它依然能够存活, 因此在静态局部变量所声明的块外它仍然存在被继续使用的可能. 为了维持静态局部变量的这一特性, 我们不能仅仅使用它的值, 而是需要记录其地址(静态局部变量的地址所指向的内存不在栈帧内, 而是与全局变量共享数据区, 因此不会随着代码块的结束而对应地被销毁释放).
综合以上对三种变量的讨论, 我们可以得到一个针对标识符作用域优化后的GuBlock:
2.1.4 关于对自动变量截获的再讨论
既然静态全局变量的处理和全局变量的处理完全一致, 自动变量(局部非静态变量)的处理能不能和静态局部变量一致呢? 能不能如下图所示把整个作用域一分为二, 仅仅用"全局/局部"这一差异作为我们对数据处理方式的分野呢?
好像不行. 原因其实我们在讨论静态局部变量之所以要保存其"地址", 而不保存其"值"里做过描述了: 静态局部变量在它声明的代码块中仍然存活, 因此我们可以通过指针访问到它, 而且在它声明的代码块外, 我们无法通过标识符(变量名)来访问到它了, 只能通过提前存储的地址值来对它进行访问. 而如果存储自动变量的地址, 那么在其所声明的代码块外, 这个自动变量已经被销毁了. 因此我们更希望保存住GuBlock创建时这个自动变量的值. 这也是坂本一树在《Objective-C高级编程 iOS与OS X多线程和内存管理》这本书中, 对自动变量使用了"截获"这个词, 而非大家惯常所使用的"捕获(capture)"这个词的原因——对自动变量的捕获, 其实获取的是这个自动变量在Block创建时的瞬时值, 对它之后的变化已经不再关心了.
2.1.5 小结
对上述讨论的总结如下:
- 对于全局变量, 由于在任何地方都能访问到它, 因此GuBlock不做捕获;
- 对于静态变量:
- 静态全局变量与全局变量完全类似, 我们总可以正确地通过标识符(变量名)访问到它, 因此GuBlock不做捕获;
- 静态局部变量的内存空间始终存在, 但其标识符会因离开其所声明的代码块而变得不可见, 因此为了保证对它的访问, 我们需要提前存储它的地址;
- 对于自动变量: 当离开其声明所在的作用域时, 它的标识符会变得不可见, 同时内存空间会被销毁归还, 因此, 我们需要截获它在GuBlock创建时的值.
2.2 GuBlock的存储
GuBlock在编译器重写后将变成一个结构体, 而结构体变量也是一种变量, 它的作用域和生命周期都应该和其他变量是一致的.
也就是说, 也应该存在全局的GuBlock, 自动的GuBlock, 静态的GuBlock. 对GuBlock做这样的分类, 是为了让GuBlock也能像普通变量一样, 它们的标识符具有各自的可见性, 他们所占据的存储空间能被正确释放.
2.2.1 栈GuBlock - 仿照自动变量的设计
我们最先讨论自动的GuBlock, 因为我们之前使用的就是自动GuBlock. 我们将一个GuBlock的声明放在了函数体内, 其重写后的结构体也因此被放在了函数体内. 自动GuBlock生活在这个函数的栈帧内的, 因此生活在栈上, 称其为栈GuBlock.
2.2.2 全局GuBlock - 仿照全局变量的设计
全局变量的出现, 可以在程序的任何地方访问到, 而且每次访问的都是用一个变量. 因此全局的GuBlock也应该具备这两个性质:
- 能在程序的任何地方被访问到;
- 每次访问的是同一个GuBlock.
为满足第一条, 我们将它存储在数据区.
为满足第二条, 它的具体内容与创建时的状态无关.
根据这两点, 我们使那些声明在代码块内, 但并未截获任何自动变量的GuBlock对象也成为全局GuBlock. 全局GuBlock都放在数据区.
我们使用一个标志位GUBLOCK_IS_GLOBAL来表示当前GuBlock"是否为全局GuBlock". 此时GuBlock重写成的结构体将变为:
当对应GUBLOCK_IS_GLOBAL被置为非零时, 就说明它是一个全局变量.
2.2.3 堆GuBlock - 仿静态局部变量的设计
静态局部变量在其作用域之外, 仍然能通过地址访问到. 因此仿照静态局部变量设计这一类型的GuBlock时, 将它搬到堆上. 这样, 在声明它的代码块之外, 它也不会自动地被销毁, 而是由我们自行管理生命周期.
既然一个对象可以被复制, 那么就应该有对其进行销毁的操作.
因此, 我们不但需要对GuBlock进行copy, 也需要对它做release.
GuBlock的copy
对GuBlock的copy是简单的, 我们只需要抓住三个要点: "我是谁", "我从哪来", "我要到哪去"就可以了.
按顺序来, 首先回答"我是谁". 显然, 被copy的变量是一个GuBlock, 在重写后是一个结构体. 因此, 我们只需要知道这个结构体的大小, 然后使用malloc()在堆上申请对应大小的空间即可.
然后是"我从哪来". 即答, 从栈上来. 会触发copy的GuBlock都应该是来自栈上的. 如果这个GuBlock来自数据区(即, 是一个全局GuBlock), 对全局变量的copy应该什么都不做, 因为我们要保证全局变量有且只有一份. 而对堆上的GuBlock, 联想到智能指针和ARC的设计, 我们为它加上引用计数. 同时, 为了标识的GuBlock所在的具体位置(引用计数只对堆GuBlock有意义), 我们将之前的GUBLOCK_IS_GLOBAL修改为一个枚举, 通过这个枚举值来获得GuBlock的具体生命周期和作用域:
最后是"我要到哪去". 去堆上. 在离开GuBlock所声明的代码块后, 这个栈帧会被销毁释放, 但这个GuBlock由于在堆上, 不在栈帧这个"覆巢之下", 因而能够得以保全.
那么这里, 我们就可以定义出Block_copy()这个基本操作了:
/**
* description: 将传入的GuBlock作一次copy:
* 若对全局GuBlock做copy, 什么也不做;
* 若对栈GuBlock做copy, 则将它复制到堆上去;
* 若对堆上的GuBlock做copy, 则增加它的引用计数.
*
* @param aGuBlock 待复制的GuBlock.
*
* @return 复制后的结果.
*/
void *Block_copy(const void *aGuBlock)
{
struct rewritten_myGuBlock *src = (struct rewritten_myGuBlock *)aGuBlock;
if (src->GUBLOCK_TYPE = GUBLOCK_IS_GLOBAL)
{
// 说明src是一个全局GuBlock
// 什么也不做, 直接返回.
return src;
}
else if (src->GUBLOCK_TYPE = GUBLOCK_IS_ON_HEAP)
{
// 说明src是一个堆上的GuBlock
// 增加引用计数并返回.
++ (src->referenceCount);
return src;
}
else
{
// 此时, rewrittenMyGuBlock是一个仿自动变量的GuBlock, 也就是栈GuBlock
// 创建等大的空间, 并按bit复制.
size_t sizeOfSrc = sizeof(struct rewritten_myGuBlock);
struct rewritten_myGuBlock *dest = (struct rewritten_myGuBlock *)malloc(sizeOfSrc);
if (!dest) return NULL;
memmove(dest, src, sizeOfSrc);
// 此外, 还要将复制后的堆GuBlock的GUBLOCK_TYPE和引用计数进行正确的设置.
dest->GUBLOCK_TYPE = GUBLOCK_IS_ON_HEAP;
dest->referenceCount = 1;
return dest;
}
}
GuBlock的release
堆上的GuBlock变量的引用计数随着copy操作而增加. 由于我们需要在一个堆对象的引用计数归零时将它销毁并释放, 因此我们起码需要一个减少其引用计数的操作. 也就是release操作.
显然, 对于全局GuBlock而言, 它始终只存在一份, 且生命周期不归我们管理; 对于栈GuBlock而言, 它的设计是仿照自动变量的, 也就是说, 在其声明所在的代码块所对应的栈帧被销毁时, 它会被自行销毁. 因此我们能够管到的, 其实只有堆GuBlock.
有了这样的分类讨论, 我们其实就获得了Block_release()的代码:
/**
* description: 将传入的GuBlock作一次release:
* 若对全局GuBlock做release, 什么也不做;
* 若对栈GuBlock做release, 什么也不做;
* 若对堆上的GuBlock做release, 则减少它的引用计数.
*
* @param aGuBlock 待release的GuBlock.
*/
void Block_release(const void *aGuBlock)
{
struct rewritten_myGuBlock *aGuBlockToRelease = (struct rewritten_myGuBlock *)aGuBlock;
if (aGuBlockToRelease->GUBLOCK_TYPE = GUBLOCK_IS_GLOBAL)
{
// 说明aGuBlockToRelease是一个全局GuBlock
// 什么也不做, 直接返回.
return;
}
else if (aGuBlockToRelease->GUBLOCK_TYPE = GUBLOCK_IS_ON_STACK)
{
// 说明src是一个堆上的GuBlock
// 什么也不做, 直接返回.
return;
}
else
{
// 说明aGuBlockToRelease是一个堆上的GuBlock
// 减少引用计数.
-- (aGuBlockToRelease->referenceCount);
if (aGuBlockToRelease->referenceCount == 0)
{
// 如果这次减少引用计数后, 引用计数被归零, 则释放其内存空间.
free(aGuBlockToRelease);
}
return;
}
}
有了copy和release, 对堆GuBlock的内存管理的设计就完成了.
2.2.4 小结
经过本节的分类讨论, 我们将GuBlock分为了全局GuBlock, 栈GuBlock和堆GuBlock.
全局的GuBlock
在上述讨论后, 我们得到了如下示的分类结果.
根据我们所期待的不同的销毁时机, 我们都能正确地定义GuBlock的存储位置和copy/release操作了:
- 全局GuBlock:
- 由于全局GuBlock仅需要一份, 因此对它的copy什么也不做;
- 它的销毁时机由程序控制, 对它的release操作应当为一个
no-op;
- 堆GuBlock:
- 由于堆GuBlock的生命周期需要由我们自己管理, 因此对它的copy和release都会影响其引用计数, 且当引用计数归零时, 其在堆上的空间将被释放;
- 栈GuBlock:
- 由于对栈GuBlock的copy的目的是创建一个新的, 位于堆上的GuBlock, 因此对栈GuBlock的copy操作会将其拷贝到堆上, 同时将这个堆上的GuBlock引用计数初始化为1;
- 它的销毁时机由程序控制, 对它的release操作应当为一个
no-op.
2.3 GuBlock对非原生类型的捕获
经过上一节的分类讨论和处理, 我们对所有的原生类型——普通的标量, 结构体, 联合体, 函数指针等——都可以正确地进行引入了. 对它们的捕获方法是简单地值拷贝. 但如果需要拷贝的是更复杂的, 需要深拷贝的对象, 比如C++的栈对象, Objective-C中的对象和GuBlock对象自己, 使用浅拷贝就不能完成任务了.
因此, 我们需要对它们进行分类讨论.
2.3.1 GuBlock对Objective-C对象的捕获
对Objective-C对象的捕获其实比较简单, 原因是Objective-C对象一定生活在堆上, 由一根指针指向它, 我们通过这跟指针来访问这个Objective-C对象. 因此, 我们捕获到的Objective-C对象, 其实就是一根与GuBlock同作用域的普通指针而已.
因此, 我们并不需要因为捕获的变量从原生类型变为了Objective-C对象指针而对GuBlock的结构做什么改变, 只需要将copy和release做出一点修改, 从浅拷贝变为深拷贝.
对GuBlock的copy进行调整:
对于之前的三种存储方式的GuBlock, 我们可以获得跟之前完全一致的定义:
- 全局GuBlock不会捕获任何作为自动变量的Objective-C对象指针
- 栈GuBlock会捕获作为自动变量的Objective-C对象指针
- 堆GuBlock由栈GuBlock的copy得到
因此, 我们只需要"从栈GuBlock的copy时创建堆GuBlock"这个过程能正确完成就可以了. 那么, 我们只需要在Block_copy()对栈GuBlock的操作里添加上对Objective-C对象指针的复制即可: 在堆GuBlock内创建一根新指针, 指向栈GuBlock的Objective-C对象指针所指向的位置.
在上一节Block_copy()的代码中, 对栈GuBlock的copy逻辑中加上对所捕获的id类型对象的处理:
/**
* description: 将传入的GuBlock作一次copy:
* 若对全局GuBlock做copy, 什么也不做;
* 若对栈GuBlock做copy, 则将它复制到堆上去;
* 若对堆上的GuBlock做copy, 则增加它的引用计数.
*
* @param aGuBlock 待复制的GuBlock.
*
* @return 复制后的结果.
*/
void *Block_copy(const void *aGuBlock)
{
struct rewritten_myGuBlock *src = (struct rewritten_myGuBlock *)aGuBlock;
if (src->GUBLOCK_TYPE = GUBLOCK_IS_GLOBAL)
{
// 说明src是一个全局GuBlock
// 什么也不做, 直接返回.
return src;
}
else if (src->GUBLOCK_TYPE = GUBLOCK_IS_ON_HEAP)
{
// 说明src是一个堆上的GuBlock
// 增加引用计数并返回.
++ (src->referenceCount);
return src;
}
else
{
// 此时, rewrittenMyGuBlock是一个仿自动变量的GuBlock, 也就是栈GuBlock
// 创建等大的空间, 并按bit复制.
size_t sizeOfSrc = sizeof(struct rewritten_myGuBlock);
struct rewritten_myGuBlock *dest = (struct rewritten_myGuBlock *)malloc(sizeOfSrc);
if (!dest) return NULL;
memmove(dest, src, sizeOfSrc);
// 此外, 还要将复制后的堆GuBlock的GUBLOCK_TYPE和引用计数进行正确的设置.
dest->GUBLOCK_TYPE = GUBLOCK_IS_ON_HEAP;
dest->referenceCount = 1;
/*********************************代码新增部分开始*********************************/
GuBlock_copy_helper_for_objc_object(dest, src);
/*********************************代码新增部分结束*********************************/
return dest;
}
}
/*********************************代码新增部分开始*********************************/
void GuBlock_copy_helper_for_objc_object(void *dest, const void *src);
{
struct rewritten_myGuBlock *destGuBlock = (struct rewritten_myGuBlock *)dest;
struct rewritten_myGuBlock *srcGuBlock = (struct rewritten_myGuBlock *)src;
destGuBlock->capturedObj = srcGuBlock->capturedObj;
}
/*********************************代码新增部分结束*********************************/
我们用显眼的注释包裹住了新增部分的代码, 显然, 在ARC的帮助下, 只需要把一根新指针指向NSObject对象所在的区域, 其引用计数会自动加1. 因此, 我们也就完成了对堆上的NSObject对象的copy(实际上是retain).
而对本就处在堆上的GuBlock的copy, 则只需要增加其引用计数即可, 与原本的copy操作完全相同:
既然我们能对捕获了Objective-C对象的GuBlock进行copy, 就也应该提供release行为.
对GuBlock的release进行调整:
对于捕获了Objective-C对象的GuBlock, 从上一小节我们获得了三种存储类型的GuBlock的定义, 即:
- 全局GuBlock不捕获任何自动类型的Objective-C对象指针
- 栈GuBlock捕获Objective-C对象的自动类型指针, 栈帧销毁时栈GuBlock和自动变量一样被销毁, 被捕获的Objective-C对象由于失去了一个持有者而减少其引用计数——甚至可能因为引用计数归零而被销毁和归还
- 堆GuBlock不会因为作用域的变化而被销毁, 而需要手动地通过
Block_release()方法减少其引用计数, 乃至在合适的时候将其销毁. 如果这个堆GuBlock被销毁了, 相应地它所捕获的Objective-C对象也要减少引用计数
因此, 实际上我们只需要对堆GuBlock做Block_release()了.
在2.2.3.2节中Block_release()的代码中, 对堆GuBlock的release逻辑中加上对所捕获的id类型对象的处理:
/**
* description: 将传入的GuBlock作一次release:
* 若对全局GuBlock做release, 什么也不做;
* 若对栈GuBlock做release, 什么也不做;
* 若对堆上的GuBlock做release, 则减少它的引用计数.
*
* @param aGuBlock 待release的GuBlock.
*/
void Block_release(const void *aGuBlock)
{
struct rewritten_myGuBlock *aGuBlockToRelease = (struct rewritten_myGuBlock *)aGuBlock;
if (aGuBlockToRelease->GUBLOCK_TYPE = GUBLOCK_IS_GLOBAL)
{
// 说明aGuBlockToRelease是一个全局GuBlock
// 什么也不做, 直接返回.
return;
}
else if (aGuBlockToRelease->GUBLOCK_TYPE = GUBLOCK_IS_ON_STACK)
{
// 说明src是一个堆上的GuBlock
// 什么也不做, 直接返回.
return;
}
else
{
// 说明aGuBlockToRelease是一个堆上的GuBlock
// 减少引用计数.
-- (aGuBlockToRelease->referenceCount);
if (aGuBlockToRelease->referenceCount == 0)
{
// 如果这次减少引用计数后, 引用计数被归零, 则释放其内存空间.
/*********************************代码新增部分开始*********************************/
// 在此之前, 先release其中所包含的对象, 以防止这种情况的发生:
// 即, 当前GuBlock是其所捕获的Objective-C对象的唯一一个持有者.
// 在这种情况下, 如果不减少所捕获的Objective-C对象的引用计数, 会发生内存泄漏.
GuBlock_release_helper_for_objc_object(aGuBlockToRelease);
/*********************************代码新增部分结束*********************************/
free(aGuBlockToRelease);
}
return;
}
}
/*********************************代码新增部分开始*********************************/
void GuBlock_release_helper_for_objc_object(void *aGuBlock);
{
// 什么都不做, ARC会在这块空间被释放后自动地减少所捕获的Objective-C对象的引用计数.
/*
struct rewritten_myGuBlock *aGuBlockToRelease = (struct rewritten_myGuBlock *)aGuBlock;
aGuBlockToRelease->capturedObj = NULL;
*/
}
/*********************************代码新增部分结束*********************************/
我们用显眼的注释包裹住了新增部分的代码, 显然, 在ARC的帮助下, 只需要将指针指向NSObject对象所在的区域, 其引用计数会自动减1. 因此, 我们也就完成了对堆上的NSObject对象的release.
2.3.2 GuBlock对C++栈对象的捕获(如果对C++不了解或不需要这部分知识, 可以跳过, 不影响后续阅读)
类似地, C++地对象我们也可以进行捕获. 而且和2.3.1节的讨论一样, 我们只关心自动变量部分(因为对它们的处理需要做额外修改), 而对全局变量和静态变量, 它们的处理没有变化, 之前的推理完全合用, 因此我们照旧使用它们.
与Objective-C对象只能存在于堆上不同, C++对象可能存在于栈上. 对堆上的情况我们不做讨论, 因为在堆上的对象的生命周期(如果不使用智能指针的话)完全由程序的编写者控制, 因此无需考虑对它的捕获影响其生命周期——即便是GuBlock逃离出了它的生命周期, 它对堆上的C++对象的使用也是无法保证始终有效的, 而只能依赖于程序编写者对C++对象生命周期正确而合理的控制.
但栈上的C++对象则不同. 捕获过程中, 就会发生拷贝, 在GuBlock中生成一个值与被捕获的C++栈对象完全相同的C++常量对象, 因此需要使用到拷贝构造函数, 而且是常量拷贝构造函数.
同时, 如果一个GuBlock捕获了自动变量, 且这个GuBlock需要逃离出这个栈帧, 那么它就需要对其中所捕获的自动变量分别做一份拷贝. 对原生类型的变量直接做值拷贝(浅拷贝)就足够了, 但对于栈上C++对象, 我们需要在堆内开辟的空间上拷贝这个类型的变量. 也需要为它生成对应的copy/release helper, 它们的作用与2.3.1中的GuBlock_copy_helper_for_objc_object和GuBlock_release_helper_for_objc_object完全一致.
2.3.3 GuBlock对GuBlock类型变量的捕获
在前面的讨论中我们发现, 对非原生类型的变量需要做特殊处理的原因仅仅是浅拷贝不能满足GuBlock生命周期的需求了. 因此我们为Objective-C对象增加了引用计数的处理, 为C++对象增加了(常量)拷贝构造函数与析构函数的要求.
因此类似地, 当捕获一个GuBlock类型的自动变量时, 我们也额外地为它进行深拷贝和销毁的支持:
对GuBlock的copy进行调整:
因为对GuBlock的深拷贝有现成的方法, 就是Block_copy()本身. 因此, 只需要在复制过程中, 对GuBlock中的GuBlock再调用一次Block_copy()的方法即可.
因此, 对Block_copy()做如下修改, 在对栈GuBlock的复制中嵌套地添加上对其中的GuBlock对象的Block_copy()调用:
/**
* description: 将传入的GuBlock作一次copy:
* 若对全局GuBlock做copy, 什么也不做;
* 若对栈GuBlock做copy, 则将它复制到堆上去;
* 若对堆上的GuBlock做copy, 则增加它的引用计数.
*
* @param aGuBlock 待复制的GuBlock.
*
* @return 复制后的结果.
*/
void *Block_copy(const void *aGuBlock)
{
struct rewritten_myGuBlock *src = (struct rewritten_myGuBlock *)aGuBlock;
if (src->GUBLOCK_TYPE = GUBLOCK_IS_GLOBAL)
{
// 说明src是一个全局GuBlock
// 什么也不做, 直接返回.
return src;
}
else if (src->GUBLOCK_TYPE = GUBLOCK_IS_ON_HEAP)
{
// 说明src是一个堆上的GuBlock
// 增加引用计数并返回.
++ (src->referenceCount);
return src;
}
else
{
// 此时, rewrittenMyGuBlock是一个仿自动变量的GuBlock, 也就是栈GuBlock
// 创建等大的空间, 并按bit复制.
size_t sizeOfSrc = sizeof(struct rewritten_myGuBlock);
struct rewritten_myGuBlock *dest = (struct rewritten_myGuBlock *)malloc(sizeOfSrc);
if (!dest) return NULL;
memmove(dest, src, sizeOfSrc);
// 此外, 还要将复制后的堆GuBlock的GUBLOCK_TYPE和引用计数进行正确的设置.
dest->GUBLOCK_TYPE = GUBLOCK_IS_ON_HEAP;
dest->referenceCount = 1;
GuBlock_copy_helper_for_objc_object(dest, src);
/*********************************代码新增部分开始*********************************/
GuBlock_copy_helper_for_nested_GuBlock(dest, src)
/*********************************代码新增部分结束*********************************/
return dest;
}
}
void GuBlock_copy_helper_for_objc_object(void *dest, const void *src);
{
struct rewritten_myGuBlock *destGuBlock = (struct rewritten_myGuBlock *)dest;
struct rewritten_myGuBlock *srcGuBlock = (struct rewritten_myGuBlock *)src;
destGuBlock->capturedObj = srcGuBlock->capturedObj;
}
/*********************************代码新增部分开始*********************************/
void GuBlock_copy_helper_for_nested_GuBlock(void *dest, const void *src);
{
struct rewritten_myGuBlock *destGuBlock = (struct rewritten_myGuBlock *)dest;
struct rewritten_myGuBlock *srcGuBlock = (struct rewritten_myGuBlock *)src;
destGuBlock->capturedGuBlock = Block_copy(srcGuBlock->capturedGuBlock);
}
/*********************************代码新增部分结束*********************************/
我们用显眼的注释包裹住了新增部分的代码, 对一个GuBlock所捕获的GuBlock也调用一次Block_copy(), 来完成深拷贝.
对GuBlock的release进行调整:
在对C++对象的处理中, 我们通过copy将栈上被捕获的C++对象拷贝到了堆上, 而对堆上的C++对象我们则需要适时释放. 类似地, 我们对被捕获的栈上的GuBlock对象也要有release的动作, 在捕获它的GuBlock将被销毁时执行.
时刻牢记copy和release的对应关系.
/**
* description: 将传入的GuBlock作一次release:
* 若对全局GuBlock做release, 什么也不做;
* 若对栈GuBlock做release, 什么也不做;
* 若对堆上的GuBlock做release, 则减少它的引用计数.
*
* @param aGuBlock 待release的GuBlock.
*/
void Block_release(const void *aGuBlock)
{
struct rewritten_myGuBlock *aGuBlockToRelease = (struct rewritten_myGuBlock *)aGuBlock;
if (aGuBlockToRelease->GUBLOCK_TYPE = GUBLOCK_IS_GLOBAL)
{
// 说明aGuBlockToRelease是一个全局GuBlock
// 什么也不做, 直接返回.
return;
}
else if (aGuBlockToRelease->GUBLOCK_TYPE = GUBLOCK_IS_ON_STACK)
{
// 说明src是一个堆上的GuBlock
// 什么也不做, 直接返回.
return;
}
else
{
// 说明aGuBlockToRelease是一个堆上的GuBlock
// 减少引用计数.
-- (aGuBlockToRelease->referenceCount);
if (aGuBlockToRelease->referenceCount == 0)
{
// 如果这次减少引用计数后, 引用计数被归零, 则释放其内存空间.
// 在此之前, 先release其中所包含的对象, 以防止这种情况的发生:
// 即, 当前GuBlock是其所捕获的Objective-C对象的唯一一个持有者.
// 在这种情况下, 如果不减少所捕获的Objective-C对象的引用计数, 会发生内存泄漏.
GuBlock_release_helper_for_objc_object(aGuBlockToRelease);
/*********************************代码新增部分开始*********************************/
GuBlock_release_helper_for_nested_GuBlock(aGuBlockToRelease);
/*********************************代码新增部分结束*********************************/
free(aGuBlockToRelease);
}
return;
}
}
void GuBlock_release_helper_for_objc_object(void *aGuBlock);
{
// 什么都不做, ARC会在这块空间被释放后自动地减少所捕获的Objective-C对象的引用计数.
/*
struct rewritten_myGuBlock *aGuBlockToRelease = (struct rewritten_myGuBlock *)aGuBlock;
aGuBlockToRelease->capturedObj = NULL;
*/
}
/*********************************代码新增部分开始*********************************/
void GuBlock_release_helper_for_nested_GuBlock(void *aGuBlock);
{
struct rewritten_myGuBlock *aGuBlockToRelease = (struct rewritten_myGuBlock *)aGuBlock;
Block_release(aGuBlockToRelease->capturedGuBlock);
}
/*********************************代码新增部分结束*********************************/
我们用显眼的注释包裹住了新增部分的代码, 对一个GuBlock所捕获的GuBlock也调用一次Block_release(), 来完成release操作.
2.3.4 小结
本节我们将GuBlock所能捕获的类型, 由原生类型(普通的标量, 结构体, 联合体, 函数指针等)扩展到了Objective-C对象, C++栈对象. 对这些在被复制时不是简单地浅拷贝, 而涉及到深拷贝的变量做了进一步地做处理.
在覆盖到所有类型的变量之后, 我们进一步扩展了GuBlock所能捕获的变量类型——GuBlock自己. 一个GuBlock可以捕获另一个GuBlock变量, 且这种捕获可以简单地通过嵌套Block_copy()与Block_release()来支持.
经过这部分的完善, GuBlock已经能够捕获Objective-C中可能出现的所有类型了.
2.4 对GuBlock外部变量的修改
这样设计的GuBlock能捕获到所需的所有类型的自动变量, 但尚不能对其中使用到的所有类型的变量的值进行写操作.
和对静态局部变量的处理类似, 我们将需要写的自动变量搬移到堆上去, 并通过间接访问, 使多个GuBlock(包括堆上的和栈上的)都能访问到同一个变量.
在栈GuBlock和被捕获的自动变量中加一个中间层, 要求栈GuBlock通过这个中间层来访问实际的自动变量. 在一个栈GuBlock被copy到堆上的时候, 这次copy操作应当负责修改这个中间层的指向, 以保证栈上的这个由所有栈GuBlock共享的中间层与堆上新复制出的中间层的指向相同. 这样就能保证捕获了同一个可读写的变量的GuBlock访问的都是同一个自动变量的值了.
此时, 我们就获得了一个进一步优化的GuBlock模型.
在这个模型中, 包含了初心——所要执行的代码块的函数指针, 包含了GuBlock类型, 包含了引用计数(仅对堆GuBlock有意义), 包含了所截获的只读自动变量的值, 包含了指向需要读写的自动变量的指针的地址(forwarding), 包含了所捕获的静态局部变量的地址, 且可以正常访问到全局变量.
此时, 为GuBlock的Block_copy()和Block_dispose()也发生了一点变化: 因为在copy完成后要做一次栈GuBlock所指向的forwarding指向的改变, 使这个栈上的forwarding也指向堆上的变量值(如图2.4.1.3-1所示).
我们需要在GuBlock中判断其所捕获的变量是不是一个可以写的变量. 但我们之前的设计只使这个变量能通过一根指针进行访问, 这个自动变量在"重写后的GuBlock"结构体并不能与其他被捕获的只读自动变量区分开来. 因此我们需要做进一步的优化——将可读写的自动变量用一个结构体来表示, 并在其中添加上类型描述.
将forwarding指针打包放到一个结构体中去, 并添加一个枚举, 表示这个自动变量的类型.
对这种可读写的变量, 我们用__block修饰.
这样, 当这个被__block修饰的BYREF(意指"by reference")结构体中说明其类型为Objective-C对象或GuBlock时它需要做额外的步骤(深拷贝).
为Objective-C对象类型和GuBlock类型的可读写变量分别添加上自己的深拷贝步骤.
/**
* description: 将传入的GuBlock作一次copy:
* 若对全局GuBlock做copy, 什么也不做;
* 若对栈GuBlock做copy, 则将它复制到堆上去;
* 若对堆上的GuBlock做copy, 则增加它的引用计数.
*
* @param aGuBlock 待复制的GuBlock.
*
* @return 复制后的结果.
*/
void *Block_copy(const void *aGuBlock)
{
struct rewritten_myGuBlock *src = (struct rewritten_myGuBlock *)aGuBlock;
if (src->GUBLOCK_TYPE = GUBLOCK_IS_GLOBAL)
{
// 说明src是一个全局GuBlock
// 什么也不做, 直接返回.
return src;
}
else if (src->GUBLOCK_TYPE = GUBLOCK_IS_ON_HEAP)
{
// 说明src是一个堆上的GuBlock
// 增加引用计数并返回.
++ (src->referenceCount);
return src;
}
else
{
// 此时, rewrittenMyGuBlock是一个仿自动变量的GuBlock, 也就是栈GuBlock
// 创建等大的空间, 并按bit复制.
size_t sizeOfSrc = sizeof(struct rewritten_myGuBlock);
struct rewritten_myGuBlock *dest = (struct rewritten_myGuBlock *)malloc(sizeOfSrc);
if (!dest) return NULL;
memmove(dest, src, sizeOfSrc);
// 此外, 还要将复制后的堆GuBlock的GUBLOCK_TYPE和引用计数进行正确的设置.
dest->GUBLOCK_TYPE = GUBLOCK_IS_ON_HEAP;
dest->referenceCount = 1;
GuBlock_copy_helper_for_objc_object(dest, src);
GuBlock_copy_helper_for_nested_GuBlock(dest, src);
/*********************************代码新增部分开始*********************************/
GuBlock_copy_helper_for_auto_variable_byref(dest, src);
/*********************************代码新增部分结束*********************************/
return dest;
}
}
void GuBlock_copy_helper_for_objc_object(void *dest, const void *src);
{
struct rewritten_myGuBlock *destGuBlock = (struct rewritten_myGuBlock *)dest;
struct rewritten_myGuBlock *srcGuBlock = (struct rewritten_myGuBlock *)src;
destGuBlock->capturedObj = srcGuBlock->capturedObj;
}
void GuBlock_copy_helper_for_nested_GuBlock(void *dest, const void *src);
{
struct rewritten_myGuBlock *destGuBlock = (struct rewritten_myGuBlock *)dest;
struct rewritten_myGuBlock *srcGuBlock = (struct rewritten_myGuBlock *)src;
destGuBlock->capturedGuBlock = Block_copy(srcGuBlock->capturedGuBlock);
}
/*********************************代码新增部分开始*********************************/
void GuBlock_copy_helper_for_auto_variable_byref(void *dest, const void *src)
{
struct rewritten_myGuBlock *destGuBlock = (struct rewritten_myGuBlock *)dest; // 堆上
struct rewritten_myGuBlock *srcGuBlock = (struct rewritten_myGuBlock *)src; // 栈上
switch (srcGuBlock->capturedAutoVariableByref->BYREF_TYPE)
{
case BYREF_IS_OBJC_OBJECT:
case BYREF_IS_GUBLOCK: // 两种情况在这里其实一致, 只是与原生类型有所不同
// 需要手动申请空间
// 这里, GuBlock其实没有size属性, 应当手动计算, 但为代码简介起见, 假定能通过某种方式获得GuBlock的size
destGuBlock->capturedAutoVarByref = (struct BYREF *)malloc(srcGuBlock->size);
// 堆上的byref->forwading应当指向自己
destGuBlock->capturedAutoVarByref->forwarding = destGuBlock->capturedAutoVarByref;
// 栈上的byref->forwading应当指向堆上的BYREF
destGuBlock->capturedAutoVarByref->forwarding = destGuBlock->capturedAutoVarByref;
break;
default:
break;
}
}
/*********************************代码新增部分结束*********************************/
这里借助了ARC完成了对对象的retain.
完成了对copy的补充, 我们需要再补充release.
/**
* description: 将传入的GuBlock作一次release:
* 若对全局GuBlock做release, 什么也不做;
* 若对栈GuBlock做release, 什么也不做;
* 若对堆上的GuBlock做release, 则减少它的引用计数.
*
* @param aGuBlock 待release的GuBlock.
*/
void Block_release(const void *aGuBlock)
{
struct rewritten_myGuBlock *aGuBlockToRelease = (struct rewritten_myGuBlock *)aGuBlock;
if (aGuBlockToRelease->GUBLOCK_TYPE = GUBLOCK_IS_GLOBAL)
{
// 说明aGuBlockToRelease是一个全局GuBlock
// 什么也不做, 直接返回.
return;
}
else if (aGuBlockToRelease->GUBLOCK_TYPE = GUBLOCK_IS_ON_STACK)
{
// 说明src是一个堆上的GuBlock
// 什么也不做, 直接返回.
return;
}
else
{
// 说明aGuBlockToRelease是一个堆上的GuBlock
// 减少引用计数.
-- (aGuBlockToRelease->referenceCount);
if (aGuBlockToRelease->referenceCount == 0)
{
// 如果这次减少引用计数后, 引用计数被归零, 则释放其内存空间.
// 在此之前, 先release其中所包含的对象, 以防止这种情况的发生:
// 即, 当前GuBlock是其所捕获的Objective-C对象的唯一一个持有者.
// 在这种情况下, 如果不减少所捕获的Objective-C对象的引用计数, 会发生内存泄漏.
GuBlock_release_helper_for_objc_object(aGuBlockToRelease);
GuBlock_release_helper_for_nested_GuBlock(aGuBlockToRelease);
/*********************************代码新增部分开始*********************************/
GuBlock_release_helper_for_auto_variable_byref(aGuBlockToRelease);
/*********************************代码新增部分结束*********************************/
free(aGuBlockToRelease);
}
return;
}
}
void GuBlock_release_helper_for_objc_object(void *aGuBlock);
{
// 什么都不做, ARC会在这块空间被释放后自动地减少所捕获的Objective-C对象的引用计数.
/*
struct rewritten_myGuBlock *aGuBlockToRelease = (struct rewritten_myGuBlock *)aGuBlock;
aGuBlockToRelease->capturedObj = NULL;
*/
}
void GuBlock_release_helper_for_nested_GuBlock(void *aGuBlock);
{
struct rewritten_myGuBlock *aGuBlockToRelease = (struct rewritten_myGuBlock *)aGuBlock;
Block_release(aGuBlockToRelease->capturedGuBlock);
}
/*********************************代码新增部分开始*********************************/
void GuBlock_copy_helper_for_auto_variable_byref(void *aGuBlock)
{
struct rewritten_myGuBlock *aGuBlockToRelease = (struct rewritten_myGuBlock *)aGuBlock;
// 保证aGuBlockToRelease是堆上的BYREF——这样对它的release才有意义
aGuBlockToRelease = aGuBlockToRelease->forwarding; // 若aGuBlockToRelease指向栈上的BYREF, 使其指向堆上
// 若aGuBlockToRelease指向栈上的BYREF, 其指向不变
switch (aGuBlockToRelease->BYREF_TYPE)
{
case BYREF_IS_OBJC_OBJECT:
case BYREF_IS_GUBLOCK: // 两种情况在这里其实一致, 只是与原生类型有所不同
// 需要手动释放申请到的内存
free(aGuBlockToRelease);
break;
default:
break;
}
}
/*********************************代码新增部分结束*********************************/
2.5 总结
到这里, 我们就已经获得了一个行为上与Apple所提供的Block一致, 且原理与Block基本相同的GuBlock了.
对整个从函数指针到GuBlock的设计流程做一个总结吧.
我们从函数指针出发, 希望编译器在编译前帮我们转写一次代码, 将Objective-C代码变为C++或C的代码, 因此, 我们为它定义了如下规则:
- 将一个以脱字符(
^)作为标识符的GuBlock重写为一个结构体, 其中包含了其所对应的代码块的函数指针, 类型提示, 和捕获的变量. - 根据变量类型, 对"捕获"这一行为进行优化.
- 对copy/release行为进行实现, 并根据变量类型进行深浅拷贝的判断.
此时获得的GuBlock与Block在实现上的差别主要在于: Block将各个标志位(如是否是全局Block, 是否需要free等)和引用计数统一放到了flags中去管理, 使用位运算对它们进行操作. 同时, 且使用了一个desc(代指"description")去存储size等. 这些因素对具体的原理其实并无影响.
到这里, 我们就获得了一个行为上与Block一致, 原理与Block基本相同的语法成分GuBlock了.
3 结语
写这篇文章的动机是这样的: 在自己学习Block的源码时, 小顾其实遇到了很多困惑不解, 这些不解常常不是来自于"这段代码是什么意思, 它做了什么", 而更多地是来自于"它为什么要这样做", "这样做有什么好处". 因此, 小顾查阅了一些资料, 结合代码, 想尽量地还原设计者的意图. 幸运的是, 根据这些资料和代码, 我们的确获得了一个结构完整且能够自洽的设计思路, 在满足使用的基础上, 比较全面地解决了一些在语言层面上导致的使用中可能出现的问题.
小顾在写这篇文章的时候也是一个不断发掘自己思维盲点, 同时惊异于设计者思路的完备与周全的过程. 希望本文能给读者带来一个新的对Block的观察角度.
附录 参考文献
[1]: "The Function Pointer Tutorials".
[2]: Apple's Extension to C. http://www.open-std.org/JTC1/SC22/WG14/www/documents - N1370.
[3]: Blocks Proposal .www.open-std.org/JTC1/SC22/W… - N1451.
hi, 我是快手电商的小顾~
快手电商无线技术团队正在招贤纳士🎉🎉🎉! 我们是公司的核心业务线, 这里云集了各路高手, 也充满了机会与挑战. 伴随着业务的高速发展, 团队也在快速扩张. 欢迎各位高手加入我们, 一起创造世界级的电商产品~
热招岗位: Android/iOS 高级开发, Android/iOS 专家, Java 架构师, 产品经理(电商背景), 测试开发... 大量 HC 等你来呦~
内部推荐请发简历至 >>>我们的邮箱: hr.ec@kuaishou.com <<<, 备注我的花名成功率更高哦~ 😘