Block 底层原理

Block语法 

Block可以认为是一个匿名函数。语法声明如下:

return_type (^block_name)(parameters) 

例如:

double (^multiplyTwoValues)(double, double); 

Block字面值的写法:

^ (double firstValue, double secondValue) { 

return firstValue *secondValue; 

上面写法省略了返回值的类型,可以显示的指出返回值类型。

typedef double (^MultiplyTwoVlaues)(double, double); 

MultiplyTwoVlaues mtv = ^(double firstValue, double secondValue){ 

return firstValue * secondValue; 

}; 

NSLog(@"%f", mtv(3, 4)); 

Block也是一个Objective-C对象,可以用于赋值,当参数传递,也可以放在集合中。

数据结构定义 

block的数据结构定义:


block 

对应的结构体定义如下:

struct Block_descriptor { 

unsigned long int reserved; 

unsigned long int size; 

void (*copy)(void *dst, void *src); 

void (*dispose)(void *); 

}; 

struct Block_layout { 

void *isa; 

int flags; 

int reserved; 

void (*invoke)(void *, ...); 

struct Block_descriptor *descriptor; 

/* Imported variables. */ 

}; 

block实际上由6部分组成:

isa指针,所有对象都有该指针,用来指向对象相关实现。 

flags,用于按位表示一些block附加信息。 

reserved, 保留变量。 

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

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

variables,capture过来的变量,block能够访问它外部的局部变量,就是因为将这些变量复制到了结构体中。 

Block分类 

在Objective-C中,一共有3种类型的Block:

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

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

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

Clang 

为了研究编译器是如何实现block的,需要使用clang,clang命令,可以将Objective-C的源码改写成C语言,命令如下:

clang -rewrite-objc xxx.c 

Block实现 

全局的静态Block实现 

新建一个globalBlock.c的源文件:

#include  

int main() 

^{ 

printf("Hello, World!/n"); 

} (); 

return 0; 

在文件所在的文件夹,使用命令:clang -rewrite-objc globalBlock.c,在目录中得到一个名为globalBlock.c的文件。其中关键代码:

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("Hello, World!/n"); 

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 (*)())&;__main_block_impl_0((void *)__main_block_func_0, &;__main_block_desc_0_DATA)) (); 

return 0; 

下面具体看一下是如何实现的。__main_block_impl_0就是block的实现,从中可以看出:

一个block实际上就是一个对象,主要由 isa、 impl和 descriptor组成。 

在LLVM实现中,开启ARC时。block的 isa应该是 _NSConcreteGlobalBlock类型。因为使用clang命令并没有开启ARC,所以还是 _NSConreteStackBlock类型。 

impl是实际的函数指针。它指向 __main_block_func_0。其实, impl就是 invoke变量。 

descriptor 是描述当前这个block的附加信息,包括大小,需要capture和dispose的变量列表等。 

静态Block实现 

新建一个名为 stackBlock.c的文件:

#include  

int main() 

int a = 100; 

void (^stackBlock)(void) = ^{ 

printf("%d/n", a); 

}; 

stackBlock(); 

return 0; 

使用clang命令:

struct __main_block_impl_0 { 

struct __block_impl impl; 

struct __main_block_desc_0* Desc; 

int a; 

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) { 

impl.isa = &;_NSConcreteStackBlock; 

impl.Flags = flags; 

impl.FuncPtr = fp; 

Desc = desc; 

}; 

static void __main_block_func_0(struct __main_block_impl_0 *__cself) { 

int a = __cself->a; // bound by copy 

printf("%d/n", a); 

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 a = 100; 

void (*stackBlock)(void) = ((void (*)())&;__main_block_impl_0((void *)__main_block_func_0, &;__main_block_desc_0_DATA, a)); 

((void (*)(__block_impl *))((__block_impl *)stackBlock)->FuncPtr)((__block_impl *)stackBlock); 

return 0; 

本例中,

isa指向_NSConcreteStackBlock,这是分配在栈上的实例。 

__main_block_impl_0中增加了一个变量a, 在block中引用的变量a实际是在声明block时,被复制到 __main_block_impl_0结构体中的那个变量a。所以,在block内部修改变量a的内容, 不会影响外部变量a。 

__main_block_impl_0增加了变量a, 所有结构体大小变了,该结构体被写在 __main_block_desc_0中。 

修改上面的代码,在变量前面添加__block关键字:

#include  

int main() 

__block int i = 100; 

void (^stackBlock)(void) = ^{ 

printf("%d/n", i); 

i = 10; 

}; 

stackBlock(); 

return 0; 

转换代码:

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 

printf("%d/n", (i->__forwarding->i)); 

(i->__forwarding->i) = 10; 

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&;dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);} 

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_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), 100}; 

