浅谈Block原理

摘要
block是2010年WWDC苹果为Objective-C提供的一个新特性,它为我们开发提供了便利,比如GCD就大量使用了block,用来往执行队列中添加执行。那么block到底是什么东西呢。其实它就是一个闭包,一个引用自动变量的函数。很多语言也实现自己的闭包,比如C#的lamda表达式。这篇文章将从分析源码的角度来分析下block到底是什么鬼。
最简单的block,不持有变量
我们先新建一个源文件:block.c 代码如下

include <stdio.h>int main(){ void (^blk)(void) = ^(){printf("This is a block.");}; blk(); return 0;}

我们使用clang(LLVM编译器,和GCC类似),通过命令clang -rewrite-objc block.c
,解析block.c这样我们就会得到对应的cpp文件block.cpp。去除一些影响我们阅读的代码。如下:
struct __block_impl { void *isa; int Flags; int Reserved; void FuncPtr;};struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0 Desc; __main_block_impl_0(void *fp, struct __main_block_desc_0 desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }};static void __main_block_func_0(struct __main_block_impl_0 __cself) { printf("This is a block.");}static struct __main_block_desc_0 { size_t reserved; size_t Block_size;} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};int main(){ void (blk)(void) = (void ()())&__main_block_impl_0((void )__main_block_func_0 ,&__main_block_desc_0_DATA); ((void ()(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); return 0;}

下面我们来分析下源码,看看我们定义的block到底是个什么东西。先看下main()函数,我们定义了block
void (^blk)(void) = ^(){printf("This is a block.");};

被转化成了
void (blk)(void) = (void ()())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);

去除影响阅读的强制转换代码后
void (*blk)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

这样就清晰了。我们写的block被转化成了指向__main_block_impl_0
结构体的指针。构造函数的参数我们先不管,慢慢一步步分析首先,我们来看下第一个struct
struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr;};

isa指针,如果我们对runtime了解的话,就明白isa指向Class的指针。
Flags,当block被copy时,应该执行的操作
Reserved为保留字段
FuncPtr指针,指向block内的函数实现

__block_impl
保存block的类型isa(如&_NSConcreteStackBlock),标识(当block发生copy时,会用到),block的方法。方法实现如下
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { printf("This is a block.");}

下面我们再看一个结构体
static struct __main_block_desc_0 { size_t reserved; size_t Block_size;} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

reserved为保留字段默认为0
Block_size为sizeof(struct __main_block_impl_0)
,用来表示block所占内存大小。因为没有持有变量,block大小为impl的大小加上Desc指针大小
__main_block_desc_0_DATA
为__main_block_desc_0的一个结构体实例这个结构体,用来描述block的大小等信息。如果持有可修改的捕获变量时(即加__block
),会增加两个函数(copy和dispose),我们后面会分析

再看最重要的一个结构体__main_block_impl_0

struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }};

__main_block_impl_0
里面有两个变量struct __block_impl impl
和struct __main_block_desc_0

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc;}

结构体构造函数用来初始化变量__main_block_impl_0
和__main_block_desc_0
注:clang转换的代码和真实运行时有区别。应该为impl.isa = &_NSConcreteGlobalBlock

我们再来看下最开始的
void (*blk)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

我们可以看到,block其实就是指向__main_block_impl_0
的结构体指针,这个结构体包含两个__block_impl
和__main_block_desc_0
两个结构体,和一个方法。通过上面的分析,是不是很已经清晰了最后,main函数里面的
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

同样,去除转化代码,上面的代码就可以转化为
blk->FuncPtr(blk);

执行block函数
这样我们就完成了,对简单block实现的分析。是不是很简单
持有变量的block
我们知道block可以持有变量,现在我们实现一个持有变量的block。修改下原来的block.c源文件

include <stdio.h>int main(){ int i = 4; void (^blk)(void) = ^(){printf("i = %d", i);}; i++; blk(); return 0;}

同样的,使用clang命令转化下上述代码
struct __block_impl { void *isa; int Flags; int Reserved; void FuncPtr;};struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0 Desc; int i; __main_block_impl_0(void *fp, struct __main_block_desc_0 desc, int _i, int flags=0) : i(_i) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }};static void __main_block_func_0(struct __main_block_impl_0 __cself) { int i = __cself->i; // bound by copy printf("i=%d", i);}static struct __main_block_desc_0 { size_t reserved; size_t Block_size;} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};int main(){ int i = 4; void (blk)(void) = (void ()())&__main_block_impl_0((void )__main_block_func_0, &__main_block_desc_0_DATA, i); ((void ()(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); i++; return 0;}

我们只看下在持有变量时,block转化,有哪些不同
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int i; /看这里~看这里~/ __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _i, int flags=0) : i(_i) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }};

__main_block_impl_0
结构体多了一个变量i。这个变量用来保存main函数的变量i。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int i = __cself->i; // bound by copy printf("i=%d", i);}

在执行block时,取出的i为__main_block_impl_0保存的值,这两个变量不是同一个。这就是为什么我们执行了i++操作,再执行block,i的值仍然不变的原因
可修改持有变量的block
为了修改持有变量,我们在变量前面加上__block
,修改后的block.c如下

