1 布景 – 从函数指针到block
1.1 Block的引入
block是Objective-C中常用的语法成分, 典型的运用场合是传递行为(如回调).
但实践上, C言语中现已有能够传递行为的语法成分了, 便是函数指针. 它指向的是内存中的可履行代码. 对函数指针的解引证将会引发对应的函数, 对这个函数的调用和一般的函数调用相同能够传入参数. 相较于”直接地”从确认的函数名引导到可履行代码, 函数指针经过从一个变量(也便是函数指针的值)”直接地”来调用函数, 这种直接的调用办法使它能够在运行时依据函数指针的值动态地挑选对应的函数来履行.
函数指针的优势在于能够将一组对数据的操作(behavior, or rather, operations on data)也笼统成了数据(data), 这比较贴合面向目标的规划理念, 但它离真实的封装还有一定的距离——因为函数指针并不能真实地包括数据.
函数指针对外部数据的包括是经过参数列表来完结的, 这种办法需求程序的编写者自行保护, 灵活性差的一起, 保护本钱和复用难度也高.
所以, 咱们就想能不能发明这样一种语法, 它和函数指针相同包括了一组可履行代码, 但能够主动地捕获所用到的变量.
在Objective-C中, Block供给了这种才能.
1.2 Block的简略介绍
Block的运用办法大致如下:
图1.2-1 Block的一个简略声明
在这儿, 咱们能够看到, Block和函数指针的声明很类似, 只是将星号(*
)改成了脱字符(). 实践上, Apple对C做Block这一拓宽时, 也提到了它们的声明只要这一区别[2].
实践上, Block语法为C言语引入了4个新的概念[3]:
- 一种新的复合类型, 也便是Block引证;
- Block的字面量;
- 一种新的存储类型及其所对应的润饰符
__block
; - 言语原语
Block_copy
和Block_release
.
本文不计划直接开端评论这四个新的概念与成分, 而是从规划者的视点, 从”程序开发者需求什么”入手, 规划一个类似于Block的语法成分——GuBlock, 在这个进程中带领咱们更好地了解Block”做了什么”, “为什么要这样做”和”这样做的优点体现在哪里”这三个方面的问题.
1.3 GuBlock = 函数指针 + 变量捕获 + 生命周期办理
咱们依据函数指针”传递一段可履行代码”的才能, 在它的基础上增加在其声明地点的效果域中主动捕获其所运用的变量的功用.
这样就有:
GuBlock=函数指针+变量捕获.\text{GuBlock} = \text{函数指针} + \text{变量捕获}.
但实践上, 如[1]中所说: 函数指针比一般指针更不简略犯错的原因是——它不需求程序的编写者手动办理内存——因为它没有可供办理的内存, 而一旦GuBlock为函数指针增加上了归于它的变量, 那么就需求考虑生命周期的问题了. 因而这个”等式”应该被修正为如下增加过”副效果”的姿态:
GuBlock=函数指针+变量捕获+(副效果)生命周期办理.\text{GuBlock} = \text{函数指针} + \text{变量捕获} + \text{(副效果)生命周期办理}.
这是咱们规划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);
}
也便是:
图2-1 GuBlock规划稿 1.0
因而, 咱们现在取得了一个简易的复合类型——GuBlock. 编译器将它重写成一个结构体, 这个结构体中包括着它对应的代码块地点的函数指针, 和该代码块中所捕获的数据.
此刻, Block的规划稿如下示:
图2-2 GuBlock规划稿 2.0
2.1 GuBlock针对变量效果域的优化
下图是GuBlock的初步规划, 对捕获到的不同类型的变量都以相同的办法打包.
图2-3 GuBlock此刻针对不同效果域的变量的行为
这是一种很浪费的办法, 因为并不是每一个被捕获的变量都需求用这么杂乱的办法存储.
2.1.1 主动变量的处理
主动变量, 或称部分变量, 它们的效果域限制在它们所处的代码块内, 因而具有块效果域. GuBlock中的函数指针所指向的代码块明显和部分变量的效果域不同:
图2.1.1-1 函数体与主动变量效果域的联系
因而, 咱们需求在重写后的”GuBlock”这一结构体中存储一份部分变量的仿制.
2.1.2 大局变量的处理
大局变量是文件级别的效果域, 因而, 在GuBlock的函数指针所指向的代码块中, 实践上是天然地能够拜访到大局变量的, 因而并不需求专门地存储在”重写后的GuBlock”这一结构体中.
静态大局变量也是一种大局变量, 因而和大局变量的处理彻底共同.
2.1.3 静态部分变量的处理
最后是静态部分变量.
静态部分变量的标识符仅在其所声明的代码块中可见, 但即便该代码块完毕, 它的内存空间也不会被毁掉偿还. 因为在其所声明的代码块完毕后, 它依然能够存活, 因而在静态部分变量所声明的块外它依然存在被继续运用的或许. 为了维持静态部分变量的这一特性, 咱们不能只是运用它的值, 而是需求记录其地址(静态部分变量的地址所指向的内存不在栈帧内, 而是与大局变量同享数据区, 因而不会跟着代码块的完毕而对应地被毁掉开释).
综合以上对三种变量的评论, 咱们能够得到一个针对标识符效果域优化后的GuBlock:
图2.1.3-1 GuBlock针对不同效果域的变量优化后的行为
2.1.4 关于对主动变量截获的再评论
已然静态大局变量的处理和大局变量的处理彻底共同, 主动变量(部分非静态变量)的处理能不能和静态部分变量共同呢? 能不能如下图所示把整个效果域一分为二, 只是用”大局/部分”这一差异作为咱们对数据处理办法的分野呢?
图2.4.1-1 将”大局/部分”作为处理数据的分野
如同不行. 原因其实咱们在评论静态部分变量之所以要保存其”地址”, 而不保存其”值”里做过描绘了: 静态部分变量在它声明的代码块中依然存活, 因而咱们能够经过指针拜访到它, 并且在它声明的代码块外, 咱们无法经过标识符(变量名)来拜访到它了, 只能经过提早存储的地址值来对它进行拜访. 而假如存储主动变量的地址, 那么在其所声明的代码块外, 这个主动变量现已被毁掉了. 因而咱们更期望保存住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重写成的结构体将变为:
图2.2.2-1 GuBlock在增加GUBLOCK_IS_GLOBAL后针对不同效果域的变量的行为
当对应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的具体生命周期和效果域:
图2.2.3.1-1 Block在增加GUBLOCK_TYPE后针对不同效果域的变量的行为
最后是”我要到哪去”. 去堆上. 在脱离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
在上述评论后, 咱们得到了如下示的分类成果.
图2.2.4-1 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目标指针所指向的方位.
图2.3.1-1 捕获了id类型目标的栈GuBlock的copy
在上一节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操作彻底相同:
图2.3.1-2 捕获了id类型目标的堆GuBlock的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.3.1-3 捕获了id类型目标的栈GuBlock的copy
在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()
的办法即可.
图2.3.3-1 捕获了GuBlock类型目标的栈GuBlock的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拜访的都是同一个主动变量的值了.
图2.4-1 经过forwarding读写主动变量
此刻, 咱们就取得了一个进一步优化的GuBlock模型.
在这个模型中, 包括了初心——所要履行的代码块的函数指针, 包括了GuBlock类型, 包括了引证计数(仅对堆GuBlock有意义), 包括了所截获的只读主动变量的值, 包括了指向需求读写的主动变量的指针的地址(forwarding
), 包括了所捕获的静态部分变量的地址, 且能够正常拜访到大局变量.
此刻, 为GuBlock的Block_copy()
和Block_dispose()
也产生了一点改变: 因为在copy完结后要做一次栈GuBlock所指向的forwarding
指向的改变, 使这个栈上的forwarding
也指向堆上的变量值(如图2.4.1.3-1所示).
咱们需求在GuBlock中判别其所捕获的变量是不是一个能够写的变量. 但咱们之前的规划只使这个变量能经过一根指针进行拜访, 这个主动变量在”重写后的GuBlock”结构体并不能与其他被捕获的只读主动变量区分开来. 因而咱们需求做进一步的优化——将可读写的主动变量用一个结构体来表明, 并在其间增加上类型描绘.
将forwarding
指针打包放到一个结构体中去, 并增加一个枚举, 表明这个主动变量的类型.
图2.4-2 对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了.
图2.5-1 一个根本完结的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 <<<, 备注我的诨名成功率更高哦~