void (*stackBlock)(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 *)stackBlock)->FuncPtr)((__block_impl *)stackBlock); 

return 0; 

代码可以看到:

源码中添加了一个名为 __Block_byref_i_0的结构体,用来保存需要capture并且修改的变量i。 

在 __main_block_impl_0中引用的 __Block_byref_i_0的结构体指针,这样就可以达到修改外部变量的作用。 

__Block_byref_i_0结构体中带有isa,说明也是一个对象。 

我们需要负责 __Block_byref_i_0结构体相关的内存管理,所以 __main_block_desc_0中增加了copy和dispose函数指针,对于在调用前后修改相应的引用计数。 

堆Block实现 

NSConcreteMallocBlock类型的block通常不会再源码中直接出现,默认当block被copy的时候,才会将block复制到堆中。以下是block被copy时的代码(来自这里),在第8步,目标block类型被修改为_NSConcreteMallocBlock。

static void *_Block_copy_internal(const void *arg, const int flags) { 

struct Block_layout *aBlock; 

const bool wantsOne = (WANTS_ONE &; flags) == WANTS_ONE; 

// 1 

if (!arg) return NULL; 

// 2 

aBlock = (struct Block_layout *)arg; 

// 3 

if (aBlock->flags &; BLOCK_NEEDS_FREE) { 

// latches on high 

latching_incr_int(&;aBlock->flags); 

return aBlock; 

// 4 

else if (aBlock->flags &; BLOCK_IS_GLOBAL) { 

return aBlock; 

// 5 

struct Block_layout *result = malloc(aBlock->descriptor->size); 

if (!result) return (void *)0; 

// 6 

memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first 

// 7 

result->flags &;= ~(BLOCK_REFCOUNT_MASK); // XXX not needed 

result->flags |= BLOCK_NEEDS_FREE | 1; 

// 8 

result->isa = _NSConcreteMallocBlock; 

// 9 

if (result->flags &; BLOCK_HAS_COPY_DISPOSE) { 

(*aBlock->descriptor->copy)(result, aBlock); // do fixup 

return result; 

变量的复制 

对于block外部变量的引用,block默认是将其复制到其数据结构中来实现访问的。 

对于__block修饰的外部变量引用,block是复制其引用地址来实现访问的。 

另外,可以参考《招聘一个靠谱的iOS (下)13、14题》。

ARC,MRC 对Block类型的影响 

在ARC开启的情况下,将只会有NSConcreteGlobalBlock和NSConcreteMallocBlock类型的block。原本NSConreteStackBlock被NSConcreteMallocBlock类型替代。

在Block中,如果只使用全局或静态变量或者不是用外部变量,那么Block块的代码会存储在全局区。

在ARC中

如果使用外部变量,Block块的代码会存储在堆区。 

在MRC中

如果使用外部变量,Block块的代码会存储在栈区。 

Block默认情况下不能修改外部变量,只能读取外部变量。

在ARC中

外部变量在堆中,这个变量在Block块内与在Block块外地址相同。 

外部变量在栈中,这个变量会被copy到为Block代码块所分配的堆中。 

在MRC中

外部变量在堆中,这个变量在Block块内与在Block块外地址相同。 

外部变量在栈中,这个变量会被copy到为Block代码块所分配的栈中。 

如果需要修改外部变量,需要在外部变量前声明__block。

在ARC中

外部变量存在堆中,这个变量在Block块内与Block块外地址相同。 

外部变量存在栈中,这个变量会被转移到堆中,不是复制。 

在MRC中

外部变量存在堆中,这个变量在Block块内与Block块外地址相同。 

外部变量存在栈中,这个变量在Block块内与Block块外地址相同。 

关于Block的面试题 

使用block时什么情况会发生引用循环,如何解决? 

在block内如何修改block外部变量? 

使用系统的某些block api(如UIView的block版本写动画时),是否也考虑引用循环问题? 

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

推荐阅读更多精彩内容