include <stdio.h>int main(){ __block int i = 4; void (^blk)(void) = ^(){printf("i = %d", i);}; i++; blk(); return 0;}

转化后的代码如下
struct __block_impl { void isa; int Flags; int Reserved; void FuncPtr;};struct __Block_byref_i_0 { void __isa; __Block_byref_i_0 __forwarding; int __flags; int __size; int i;};struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0 Desc; __Block_byref_i_0 i; // by ref __main_block_impl_0(void fp, struct __main_block_desc_0 desc, __Block_byref_i_0 _i, int flags=0) : i(_i->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }};static void __main_block_func_0(struct __main_block_impl_0 __cself) { __Block_byref_i_0 i = __cself->i; // bound by ref (i->__forwarding->i)++; printf("i=%d", (i->__forwarding->i));}static void __main_block_copy_0(struct __main_block_impl_0dst, struct __main_block_impl_0src) { _Block_object_assign((void)&dst->i, (void)src->i, 8/BLOCK_FIELD_IS_BYREF/);}static void __main_block_dispose_0(struct __main_block_impl_0src) { _Block_object_dispose((void)src->i, 8/BLOCK_FIELD_IS_BYREF/);}static struct __main_block_desc_0 { size_t reserved; size_t Block_size; void (copy)(struct __main_block_impl_0, struct __main_block_impl_0); void (dispose)(struct __main_block_impl_0);} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};int main(){ attribute((blocks(byref))) __Block_byref_i_0 i = {(void)0,(__Block_byref_i_0 )&i, 0, sizeof(__Block_byref_i_0), 4}; void (blk)(void) = (void ()())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 )&i, 570425344); ((void ()(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); return 0;}

我们发现当我们想要修改持有变量时,转化后的代码有所增加。当我们在变量前面加上__block
时,就会生成一个结构体,来保存变量值。新增了结构体__Block_byref_i_0
,实现如下
struct __Block_byref_i_0 { void *__isa; __Block_byref_i_0 *__forwarding; int __flags; int __size; int i;};

__isa指向变量Class
____forwarding,指向自己的指针,当从栈copy到堆时,指向堆上的block
__flags,当block被copy时,标识被捕获的对象,该执行的操作
__size,结构体大小
i,持有的变量

看下转换后的main函数
attribute((blocks(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 4};

Block_byref_i_0 i = {(void)0,&i, 0, sizeof(*Block_byref_i_0), 4};int i = 4
被转化成上述代码。它被转化成结构体__Block_byref_i_0
。__Block_byref_i_0
持有变量i。
i++;blk();

也转化成对__Block_byref_i_0
中的变量i进行++运算
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_i_0 *i = __cself->i; // bound by ref (i->__forwarding->i)++; printf("i=%d", (i->__forwarding->i));}

这样便达到对i值的修改
Block_copy(...)的实现
根据Block.h上显示,Block_copy(...)被定义如下:

define Block_copy(...) ((__typeof(VA_ARGS))_Block_copy((const void *)(VA_ARGS)))

_Block_copy
被声明在runtime.c中,对应实现:
void *_Block_copy(const void *arg) { return _Block_copy_internal(arg, WANTS_ONE);}

该方法调用了
/* Copy, or bump refcount, of a block. If really copying, call the copy helper if present. */static void *_Block_copy_internal(const void *arg, const int flags) { struct Block_layout *aBlock; ... if (aBlock->flags & BLOCK_NEEDS_FREE) { // latches on high latching_incr_int(&aBlock->flags); return aBlock; } else if (aBlock->flags & BLOCK_IS_GLOBAL) { return aBlock; } // Its a stack block. Make a copy. struct Block_layout *result = malloc(aBlock->descriptor->size); if (!result) return (void )0; memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first // reset refcount result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed result->flags |= BLOCK_NEEDS_FREE | 1; result->isa = _NSConcreteMallocBlock; if (result->flags & BLOCK_HAS_COPY_DISPOSE) { //printf("calling block copy helper %p(%p, %p)...\n", aBlock->descriptor->copy, result, aBlock); (aBlock->descriptor->copy)(result, aBlock); // do fixup return result; }}

当原始block在堆上时,引用计数+1。当为全局block时,copy不做任何操作
// Its a stack block. Make a copy.struct Block_layout *result = malloc(aBlock->descriptor->size);if (!result) return (void )0;memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first// reset refcountresult->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not neededresult->flags |= BLOCK_NEEDS_FREE | 1;result->isa = _NSConcreteMallocBlock;if (result->flags & BLOCK_HAS_COPY_DISPOSE) { //printf("calling block copy helper %p(%p, %p)...\n", aBlock->descriptor->copy, result, aBlock); (aBlock->descriptor->copy)(result, aBlock); // do fixup}

当block在栈上时,调用Block_copy,block将被copy到堆上。如果block实现了copy和dispose方法,则调用对应的方法,来处理捕获的变量。
小节
通过上面的分析,相信大家对block有了更加清晰的理解。😊如果大家有兴趣,可以看下block在runtime的源码,结合我们上面转换的c++代码,可以看到更完整实现细节。下节,我将从使用block所引发的retain cycle问题,来分析runtime的源码

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

推荐阅读更多精彩内容