Block 最早出现是在 Mac OS X 10.6 和 iOS 4 中,作为对 C 语言的扩展,用来实现匿名函数的特性,在如今 Objective-C 开发的项目中 Block 随处可见。Block 为 Objective-C 提供了强大的函数式编程能力,为日常开发带来了极大的便利。那么对于 Block,你又了解多少?
初识
在 A Short Practical Guide to Blocks 文章中, Apple 列举了几种在系统框架 API 中 Block 的使用场景:
- 任务完成回调
- 通知回调
- 错误回调
- 枚举
- 视图动画以及变换
- 排序
在Clang 9的官方文档中,Block的实现规范本文的开头是这样描述Block的:
struct Block_literal_1 {
void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 {
unsigned long int reserved; // NULL
unsigned long int size; // sizeof(struct Block_literal_1)
// optional helper functions
void (*copy_helper)(void *dst, void *src); // IFF (1<<25)
void (*dispose_helper)(void *src); // IFF (1<<25)
// required ABI.2010.3.16
const char *signature; // IFF (1<<30)
} *descriptor;
// imported variables
};
Block是个包含isa指针的结构体,熟悉Objective-C的同学都知道,Objective-C中的对象也是一个包含isa指针的结构体,所有Block也可以当做是一个对象。通过注释发现Block可以被初始化NSConcreteStackBlock或NSConcreteGlobalBlock。(以下所述内容默认环境为ARC)
类型
区块有以下几种:
// the raw data space for runtime classes for blocks
// class+meta used for stack, malloc, and collectable based blocks
BLOCK_EXPORT void * _NSConcreteMallocBlock[32]
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
BLOCK_EXPORT void * _NSConcreteAutoBlock[32]
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
BLOCK_EXPORT void * _NSConcreteFinalizingBlock[32]
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
BLOCK_EXPORT void * _NSConcreteWeakBlockVariable[32]
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
// declared in Block.h
// BLOCK_EXPORT void * _NSConcreteGlobalBlock[32];
// BLOCK_EXPORT void * _NSConcreteStackBlock[32];
其中NSConcreteAutoBlock,NSConcreteFinalizingBlock,NSConcreteWeakBlockVariable只在GC环境下使用。
NSConcreteGlobalBlock
未捕获任何变量或仅捕获的变量为以下类型的Block是NSConcreteGlobalBlock。
静态变量
整体变量
静态变量
NSLog(@"%@",^(void) {});
NSConcreteStackBlock
只要捕获了以上三种类型以外的变量的Block是NSConcreteStackBlock。
int c;
NSLog(@"%@",^(void) { c; });
NSConcreteMallocBlock
系统不提供直接创建NSConcreteMallocBlock的方式,但是可以对NSConcreteStackBlock进行复制操作来生成NSConcreteMallocBlock。
以下情况,块会进行复制操作:
- 手动执行copy方法
- 将Block赋值给__strong修饰符修饰(系统替换)的Block或id对象
- 作为方法的返回值
- 系统API中包含usingBlock的方法
int c;
id block = ^(void) {
c;
};
NSLog(@"%@",block);
生命周期
先看一张内存段分布图:
也就是说 NSConcreteStackBlock 是由编译器自动管理,超过作用域之外就会自动释放了。而 NSConcreteMallocBlock 是由程序员自己管理,如果没有被强引用也会被销毁。NSConcreteGlobalBlock 由于存在于全局区,所以会一直伴随着应用程序。
变量
任意类型的变量都可以在 Block 中被访问,但是能够被修改的变量只有以下三种:
- 静态变量
- 全局变量
- 静态全局变量
全局变量和静态全局变量由于存在于全局区作用域广,所以在 Block 内部能够直接修改。那么对于静态变量是怎么实现修改的?
可以使用 Clang 提供的命令来进一步的分析:
clang -rewrite-objc Hello.m
使用 clang -rewrite-objc 命令对代码的转换并不与实际编译过程相同,但是转换后的代码可读性更高,可以更好的帮助理解 Block 的机制。
该命令可以将 Objective-C 的代码转成 C++ 代码。
int a;
static int b;
- (void)main
{
static int c;
int d;
^(void) {
a++;
b++;
c++;
d;
};
}
以上代码转换后变为:
int a;
static int b;
struct __Hello__main_block_impl_0 {
struct __block_impl impl;
struct __Hello__main_block_desc_0* Desc;
int *c;
int d;
__Hello__main_block_impl_0(void *fp, struct __Hello__main_block_desc_0 *desc, int *_c, int _d, int flags=0) : c(_c), d(_d) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __Hello__main_block_func_0(struct __Hello__main_block_impl_0 *__cself) {
int *c = __cself->c; // bound by copy
int d = __cself->d; // bound by copy
a++;
b++;
(*c)++;
d;
}
static struct __Hello__main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __Hello__main_block_desc_0_DATA = { 0, sizeof(struct __Hello__main_block_impl_0)};
static void _I_Hello_main(Hello * self, SEL _cmd) {
static int c;
int d;
((void (*)())&__Hello__main_block_impl_0((void *)__Hello__main_block_func_0, &__Hello__main_block_desc_0_DATA, &c, d));
}
原 Block 转换为 __Hello__main_block_impl_0 结构体,内部定义了指针 c 以及变量 d。原 main 方法转换为 _I_Hello_main 函数,函数中构造了 __Hello__main_block_impl_0,传入了 c 变量的地址,以及 d 变量的值。这就是为什么局部变量 d 不能被修改,而静态变量 c 可以被修改的原因。c 是指针传递,而 d 是值传递。之所以使用指针传递,是因为作用域的限制,通过指针进行作用域扩展,在 C 语言中是很常见且简单的做法。
那么为什么 d 不使用指针传递,这是因为局部变量是存储在栈上,其生命周期是不稳定,Block 中通过指针访问到的局部变量可能已经销毁了。而静态变量是存储在静态数据存储区的,与应用程序生命周期一致,是可以保证正确访问的变量。
__Hello__main_block_func_0 是 Block 的执行函数与 void (*invoke)(void *, ...) 对应,该函数的入参为 Block 实例。由于 a、b 是全局变量,所以在函数内部直接进行 + 操作,而对 c 进行 + 操作是通过指针来执行的,对于 d 不能进行 + 操作,所以在开发阶段编译器直接进行了报错。
Property 和 Ivar
如果在 Block 内部对 Property 或 Ivar 进行修改,发现是可以修改成功的,实际上 Property 内部操作的还是 Ivar,所以需要了解下为何 Ivar 可以在 Block 内部修改。
{
int _b;
}
- (void)main
{
^(void) {
_b++;
};
}
以上代码转换后变为:
extern "C" unsigned long OBJC_IVAR_$_Hello$_b;
struct __Hello__main_block_impl_0 {
struct __block_impl impl;
struct __Hello__main_block_desc_0* Desc;
Hello *self;
__Hello__main_block_impl_0(void *fp, struct __Hello__main_block_desc_0 *desc, Hello *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __Hello__main_block_func_0(struct __Hello__main_block_impl_0 *__cself) {
Hello *self = __cself->self; // bound by copy
(*(int *)((char *)self + OBJC_IVAR_$_Hello$_b))++;
}
static void __Hello__main_block_copy_0(struct __Hello__main_block_impl_0*dst, struct __Hello__main_block_impl_0*src) {_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __Hello__main_block_dispose_0(struct __Hello__main_block_impl_0*src) {_Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static struct __Hello__main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __Hello__main_block_impl_0*, struct __Hello__main_block_impl_0*);
void (*dispose)(struct __Hello__main_block_impl_0*);
} __Hello__main_block_desc_0_DATA = { 0, sizeof(struct __Hello__main_block_impl_0), __Hello__main_block_copy_0, __Hello__main_block_dispose_0};
static void _I_Hello_main(Hello * self, SEL _cmd) {
((void (*)())&__Hello__main_block_impl_0((void *)__Hello__main_block_func_0, &__Hello__main_block_desc_0_DATA, self, 570425344));
}
可以看到这次转换后的代码多了一个 OBJC_IVAR__b 全局变量,这个全局变量表示变量 b 的内存偏移量,并且在 Block 内部引用了 self(这也是为什么 Block 中使用 Ivar 也会造成循环引用的原因),在 _Hello__main_block_func_0 函数中使用 self 作为基地址 + OBJC_IVAR_b 偏移量的方式获取到内存地址,然后进行 + 操作。(这里还多了两个函数: copy 和 dispose ,后文会解释)
__block 修饰符
上文中我们对局部变量 d 进行 + 操作时,编译器提示我们需要加 __block 修饰符,这个 __block 修饰符是什么?为什么加上之后就可以在 Block 中对局部变量进行修改?
__block int a = 1;
^(void) {
a++;
};
以上代码转换后变为:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __Hello__main_block_impl_0 {
struct __block_impl impl;
struct __Hello__main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__Hello__main_block_impl_0(void *fp, struct __Hello__main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __Hello__main_block_func_0(struct __Hello__main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a)++;
}
static void __Hello__main_block_copy_0(struct __Hello__main_block_impl_0*dst, struct __Hello__main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __Hello__main_block_dispose_0(struct __Hello__main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
static struct __Hello__main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __Hello__main_block_impl_0*, struct __Hello__main_block_impl_0*);
void (*dispose)(struct __Hello__main_block_impl_0*);
} __Hello__main_block_desc_0_DATA = { 0, sizeof(struct __Hello__main_block_impl_0), __Hello__main_block_copy_0, __Hello__main_block_dispose_0};
static void _I_Hello_main(Hello * self, SEL _cmd) {
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1};
((void (*)())&__Hello__main_block_impl_0((void *)__Hello__main_block_func_0, &__Hello__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
}
在 __Hello__main_block_impl_0 中,局部变量 a 变成了 __Block_byref_a_0, 这个 struct 中持有 int 类型的变量 a,并且还有 __Block_byref_a_0 类型的指针 __forwarding。
可以发现 __Block_byref_a_0 并没有声明在 __Hello__main_block_impl_0。是因为当有多个 Block 引用了用一个 __block 修饰的变量的情况下,可以复用 __Block_byref_a_0。
在 _I_Hello_main 函数的实现中,先是构建了 __Block_byref_a_0,将 isa 指向 (void*)0 也就是 NULL,将 __forwarding 指向自身,并且将 int 类型的 a 初始化为 1。在构建 __Hello__main_block_impl_0 的时候,将 __Block_byref_a_0 的地址传入了构造函数中,通过指针传递,解决了作用域限制的问题,达到了在 Block 调用函数中使用 __Block_byref_a_0 的目的。
在 __Hello__main_block_func_0 中使用 (a->__forwarding->a)++ 的方式来使局部变量 a 进行 + 操作。第一个 a 指的是 __Block_byref_a_0,由于 __forwarding 指向自身,所以 a->__forwarding 还是 __Block_byref_a_0。第二个 a 就是 __Block_byref_a_0 中的变量 a。
源码
关于 Block 的底层实现源码,可以参考这个 libclosure-67,本文只介绍关于 copy 相关的源码,更多细节读者可以自行研读源码。
_Block_copy
上文提到对 NSConcreteStackBlock 进行 copy 操作后可以生成 NSConcreteMallocBlock ,在 runtime.c 中可以看到 copy 的具体实现:
// Copy, or bump refcount, of a block. If really copying, call the copy helper if present.
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
if (!arg) return NULL;
// The following would be better done as a switch statement
aBlock = (struct Block_layout *)arg;
// 1
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
// 2
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
// 3
else {
// Its a stack block. Make a copy.
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return NULL;
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// reset refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
result->isa = _NSConcreteMallocBlock;
return result;
}
}
实现中有多个针对 flags 的条件判断,flags 在 Block_private.h是这样定义的:
// Values for Block_layout->flags to describe block objects
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};
其中 BLOCK_REFCOUNT_MASK 表示 NSConcreteStackBlock,BLOCK_NEEDS_FREE 表示 NSConcreteMallocBlock,BLOCK_IS_GLOBAL 表示 NSConcreteGlobalBlock。
1、如果是 NSConcreteMallocBlock,则对引用计数递增并且返回 aBlock。
2、如果是 NSConcreteGlobalBlock 则直接返回 aBlock。
3、如果是 NSConcreteStackBlock ,首先分配一块与原 Block 大小相同的内存,然后使用 memmove() 函数将原 Block 的所有元数据按位复制到 result 上,接着将 result 的引用计数置为 0(注释表示这是不需要的,可能是防止某种异常情况出现),之后将 result 的 flags 置为 BLOCK_NEEDS_FREE,引用计数置为 2(注释表示逻辑引用计数为 1,Block 的引用计数以 2 为单位,每次递增2),再调用 _Block_call_copy_helper 函数,这个函数只在 Block 内引用了对象类型或 __block 修饰变量的情况下才会有作用(这种情况转换后的代码中会生成 copy 和 dispose 函数,这两个函数用来管理对象内存的),对于 Block 中的对象类型的 copy 都是指针 copy,生成一个指针指向原对象,最后将 result 的 isa 置为 NSConcreteMallocBlock。
_Block_byref_copy
前面提到对于 __block 修饰的变量最终会转换成 __Block_byref_a_0。在 Block 由栈 copy 到堆的时候,__Block_byref_a_0 也会有 copy 行为,_Block_byref_copy 的具体实现为:
// Runtime entry points for maintaining the sharing knowledge of byref data blocks.
// A closure has been copied and its fixup routine is asking us to fix up the reference to the shared byref data
// Closures that aren't copied must still work, so everyone always accesses variables after dereferencing the forwarding ptr.
// We ask if the byref pointer that we know about has already been copied to the heap, and if so, increment and return it.
// Otherwise we need to copy it and update the stack forwarding pointer
static struct Block_byref *_Block_byref_copy(const void *arg) {
struct Block_byref *src = (struct Block_byref *)arg;
// 1
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;
// 2
if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
// Trust copy helper to copy everything of interest
// If more than one field shows up in a byref block this is wrong XXX
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;
if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
copy3->layout = src3->layout;
}
(*src2->byref_keep)(copy, src);
}
else {
// Bitwise copy.
// This copy includes Block_byref_3, if any.
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
// already copied to heap
// 3
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
// 4
return src->forwarding;
}
1、如果是栈上的 Block_byref 则分配与原 Block_byref 大小相同的内存,将 isa 置为 NULL,将 copy 后的 Block_byref 置为 BLOCK_BYREF_NEEDS_FREE,引用逻辑计数为 2,调用方和栈各有一份,copy 后的 Block_byref 的 forwarding 指向自己,原 Block_byref 的 forwarding 指向 copy 后的 Block_byref,最后赋值 size。
2、对 Block_byref 是否含有对象类型进行判断,并针对不同情况进行内存管理。
3、如果是堆上的 Block_byref 则对其引用计算递增。
4、返回堆上的 Block_byref。
之所以 __block 修饰的变量可以在 Block 中被修改,是因为在 Block 被 copy 到堆上时, Block_byref 也被 copy 到了堆上,并且栈和堆中的 Block_byref 都指向了堆中的 Block_byref。