前言
对于 Block 以及 __block 相关的知识点,之前的面试题已经涉及过:04·iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的?,20·iOS 面试题·请解释以下 keywords 的区别: assign vs weak, __block vs __weak。
但是为了保持 MrPeak-如何面试 iOS 工程师? 中面试题的完整性,我们这里会按照顺序来学习这 20 道面试题。由于之前已经聊过 Block 了,所以我们这篇只会粗略的说明 __block 在 ARC 和 MRC 环境下的区别,对于 Block 和 __block 更详细的的知识点,可以看看之前两篇相关的面试题。
__Block 的作用
我们知道 Block 是匿名函数指针,可以截获外部变量。Block 截获外部变量是将外部变量作为成员变量追加到 Block 结构体中,但是匿名函数存在作用域的问题,这个就是为什么我们不能在 Block 内部去修改普通外部变量的原因。所有就出现了 __block 修饰符,以便解决作用域这个问题。
我们先看下 __block 修饰的变量,转换成 C++ 代码是什么样子的:
//Objective-C 代码
- (void)blockDataBlockFunction {
__block int a = 100; ///在栈区
void (^blockDataBlock)(void) = ^{
a = 1000;
NSLog(@"%d", a);
}; ///在堆区
blockDataBlock();
}
//C++ 代码
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __BlockStructureViewController__blockDataBlockFunction_block_impl_0 {
struct __block_impl impl;
struct __BlockStructureViewController__blockDataBlockFunction_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
};
用 __block 修饰的变量,会生成一个 Block_byref_a_0 结构体来表示外部变量,然后再追加到 Block 结构体中。这里生成 Block_byref_a_0 这个结构体大概有两个原因:
- 抽象出一个结构体,可以让多个 Block 同时引用这个外部变量;
- 好管理,因为 Block_byref_a_0 中有个非常重要的成员变量
forwarding
指针,这个指针非常重要(这个指针指向 Block_byref_a_0 结构体),这里可以保证当我们将 Block 从栈拷贝到堆中,之后修改的变量都是同一份。
__Block 在 ARC 和 MRC 环境的区别
结论先行:
- MRC 环境下,block 截获外部用 __block 修饰的变量,不会增加对象的引用计数
- ARC 环境下,block 截获外部用 __block 修饰的变量,会增加对象的引用计数
所以,在 MRC 环境下,可以通过 __block 来打破循环引用,在 ARC 环境下,则需要用 __weak 来打破循环引用。
将 block 从栈区拷贝到堆区,需要调用 block 的 copy 方法,copy 方法实际上调用的是 Block_object_assign 方法,我们这里看一下底层实现:(这里主要是通过 block 的 flag 来确定是否增加对象的引用计数。)
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void **dest = (const void **)destArg;
switch ((flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_OBJECT:
/*******
id object = ...;
[^{ object; } copy];
********/
_Block_retain_object(object); //object 引用计数+1
*dest = object; //赋值
break;
case BLOCK_FIELD_IS_BLOCK:
/*******
void (^object)(void) = ...;
[^{ object; } 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];
********/
*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];
********/
/// 在mrc的情况下,你对对象添加__block, 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];
********/
*dest = object;
break;
default:
break;
}
}
在 _Block_object_assign 中,是通过 flags 来对 object 对象来执行不同的操作,在 MRC 环境下,会走 case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
这个 case,只会引用 object 这个对象,并不会对 object 进行 _Block_retain_object 来增加引用计数。
总结
通过上面两点,我们可以知道 __block 在 ARC 和 MRC 环境下是有区别的:在 MRC 环境下,可以通过 __block 来打破循环引用,在 ARC 环境下,则需要用 __weak 来打破循环引用。
参考文献
04·iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的?
20·iOS 面试题·请解释以下 keywords 的区别: assign vs weak, __block vs __weak。