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版本写动画时),是否也考虑引用循环问题? 

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容