一 简介
Objective-C 中的block
,是匿名函数,匿名函数在别的语言中也被称作闭包、λ表达式等。Objective-C 中的 block,有以下几个特性:
- 可以将
block
作为函数,也能作为对象,当做属性持有,定义内存修饰词。 - 可以长期存在,出了定义的作用域,内部实现也能执行。
- 可以捕获外部变量,甚至使用
__block
修饰后,block 可以修改这个捕获变量。
这背后的原理是什么呢,看了很多博客和代码,理清楚了其中奥秘,在这里总结一下。
二 结构
block
的结构,用clang -rewrite-objc
把objc
代码转成cpp
代码,就能很清楚的看到了。在cpp
中,block
实际被编译器转写成结构体了。结构体的介绍网上已经有很多完善的资料了,我这里直接参考了这篇博客。
// objc 代码
int test()
{
void (^blk)(void) = ^{
printf("Block\n");
};
blk();
return 0;
}
// cpp 重写后
// 自己定义的 test 方法
int test()
{
// 声明&赋值
void (*blk)(void) = ((void (*)())&__test_block_impl_0/*构造函数*/(
(void *)__test_block_func_0,
&__test_block_desc_0_DATA)
);
// 执行
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
// 我们创建的 block 的结构体
struct __test_block_impl_0 {
struct __block_impl impl; // 核心:函数指针
struct __test_block_desc_0* Desc; // 描述
// 构造函数
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;// 栈 block
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __block_impl {
void *isa; // isa指针,所以是objc对象
int Flags;
int Reserved;
void *FuncPtr; // 函数指针
};
// 静态函数指针
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
printf("Block\n");
}
// 描述
static struct __test_block_desc_0 {
size_t reserved;
size_t Block_size; // 大小
} __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0)}; // __test_block_desc_0_DATA 是这个结构体的实例
这个结构刚才引用的博客中已经分析的很透彻了,这里就不一步步细讲了。直接说一下我总结的结构。
__xx_block_impl_0 // block完整结构体
{
__block_impl, // 核心结构,具体结构在下面
desc, // block 描述,具体结构在下面
int foo, // 捕获的变量,依次排列开
...
__xx_block_impl_0(fp, desc, param0,param1...)//构造函数
}
struct __block_impl
{
id isa, // isa 指针
void *fp, // 函数指针
flag // 标记位,用来描述 block 类型
}
block_desc // block 描述
{
size,// 大小
void *copy,// 捕获对象时,捕获变量们的copy 函数
void *dispose// 捕获对象时,捕获变量们的dispose 函数
}
// 静态函数指针
static void __xx_block_func_0() {
// 具体实现
}
在讲一下该结构引出的比较让人疑惑的点:
- 一个
block
定义会被编译器clang
编译生成一套特殊结构体,依据block
行为不同(入参、捕获变量),生成的结构体也不一样。从结构体名称类名__方法名__block__impl__序号
也可以知道,它是动态创建的。
相关的创建详情可以参考clang
中genCode
模块,GCBlocks。 - 结构体是对象,分成三类:
- 栈block
_NSConcreteStackBlock
- 全局block
_NSConcreteGlobalBlock
- 堆block
_NSConcreteMallocBlock
详细的区别参考这里。这里只说一点,栈block在赋值时,会被拷贝到堆上,这样就通过引用计数管理了(引用计数的内容可以看我的博客坑位、待填)。
关于这个拷贝操作,开始我并没有在代码中看见,只看见了赋值void (*blk)(void) = ((void (*)())&__test_block_impl_0...
,找了很久,才发现真实依据是NSObject.mm。
// The -fobjc-arc flag causes the compiler to issue calls to objc_{retain/release/autorelease/retain_block}
id objc_retainBlock(id x) {
return (id)_Block_copy(x);
}
- 一个奇怪的细节:
block
声明时,得到的是__test_block_impl_0
类型变量blk
;执行时,却转成了__block_impl
类型。
这里是因为__block_impl
是__test_block_impl_0
的第一个变量,二者的指针地址相同的,所以可以直接强转,详见这篇的 2.3 部分。
三 参数捕获
上面说到block
捕获的变量不同,动态生成的结构也不一样。block可以捕获:objc 对象
、基础数据类型
、另一个 block
。我们常用的是捕获基础数据类型和 objc 对象
和基础数据类型
。
下面就常用情况再分成四种情况讨论:
这部分有大牛珠玉在前,解释的比较透彻了。所以我就简单总结一下:
- 捕获基础数据,在
block
结构中,会将捕获参数添加进去。前面讲了,在block
赋值时,会被执行一个_Block_copy
操作,这其中对整个block
结构进行拷贝,捕获的基础数据值也会被拷贝到堆上。 - 捕获对象变量时,除了会被添加到
block
结构体中,还会额外生成一对被捕获变量的拷贝函数
和销毁函数
,保存在__block_desc
结构中。这里面描述了被捕获的变量,如何被拷贝
到堆上去。
// 捕获变量的copy
// _Block_copy 的时候会调用
// _Block_copy 在 block 被赋值时候调用
static void __TestClass__testMethod_block_copy_2(struct __TestClass__testMethod_block_impl_2*dst, struct __TestClass__testMethod_block_impl_2*src) {
_Block_object_assign((void*)&dst->tmpB, (void*)src->tmpB, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
// 捕获变量的dispose
// _Block_release 的时候会调用
// _Block_release 的调用时机是堆 block 引用计数为 0 时
static void __TestClass__testMethod_block_dispose_2(struct __TestClass__testMethod_block_impl_2*src) {
_Block_object_dispose((void*)src->tmpB, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
- 捕获
__block
修饰的基础数据,该基础数据会被转化成一个特殊的结构体:
// 定义处,在栈上
__block int tmpB = 1;
void(^blk003)(int a) = ^(int a) {
// 使用处,在堆上
NSLog(@"tmpB=%d", tmpB);
};
// __block 修饰的 int 变量 tempB
struct __Block_byref_tmpB_0 {
void *__isa;
__Block_byref_tmpB_0 *__forwarding;
int __flags;
int __size;
int tmpB;
};
我们称它为byref 封装
,可以看到它也是一个 objc 对象
。所以byref 封装
和被捕获变量一样,会被添加到block 结构
里之外,还会生成copy
和dispose
函数指针。
byref 封装
除了封装了自身的实际值之外,还持有一个自己类型的__forwarding
指针。当byref 封装
在栈上的时候,__forwarding
指针会指向它堆上的拷贝,在堆上的时候回指向自己。这样就保证了定义处和使用处调用方式是一样的,至于源码是如何实现的,下面会讲到。
- 捕获
__block
修饰的对象,同样也会生成一个byref 封装
,结构也和上一条类似:
__block NSString * tempC = [NSString stringWithFormat:@"1"];
void (^test)() = ^ {
NSLog(@"%@",tempC);
};
// __block 修饰的 int 变量 tempC
struct __Block_byref_tempC_0 {
void *__isa;
__Block_byref_tmpC_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);//实际赋值__Block_byref_id_object_copy_131
void (*__Block_byref_id_object_dispose)(void*);//同上
NSString *tempC;
};
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
但是多处量函数指针变量,这俩变量在创建时,被传值__Block_byref_id_object_copy_131
和__Block_byref_id_object_dispose_131
。这俩在byref 封装
拷贝时会调用到,用来拷贝NSString *tempC
的。
总结:可以看到 __xx_block_impl_0
结构随着捕获变量的复杂而变得复杂。而block
从栈到堆的拷贝,是由自身结构
->捕获变量
->byref的value
,层层进行的。
四 堆 block 拷贝
上面总结了拷贝的流程,这一节撸一下代码,一来加深印象,二来找下几个常见问题的答案:
- 在 block 中被修改了,栈上的原值也会被修改吗?
- 循环引用是如何产生的,
weak-strong-dance
为何能解决? - 使用
weak-strong-dance
,被引用对象何时会为 nil ?
block
结构体的拷贝方法 _Block_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;
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;
}
else {
// Its a stack block. Make a copy.
// !!!!开辟堆上内存空间
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
// !!!!将内容移到指定堆上
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
result->invoke = aBlock->invoke;
#endif
// 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;
}
}
_Block_call_copy_helper
参数拷贝助手
static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
if (!desc) return;
(*desc->copy)(result, aBlock); // do fixup
}
static void __TestClass__testMethod_block_copy_2(struct __TestClass__testMethod_block_impl_2*dst, struct __TestClass__testMethod_block_impl_2*src) {
_Block_object_assign((void*)&dst->tmpB, (void*)src->tmpB, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
_Block_object_assign
参数拷贝函数
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void **dest = (const void **)destArg;
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_OBJECT:
/*******
id object = ...;
[^{ object; } copy];
********/
_Block_retain_object(object);
// static void _Block_retain_object_default(const void *ptr __unused) { }
// static void _Block_destructInstance_default(const void *aBlock __unused) {}
// !!!!都是空方法
*dest = object;
break;
case BLOCK_FIELD_IS_BLOCK:
/*******
void (^object)(void) = ...;
[^{ object; } copy];
********/
// !!!!block 参数,再调一次 `_Block_copy`
*dest = _Block_copy(object);
break;
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
/*******
// copy the onstack __block container to the heap
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__block ... x;
__weak __block ... x;
[^{ x; } copy];
********/
// !!!!__block 修饰的基础数据类型
*dest = _Block_byref_copy(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
/*******
// copy the actual field held in the __block container
// Note this is MRC unretained __block only.
// ARC retained __block is handled by the copy helper directly.
__block id object;
__block void (^object)(void);
[^{ object; } copy];
********/
// !!!!__block 修饰的对象
// !!!!直接赋值,对其引用计数+1
*dest = object;
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
/*******
// copy the actual field held in the __block container
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__weak __block id object;
__weak __block void (^object)(void);
[^{ object; } copy];
********/
// !!!!__block __weak 双重修饰的对象
// !!!!直接赋值,对其引用计数+1
// !!!!相当于 __weak 没有起作用
*dest = object;
break;
default:
break;
}
}
_Block_byref_copy
对 __forwarding
指针的操作
// 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;
// !!!! 通过引用计数来判断原 byref 的 forwarding 是否为堆上变量
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;
// !!!! 堆上 byref 指向自己地址本身
copy->forwarding = copy; // patch heap copy to point to itself
// !!!! 原栈上 byref 指向堆上地址
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;
// !!!! 判断有 copy 或 dispose 函数,会执行相应函数
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
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
//!!!! 引用计数+1 难道所有对象的引用计数,都是用 flag 来表示的??
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}
static int32_t latching_incr_int(volatile int32_t *where) {
while (1) {
int32_t old_value = *where;
if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
return BLOCK_REFCOUNT_MASK;
}
if (OSAtomicCompareAndSwapInt(old_value, old_value+2, where)) {
return old_value+2;
}
}
}
五 总结
关于 __block 修饰的变量,怎么同修改的
1、__block 修饰的变量,会生成一个结构体 blk_byref。里面有一个 forwarding 指针、一个实际值,开始时 forwarding 指向自己。
2、在 blk 赋值时,(void (^blk2)(void) = ^{ .....} )会把 blk copy 到堆上,同时 copy 捕获的参数(刚才的栈上 blk_byref)到堆上。 见 _Block_copy 方法。
3、copy 捕获的参数时,设置 forwarding 指针,栈->堆,堆->自己。见 _Block_object_assign 方法。
4、一个参数如果被多个 blk 捕获了(如果 forwarding 指针有值),在 copy 时候,就不在堆上创建新的 blk_byref,这样就能保证一个变量被多个blk捕获时,有且只有一份堆结构 blk_byref。见 _Block_byref_copy 方法。
5、实际上,所有访问操作(blk内,blk外),都是 blk_byref->forwardig->val,因为栈/堆forwarding都到堆上了,最后实际修改的都是堆上的blk_ref ->val,不会改栈上blk_ref ->val。
sunnyxx 面试题
看完了上面内容,可以用下面的几道题测试下自己的掌握程度
参考:
破弓的《iOS Block》系列
libclosure 源码
objc4 源码
https://www.jianshu.com/p/51d04b7639f1
https://www.jianshu.com/p/b554e813fce1
https://www.jianshu.com/p/e42f86a81045
http://clang.llvm.org/docs/Block-ABI-Apple.html#block-escapes
GCBlocks