资源准备
Block 的类型介绍
Block分为三种类型,分别是:GlobalBlock、MallocBlock、StackBlock:
-
GlobalBlock:-
位于全局区
-
在
Block内部不能捕获外部变量,或只使用静态变量或全局变量
-
-
MallocBlock-
位于堆区
-
在
Block内部使用局部变量或OC属性,并且赋值给强引用或Copy修饰的变量
-
-
StackBlock-
位于栈区
-
与
MallocBlock一样,可以在内部使用局部变量或OC属性。但不能赋值给强引用或Copy修饰的变量
-
GlobalBlock
typedef void(^KCBlock)(id data);
全局Block,不能捕获外部变量,但可以使用静态变量或全局变量:
- 使用静态变量、全局变量、
Block内部声明的变量,都没有问题。
MallocBlock
堆区Block,内部使用局部变量或OC属性,并且赋值给强引用或Copy修饰的变量:
-
对外部的
a变量进行捕获; -
赋值给强引用的
block; -
其中
block持有的是堆区Block的内存地址。
StackBlock
栈区Block,和堆区Block的使用大致相同,区别在于不能赋值给强引用或Copy修饰的变量:
- 同样对外部的
number变量进行捕获 - 和堆区
Block的区别:赋值给弱引用的block
Block 案例
Block的内存拷贝
NSObject+Block.hNSObject+Block.m
然后在控制器里面实现相关案例:
代码解读:
-
第一步,
weakBlock为栈区Block; -
第二步,按
Block的底层源码,自定义_LGBlock结构体。只要内存结构一致,即可将Block桥接为自定义对象; -
第三步,
Block的本质是结构体,将结构体首地址赋值给__strong修饰的对象; -
第四步,将结构体的
invoke置空,即:Block的函数指针; -
第五步,将
strongBlock赋值给强引用的strongBlock1,然后对其进行调用。
闪退原因:
-
在第三步中,将结构体首地址赋值给对象,二者指向相同内存空间;
-
在第四步中,将结构体的
invoke置空,修改的是同一片内存空间; -
在第五步中,将
invoke置空后的Block赋值给strongBlock1,调用时坏地址访问,程序闪退。
修改案例:
先将栈区Block赋值给强引用的Block,然后将invoke置空:
但依然闪退.
闪退原因:Block的赋值只是进行了浅拷贝,相当于对指向对象的指针进行复制,产生一个新的指向对象的指针,但两个指针依然指向同一个对象
所以,解决办法,必须在invoke置空之前,将Block进行深拷贝:
使用lldb,观察两个Block在invoke置空前后的变化:
- 深拷贝相当于将对象进行复制,产生一个新的对象。并且会递归复制每个指针类型的实例变量,直到两个对象没有任何公共的部分。
所以,栈区Block将invoke(指针)置空,并不影响堆区Block的调用。
对外部变量的引用计数处理
代码解读:
-
第一步,
objc初始化后的打印1,没有任何问题; -
第二步,在
strongBlock中打印3,拆分成两步进行分析:1、外部
objc变量,被栈区Block捕获,引用计数+1;2、栈区
Block赋值给强引用的strongBlock,将栈区Block拷贝到堆区,底层进行深拷贝,引用计数也会+1。 -
第三步,赋值给弱引用的
weakBlock,属于栈区Block,仅对外部objc变量进行捕获,引用计数+1。 -
第四步,将栈区
Block调用copy方法,赋值给mallocBlock,仅对栈区Block进行了深拷贝,引用计数+1。
其实第三步和第四步,等同于第二步的分解,所以打印结果:1、3、4、5。
堆栈Block的释放
代码解读:
-
第一步,
weakBlock使用__weak修饰,赋值为nil; -
第二步,定义代码块,实现一个堆区
Block; -
第三步,将堆区
Block赋值给代码块外面的weakBlock; -
第四步,在代码块执行完毕后,调用
weakBlock。
闪退原因:
-
在第三步中,堆区
Block赋值__weak修饰的weakBlock,相当于映射关系; -
在第四步中,当代码块执行完毕,
strongBlock由于是堆区Block,出了代码块就会被释放。作为映射的weakBlock,自然也会被置为nil。此时对其进行调用,出现坏地址访问,程序闪退。
修改案例:
- 将
strongBlock使用__weak修饰,即可正常打印。
当strongBlock使用__weak修饰后,成为栈区Block。将其赋值给__weak修饰的weakBlock,此时依然是栈区Block。栈区Block的生命周期与代码块无关,依赖于函数栈帧,所以可以正常打印。
Block 拷⻉到堆区
如果Block为全局Block,使用任何方式都不会拷贝到堆区,即使手动copy也没用,它依然是全局Block:
除此之外,以下四种操作,系统会将Block复制到堆上:
-
手动
Copy; -
Block作为返回值; -
被强引用或
Copy修饰; -
系统
API包含usingBlock。
Block作为返回值
由于栈区Block所属的变量域一旦结束,那么该Block就会被销毁。在ARC环境下,编译器会自动的判断,把Block自动的从栈区copy到堆区。例如:当Block作为函数返回值的时候,肯定会copy到堆区。
系统API包含usingBlock
当Block为函数参数时,需要将其手动copy到堆区。但系统API我们不需要处理,比如GCD中携带的usingBlock方法。其他自定义方法传递Block为参数时,都需要进行手动copy。
循环引用
对比以下两种Block代码:
-
Block1会出现循环引用,因为block被self持有,而block中使用self,所以self又被block持有。这种相互持有的情况下,就会出现循环引用。 -
Block2不会出现循环引用,因为block的持有者是UIView,和self无关,不会出现相互持有的情况,所以不会循环引用。
对象正常释放的过程:
- 当
A持有B,B的retainCount进行+1; - 当
A触发dealloc时,会给B放信号,B的retainCount进行-1。此时B的retainCount如果为0,就会调用dealloc,正常释放。
对象循环引用的过程:
-
当
A和B相互持有时,A的dealloc无法触发,因为A要等B放信号才能对retainCount进行-1; -
但是
B的dealloc也无法触发,因为B也在等待A的信号。此时A和B都在等待对方的释放,最终出现循环引用。
避免循环引用的方式:
-
weak-strong-dance; -
Block中强行切断持有者; -
将持有者作为
Block参数进行传递和使用。
weak-strong-dance
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;
NSLog(@"%@",strongSelf.name);
};
-
将
self赋值给__weak修饰的weakSelf,此时weakSelf属于self的映射,指向同一片内存空间,并且self的引用计数不会发生变化; -
在
Block中将weakSelf赋值给__strong修饰的strongSelf,避免self提前释放导致访问为nil的情况; -
因为
strongSelf为临时变量,在Block作用域结束后,即可自动释放,因此不会循环引用。
Block中强行切断持有者
__block ViewController *vc = self;
self.block = ^{
NSLog(@"%@",vc.name);
vc = nil;
};
self.block();
-
使用
__block修饰对象,否则vc无法改变,也就是说无法置为nil; -
在
Block中使用结束,手动将对象置为nil。相当于手动切断持有关系,可以避免循环引用; -
缺陷:这种方式
Block必须调用,否则将无法手动切断持有关系,self和block都无法释放,最终出现循环引用。
将持有者作为Block参数进行传递和使用
self.vcBlock = ^(ViewController *vc){
NSLog(@"%@",vc.name);
};
self.vcBlock(self);
-
将
self作为参数,提供给Block内部使用。当Block执行结束,vc会自动释放,然后相互持有关系的切断,self也会释放; -
这种方式,
self的是否依赖于Block的执行结束。如果Block中有延迟执行的代码,self的释放也会延迟。
面试题
以下几个案例,是否会出现循环引用?
案例1:
static ViewController *staticSelf_;
- (void)blockWeak_static {
__weak typeof(self) weakSelf = self;
staticSelf_ = weakSelf;
}
[self blockWeak_static];
-
会出现循环引用;
-
将
self赋值__weak修饰的对象,它们属于映射关系,指向同一片内存空间。当weakSelf赋值全局静态变量,staticSelf_在程序运行过程中不会主动释放,它会持续持有self,所以self也无法释放。
案例2:
- (void)block_weak_strong {
__weak typeof(self) weakSelf = self;
self.doWork = ^{
__strong typeof(self) strongSelf = weakSelf;
weakSelf.doStudent = ^{
NSLog(@"%@", strongSelf);
};
weakSelf.doStudent();
};
self.doWork();
}
[self block_weak_strong];
-
会出现循环引用;
-
在
doWork内部,strongSelf持有的是self。虽然strongSelf是临时变量,但在doStudent中又被持有,导致引用计数+1。在doWork执行完毕后引用计数-1,但doStudent中的持有还存在,所以会出现循环引用。
底层 cpp 分析
Block本质
创建block.c文件,写入以下代码:
#include "stdio.h"
int main() {
void(^block)(void) = ^{
printf("LG");
};
block();
return 0;
}
使用xcrun命令,生成block.cpp文件,底层 C++ 文件。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc block.c -o block.cpp
打开block.cpp文件,找到main函数:
为了方便阅读,剔除强转代码
int main() {
void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
block->FuncPtr(block);
return 0;
}
-
只生成两行代码:
-
调用
__main_block_impl_0函数,传入两个参数,取地址并赋值block; -
调用
block的FuncPtr函数,传入block。
-
找到__main_block_impl_0的定义:
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;
}
}
参数 1
1static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("LG");
}
参数 2
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)};
-
不难看出,
Block的本质是结构体,main函数中调用的是结构体的构造函数。 -
结构体中包含两个成员变量:
-
__block_impl结构体类型的impl; -
__main_block_desc_0结构体指针类型的Desc。
-
-
构造函数中生成了
flags等于0的默认值,赋值impl的Flags。 -
参数
fp为Block代码块的函数指针,赋值impl的FuncPtr。 -
参数
desc赋值给成员变量Desc。 -
所以
main函数中,代码block->FuncPtr(block)就是在对Block进行调用。 -
由此可见,当
Block仅定义不调用执行,不会触发Block中的代码块。
捕获外界变量
代码如下:
生成block.cpp文件,找到main函数:
int main(){
int a = 8;
void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a);
block->FuncPtr(block);
return 0;
}
-
代码发生了改变,之前
__main_block_impl_0函数的入参变成三个; -
增加的第三个参数为外界变量
a。
找到__main_block_impl_0的定义:
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;
}
};
参数 1:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
printf("LG - %d",a);
}
参数 2:
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)};
-
结构体中的成员变量也发生了变化,当捕获外界变量时,在结构体内部,会生成相应的成员变量用来存储;
-
成员变量
a通过结构体的构造函数赋值:a(_a); -
main函数中调用Bolck,由于捕获外界变量,此时传入的FuncPtr中的block发挥作用:-
block为自身结构体的指针,将block中的成员变量a赋值给临时变量a,然后对其打印; -
临时变量
a和__cself->a的值相同,但地址不同; -
由于
a是值拷贝,Bolck的代码块中不能对a的值进行改变,会造成编译器的代码歧义。所以,此时的a是只读的。
-
-
捕获外界变量并赋值强引用变量,本该是堆区
Block,但结构体中impl的isa赋值为&_NSConcreteStackBlock,标记为栈区Block。因为在编译时,无法开辟内存空间,所以暂且标记为StackBlock。在运行时,会根据情况将Block拷贝到堆区,然后生成MallocBlock。
__block的作用
代码如下:
#include "stdio.h"
int main() {
__block int a = 18;
void(^block)(void) = ^{
a++;
printf("LG - %d",a);
};
block();
return 0;
}
生成block.cpp文件,找到main函数:
int main() {
__Block_byref_a_0 a = {
0,
(__Block_byref_a_0 *)&a,
0,
sizeof(__Block_byref_a_0),
18
};
void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344);
block->FuncPtr(block);
return 0;
}
int类型a,对应生成__Block_byref_a_0结构体。成员变量2,对结构体a取地址,转为结构体指针。
找到__Block_byref_a_0定义:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
-
其中
__forwarding存储的就是a结构体的地址; -
最后的成员变量
a存储18的值; -
结构体中存储了自身的地址和值。
找到__main_block_impl_0的定义:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __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;
}
};
参数 1:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a)++;
printf("LG - %d",(a->__forwarding->a));
}
参数 2 :
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};
//参数2使用的copy和dispose函数
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
-
局部变量
a和__cself->a指针地址相同,它的值一旦改变,相当于对外界变量的值进行修改; -
没有
__block修饰,属于值拷贝,也就是深拷贝。拷贝的值不可更改,它们指向不同的内存空间; -
使用
__block修饰,属于地址拷贝,也就是浅拷贝。生成的对象指向同一片内存空间,内部修改等同于对外界变量的修改。
汇编分析
流程分析
搭建App项目,写入以下代码:
- (void)viewDidLoad {
[super viewDidLoad];
__block NSObject *objc = [NSObject alloc];
void (^block)(void) = ^{
NSLog(@"LG_Block %@ ",objc);
};
block();
}
针对block的定义设置断点,运行项目,查看汇编代码:
在项目中,设置objc_retainBlock符号断点:
- 来自
libobjc.A.dylib框架。
打开objc4-818.2源码,找到objc_retainBlock函数:
id objc_retainBlock(id x) {
return (id)_Block_copy(x);
}
- 调用
_Block_copy函数,但是objc源码中找不到它的实现
在项目中,设置_Block_copy符号断点:
来自libsystem_blocks.dylib框架,但该框架暂未开源,可以在libclosure替代工程中查看。
Block结构
通过cpp分析,Block的本质是结构体。在libclosure-79源码中,来到_Block_copy函数,可以找到Block的真实类型:Block_layout。
找到Block_layout的定义:
-
isa:标示Block类型的类; -
flags:标识符,按bit位表示Block的附加信息,类似于isa中的位域; -
reserved:预留位置; -
invoke:函数指针,指向Block实现的调用地址; -
descriptor:附加信息,例如:存储保留变量数、Block的大小、进行copy或dispose的函数指针。
找到flags的标示:
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_INLINE_LAYOUT_STRING = (1 << 21), // compiler
#if BLOCK_SMALL_DESCRIPTOR_SUPPORTED
BLOCK_SMALL_DESCRIPTOR = (1 << 22), // compiler
#endif
BLOCK_IS_NOESCAPE = (1 << 23), // compiler
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_DEALLOCATING:释放标记,一般常用于BLOCK_BYREF_NEEDS_FREE做位与运算,一同传入flags,告知该Block可释放; -
BLOCK_REFCOUNT_MASK:存储引用引用计数的值,是一个可选用参数; -
BLOCK_NEEDS_FREE:低16位是否有效的标志,程序根据它来决定是否增加或者减少引用计数位的值; -
BLOCK_HAS_COPY_DISPOSE:是否拥有拷贝辅助函数,用于拷贝到堆区,决定block_description_2; -
BLOCK_HAS_CTOR:是否拥有Block的C++析构函数; -
BLOCK_IS_GC:标志是否有垃圾回收,OSX; -
BLOCK_IS_GLOBAL:标志是否是全局Block; -
BLOCK_USE_STRET:与BLOCK_HAS_SIGNATURE相对,判断是否当前Block拥有一个签名,用于runtime时动态调用; -
BLOCK_HAS_SIGNATURE:是否有签名; -
BLOCK_HAS_EXTENDED_LAYOUT:是否有拓展,决定block_description_3。
运行时Copy
进入_Block_copy函数的汇编代码,读取 x0 寄存器的值,并对其进行打印:
- 进入
_Block_copy函数,当前Block标记为StackBlock。
直接运行到函数结尾,读取返回值x0寄存器,并对其进行打印:
- 程序运行时, 当前
Block符合MallocBlock的条件,经过_Block_copy函数,会将Block复制到堆区。
Block调用
_Block_copy函数执行完毕,回到viewDidLoad方法,继续进行Block的调用:
-
ldr x8, [x0, #0x10]:x0为当前Block,读取x0 + 16字节的值,赋值给x8; -
Block_layout结构体中,首地址偏移16字节,相当于跳过isa、flags和reserved,读取到invoke函数地址,赋值给x8;
-
blr x8:跳转到invoke函数地址,相当于Block的调用。
descriptor
当_Block_copy函数执行完毕,打印当前Block为MallocBlock:
- 同时还打印出
signature、copy、dispose等数据。
descriptor类型
signature、copy、dispose等数据,存储在Block_layout结构体的descriptor中
descriptor分为三种类型:
-
Block_descriptor_1 -
Block_descriptor_2 -
Block_descriptor_3
其中Block_descriptor_1一定存在,Block_descriptor_2和Block_descriptor_3为可选
找到descriptor的定义:
Block_descriptor_1:存储预留字段和Block大小
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
Block_descriptor_2:存储copy和dispose的函数指针
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
BlockCopyFunction copy;
BlockDisposeFunction dispose;
};
Block_descriptor_3:存储signature签名和layout
#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
Block_descriptor_1的读取:
static struct Block_descriptor_1 * _Block_descriptor_1(struct Block_layout *aBlock) {
return aBlock->descriptor;
}
- 由于
Block_descriptor_1一定存在,直接通过Block的descriptor读取。
Block_descriptor_2的读取:
static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock) {
if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
uint8_t *desc = (uint8_t *)aBlock->descriptor;
desc += sizeof(struct Block_descriptor_1);
return (struct Block_descriptor_2 *)desc;
}
-
通过位与运算,判断
Block_descriptor_2不存在,返回NULL; -
否则,读取
Block_descriptor_1的首地址,偏移自身大小,即:Block_descriptor_2的首地址; -
强转为
Block_descriptor_2的结构体指针返回。
Block_descriptor_3的读取:
static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock) {
if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
uint8_t *desc = (uint8_t *)aBlock->descriptor;
desc += sizeof(struct Block_descriptor_1);
if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
desc += sizeof(struct Block_descriptor_2);
}
return (struct Block_descriptor_3 *)desc;
}
-
通过位与运算,判断
Block_descriptor_3不存在,返回NULL; -
否则,读取
Block_descriptor_1的首地址,偏移自身大小,即:Block_descriptor_2的首地址; -
通过位与运算,判断
Block_descriptor_2是否存在:-
存在,再偏移
Block_descriptor_2大小,即:Block_descriptor_3的首地址; -
不存在,强转为
Block_descriptor_3的结构体指针返回。
-
因为Block_descriptor_2和Block_descriptor_3的内存结构相同,所以偏移Block_descriptor_1之后的地址,即可强转为2,可强转为3。
lldb验证descriptor
通过flags进行位与运算,可以得知Block_descriptor_2和Block_descriptor_3是否存在。
使用x/8g命令,输出Block的内存结构:
(lldb) x/8g 0x283aaa430
0x283aaa430: 0x00000001de1c0880 0x00000000c3000002
0x283aaa440: 0x00000001021422ac 0x0000000102144018
0x283aaa450: 0x0000000283aaa400 0x0000000000000000
0x283aaa460: 0x000021a1de1c6221 0x0000000000000000
0xc3000002:Block的flags标识符。
验证Block_descriptor_2:
//BLOCK_HAS_COPY_DISPOSE = (1 << 25)
(lldb) p/x 1 << 25
(int) $7 = 0x02000000
(lldb) p/x (0xc3000002 & 0x02000000)
(unsigned int) $8 = 0x02000000
- 运算结果不为
0,证明Block_descriptor_2存在。
验证Block_descriptor_3:
//BLOCK_HAS_SIGNATURE = (1 << 30)
(lldb) p/x 1 << 30
(int) $10 = 0x40000000
(lldb) p/x (0xc3000002 & 0x40000000)
(unsigned int) $16 = 0x40000000
- 运算结果不为
0,证明Block_descriptor_3存在。
Block签名
lldb验证签名
使用x/8g命令,输出Block的内存结构:
(lldb) x/8g 0x283aaa430
0x283aaa430: 0x00000001de1c0880 0x00000000c3000002
0x283aaa440: 0x00000001021422ac 0x0000000102144018
0x283aaa450: 0x0000000283aaa400 0x0000000000000000
0x283aaa460: 0x000021a1de1c6221 0x0000000000000000
- 首地址平移
24字节,0x0000000102144018就是descriptor的结构体指针。
使用x/8g命令,输出descriptor的内存结构:
(lldb) x/8g 0x0000000102144018
0x102144018: 0x0000000000000000 0x0000000000000028
0x102144028: 0x00000001021422e0 0x00000001021422f0
0x102144038: 0x00000001021433e3 0x0000000000000010
0x102144048: 0x00000001de78a280 0x00000000000007c8
-
0x00000001021422e0:copy函数指针; -
0x00000001021422f0:dispose函数指针; -
0x00000001021433e3:signature签名。
对0x00000001021433e3进行强转输出:
(lldb) po (char *)0x00000001021433e3
"v8@?0"
签名含义
对于签名v8@?0的解释:
-
v:返回值类型viod -
8:方法所占用的内存8字节 -
@?:参数0类型 -
0:参数0的起始位置,从0字节开始
签名的详细信息,可以使用NSMethodSignature的signatureWithObjCTypes方法输出:
(lldb) po [NSMethodSignature signatureWithObjCTypes:"v8@?0"] <NSMethodSignature: 0xbb2001894a750adf>
number of arguments = 1
frame size = 224
is special struct return? NO
return value: -------- -------- -------- --------
type encoding (v) 'v'
flags {}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
memory {offset = 0, size = 0}
argument 0: -------- -------- -------- --------
type encoding (@) '@?'
flags {isObject, isBlock}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8}
-
number of arguments = 1:表示传入一个参数; -
is special struct return? NO:没有返回值; -
return value:返回值:-
type encoding (v) 'v':void类型的缩写; -
memory {offset = 0, size = 0}:无返回值,故此size为0。
-
-
argument 0:参数0; -
type encoding (@) '@?':其中@为id类型,?未知类型; -
flags {isObject, isBlock}:即是Object类型,也是Block类型; -
memory {offset = 0, size = 8}:参数0从0字节开始,占8字节。
源码分析
使用替代工程libclosure进行源码分析:
当Block捕获使用__block修饰的对象,底层会触发Block的三层拷贝。
_Block_copy
进入_Block_copy函数:
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
// 如果 arg 为 NULL,直接返回 NULL
if (!arg) return NULL;
// The following would be better done as a switch statement
// 强转为 Block_layout 类型
aBlock = (struct Block_layout *)arg;
// 如果现在已经在堆上
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
// 就只将引用计数加 1
latching_incr_int(&aBlock->flags);
return aBlock;
}
// 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
else {// 栈 - 堆 (编译期)
// Its a stack block. Make a copy.
// block 现在在栈上,现在需要将其拷贝到堆上
// 在堆上重新开辟一块和 aBlock 相同大小的内存
size_t size = Block_size(aBlock);
struct Block_layout *result = (struct Block_layout *)malloc(size);
// 开辟失败,返回 NULL
if (!result) return NULL;
// 将 aBlock 内存上的数据全部复制新开辟的 result 上
memmove(result, aBlock, size); // bitcopy first
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
result->invoke = aBlock->invoke;
#if __has_feature(ptrauth_signed_block_descriptors)
if (aBlock->flags & BLOCK_SMALL_DESCRIPTOR) {
uintptr_t oldDesc = ptrauth_blend_discriminator( &aBlock->descriptor, _Block_descriptor_ptrauth_discriminator);
uintptr_t newDesc = ptrauth_blend_discriminator( &result->descriptor, _Block_descriptor_ptrauth_discriminator);
result->descriptor = ptrauth_auth_and_resign(aBlock->descriptor, ptrauth_key_asda, oldDesc, ptrauth_key_asda, newDesc);
}
#endif
#endif
// reset refcount
// 将 flags 中的 BLOCK_REFCOUNT_MASK 和 BLOCK_DEALLOCATING 部分的位全部清为 0
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
// 将 result 标记位在堆上,需要手动释放;并且引用计数初始化为 1
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
// copy 方法中会调用做拷贝成员变量的工作
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
// isa 指向 _NSConcreteMallocBlock
result->isa = _NSConcreteMallocBlock;
return result;
}
}
-
_Block_copy函数,负责Block对象的自身拷贝,从栈区拷贝到堆区; -
参数
arg就是Block_layout对象; -
如果原来就在堆上,就将引用计数
+1; -
如果
Block在全局区,不用加引用计数,也不用拷贝,直接返回Block本身; -
如果原来在栈上,会拷贝到堆上,引用计数初始化为
1,并且会调用_Block_call_copy_helper方法(如果存在的话); -
返回值是拷贝后
Block的地址。
_Block_object_assign
cpp分析
打开cpp文件,找到声明Block时的第二个参数,__main_block_desc_0_DATA结构体的定义:
//void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344);
__main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0),
__main_block_copy_0,
__main_block_dispose_0
};
-
__main_block_copy_0:Block_descriptor_2中的copy; -
__main_block_dispose_0:Block_descriptor_2中的dispose。
找到__main_block_copy_0的定义:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
- 调用
_Block_object_assign函数。
lldb分析
使用x/8g命令,输出Block的内存结构:
(lldb) x/8g 0x000000028167eca0
0x28167eca0: 0x00000001de1c0880 0x00000000c3000002
0x28167ecb0: 0x0000000104f1a2ac 0x0000000104f1c018
0x28167ecc0: 0x000000028167ec70 0x0000000000000000
0x28167ecd0: 0x000021a1de1c6221 0x0000000000000000
使用x/8g命令,输出descriptor的内存结构:
(lldb) x/8g 0x0000000104f1c018
0x104f1c018: 0x0000000000000000 0x0000000000000028
0x104f1c028: 0x0000000104f1a2e0 0x0000000104f1a2f0
0x104f1c038: 0x0000000104f1b3e3 0x0000000000000010
0x104f1c048: 0x00000001de78a280 0x00000000000007c8
0x0000000104f1a2e0:copy函数指针
使用dis -s读取汇编代码:
(lldb) dis -s 0x0000000104f1a2e0
004-Block结构与签名`__copy_helper_block_e8_32r:
0x104f1a2e0 <+0>: add x0, x0, #0x20 ; =0x20
0x104f1a2e4 <+4>: ldr x1, [x1, #0x20]
0x104f1a2e8 <+8>: mov w2, #0x8
0x104f1a2ec <+12>: b 0x104f1a484 ; symbol stub for: _Block_object_assign
- 调用
_Block_object_assign函数。
调用时机
在_Block_copy函数中,调用_Block_call_copy_helper函数
static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock) {
if (auto *pFn = _Block_get_copy_function(aBlock)) pFn(result, aBlock);
}
进入_Block_get_copy_function函数
static inline __typeof__(void (*)(void *, const void *)) _Block_get_copy_function(struct Block_layout *aBlock) {
if (!(aBlock->flags & BLOCK_HAS_COPY_DISPOSE))
return NULL;
void *desc = _Block_get_descriptor(aBlock);
#if BLOCK_SMALL_DESCRIPTOR_SUPPORTED
if (aBlock->flags & BLOCK_SMALL_DESCRIPTOR) {
struct Block_descriptor_small *bds = (struct Block_descriptor_small *)desc;
return _Block_get_relative_function_pointer( bds->copy, void (*)(void *, const void *));
}
#endif
struct Block_descriptor_2 *bd2 = (struct Block_descriptor_2 *)((unsigned char *)desc + sizeof(struct Block_descriptor_1));
return _Block_get_copy_fn(bd2);
}
-
如果存在
copy和dispose,通过内存获取Block_descriptor_2的结构体指针; -
调用
_Block_get_copy_fn函数,传入bd2.
进入_Block_get_copy_fn函数:
static inline __typeof__(void (*)(void *, const void *)) _Block_get_copy_fn(struct Block_descriptor_2 *desc) {
return (void (*)(void *, const void *))_Block_get_function_pointer(desc->copy);
}
- 经过处理返回
Block_descriptor_2下的copy函数。
最终,回到_Block_call_copy_helper函数,将copy函数地址赋值pFn,然后通过pFn(result, aBlock)对其进行调用。
源码分析
在源码中,找到Block捕获外界变量的种类
enum {
// see function implementation for a more complete description of these fields and combinations
BLOCK_FIELD_IS_OBJECT = 3, // id, NSObject, __attribute__((NSObject)), block, ...
BLOCK_FIELD_IS_BLOCK = 7, // a block variable
BLOCK_FIELD_IS_BYREF = 8, // the on stack structure holding the __block variable
BLOCK_FIELD_IS_WEAK = 16, // declared __weak, only used in byref copy helpers
BLOCK_BYREF_CALLER = 128, // called from __block (byref) copy/dispose support routines.
};
-
BLOCK_FIELD_IS_OBJECT:普通对象类型; -
BLOCK_FIELD_IS_BLOCK:Block类型作为变量; -
BLOCK_FIELD_IS_BYREF:使用__block修饰的变量; -
BLOCK_FIELD_IS_WEAK:weak弱引用变量; -
BLOCK_BYREF_CALLER:返回的调用对象,处理block_byref内部对象内存会加的一个额外标记,配合flags一起使用。
进入_Block_object_assign函数:
// 当 block 和 byref 要持有对象时,它们的 copy helper 函数会调用这个函数来完成 assignment
// 参数 destAddr 其实是一个二级指针,指向真正的目标指针
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_default = fn (arc)
// 默认什么都不干,但在 _Block_use_RR() 中会被 Objc runtime 或者 CoreFoundation 设置 retain 函数
// 其中,可能会与 runtime 建立联系,操作对象的引用计数什么的
// 可以理解为交给系统 ARC 处理
_Block_retain_object(object);
// 使 dest 指向的目标指针指向
object *dest = object;
break;
case BLOCK_FIELD_IS_BLOCK:
/*******
void (^object)(void) = ...;
[^{ object; } copy];
********/
// 使 dest 指向拷贝到堆上object
*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 指向拷贝到堆上的byref
*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];
********/
// 使 dest 指向的目标指针指向 object
*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
*dest = object;
break;
default:
break;
}
}
-
普通对象类型,交给系统
ARC处理,使dest指向的目标指针指向object; -
Block类型作为变量,调用_Block_copy函数,使dest指向拷贝到堆上object; -
使用
__block修饰的变量,调用_Block_byref_copy函数,使dest指向拷贝到堆上的byref。
_Block_byref_copy
进入_Block_byref_copy函数:
// 1. 如果 byref 原来在堆上,就将其拷贝到堆上,拷贝的包括 Block_byref、Block_byref_2、Block_byref_3
// 被 __weak 修饰的 byref 会被修改 isa 为 _NSConcreteWeakBlockVariable
// 原来 byref 的 forwarding 也会指向堆上的 byref;
// 2. 如果 byref 已经在堆上,就只增加一个引用计数。
static struct Block_byref *_Block_byref_copy(const void *arg) {
// arg 强转为 Block_byref * 类型
struct Block_byref *src = (struct Block_byref *)arg;
// 引用计数等于 0
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack
// 为新的 byref 在堆中分配内存
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
// 新 byref 的 flags 中标记了它是在堆上,且引用计数为 2。
// 为什么是 2 呢?注释说的是 non-GC one for caller, one for stack
// one for caller 很好理解,那 one for stack 是为什么呢?
// 看下面的代码中有一行 src->forwarding = copy。src 的 forwarding 也指向了 copy,相当于引用了
copy copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
// 堆上 byref 的 forwarding 指向自己
copy->forwarding = copy; // patch heap copy to point to itself
// 原来栈上的 byref 的 forwarding 现在也指向堆上的 byref
src->forwarding = copy; // patch stack to point to heap copy
// 拷贝 size
copy->size = src->size;
// 如果 src 有 copy/dispose helper
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
// 取得 src 和 copy 的 Block_byref_2
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
// copy 的 copy/dispose helper 也与 src 保持一致
// 因为是函数指针,估计也不是在栈上,所以不用担心被销毁
copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;
// 如果 src 有扩展布局,也拷贝扩展布局
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);
// 没有将 layout 字符串拷贝到堆上,是因为它是 const 常量,不在栈上
copy3->layout = src3->layout;
}
// 调用 copy helper,因为 src 和 copy 的 copy helper 是一样的,所以用谁的都行,调用的都是同一个函数
// 发起第三层拷贝
(*src2->byref_keep)(copy, src);
} else {
// Bitwise copy.
// This copy includes Block_byref_3, if any.
// 如果 src 没有 copy/dispose helper
// 将 Block_byref 后面的数据都拷贝到 copy 中,一定包括 Block_byref_3
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
// already copied to heap
// src 已经在堆上,就只将引用计数加 1
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}
-
对
Block_byref进行拷贝,属于三层拷贝中的第二层; -
如果引用计数为
0,为新的byref在堆中分配内存:-
将堆上
byref的forwarding指向自己; -
将原来栈上的
byref的forwarding也指向堆上的byref; -
由
byref_keep发起Block的第三层拷贝。
-
-
如果已经在堆上,就只将引用计数
+1;
byref_keep分析
找到byref_keep的定义:
struct Block_byref {
void *isa;
struct Block_byref *forwarding;
volatile int32_t flags; // contains ref count
uint32_t size;
};
struct Block_byref_2 {
// requires BLOCK_BYREF_HAS_COPY_DISPOSE
BlockByrefKeepFunction byref_keep;
BlockByrefDestroyFunction byref_destroy;
};
struct Block_byref_3 {
// requires BLOCK_BYREF_LAYOUT_EXTENDED
const char *layout;
};
对应cpp文件查看:
struct __Block_byref_objc_0 {
void *__isa;
__Block_byref_objc_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *objc;
};
- 源码中的
byref_keep,对应cpp中的__Block_byref_id_object_copy。
找到__Block_byref_id_object_copy的赋值:
__Block_byref_objc_0 objc = {
(void*)0,
(__Block_byref_objc_0 *)&objc,
33554432,
sizeof(__Block_byref_objc_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc"))
};
- 传入
__Block_byref_id_object_copy_131。
找到__Block_byref_id_object_copy_131的定义:
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
对应源码查看传入的参数:
(*src2->byref_keep)(copy, src);
- 传入堆上的
Block_byref,对应cpp中的__Block_byref_objc_0结构体。
byref_keep的结论
调用byref_keep,底层又调用一次_Block_object_assign函数
(char*)dst + 40参数,结构体内存平移,传入的就是objc实例对象
进入_Block_object_assign函数,命中普通对象类型的copy逻辑
将对象拷贝到堆区,完成Block的第三层拷贝
Block三层拷贝的结论
当Block捕获使用__block修饰的对象,底层会触发Block的三层拷贝
-
【第一层】
_Block_copy函数,负责Block对象的自身拷贝,从栈区拷贝到堆区-
通过
_Block_copy→_Block_call_copy_helper,调用_Block_object_assign函数 -
传入
Block_byref结构体指针,类型为BLOCK_FIELD_IS_BYREF,调用_Block_byref_copy函数
-
-
【第二层】
_Block_byref_copy函数,将Block_byref拷贝到堆区- 通过
byref_keep函数,调用_Block_object_assign函数,传入objc实例对象
- 通过
-
【第三层】
_Block_object_assign函数,将捕获的外界变量拷贝到堆区
Block释放
如果Block在堆上,需要进行release。在全局区和栈区的Block,都不需要release。
_Block_release
进入_Block_release函数:
// block 在堆上,才需要 release,在全局区和栈区都不需要 release.
// 先将引用计数减 1,如果引用计数减到了 0,就将 block 销毁
void _Block_release(const void *arg) {
struct Block_layout *aBlock = (struct Block_layout *)arg;
// 如果 block == nil
if (!aBlock) return;
// 如果 block 在全局区
if (aBlock->flags & BLOCK_IS_GLOBAL) return;
// block 不在堆上
if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;
// 引用计数减 1,如果引用计数减到了 0,会返回 true,表示 block 需要被销毁
if (latching_decr_int_should_deallocate(&aBlock->flags)) {
// 调用 block 的 dispose helper,dispose helper 方法中会做诸如销毁 byref 等操作
_Block_call_dispose_helper(aBlock);
// _Block_destructInstance 啥也不干,函数体是空的
_Block_destructInstance(aBlock); free(aBlock);
}
}
- 和
_Block_copy相似,通过_Block_call_dispose_helper函数,调用_Block_object_dispose函数。
_Block_object_dispose
进入_Block_object_dispose函数:
// 当 block 和 byref 要 dispose 对象时,它们的 dispose helper 会调用这个函数
void _Block_object_dispose(const void *object, const int flags) {
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
// 如果是 byref
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
// get rid of the __block data structure held in a Block
// 对 byref 对象做 release 操作
_Block_byref_release(object);
break;
case BLOCK_FIELD_IS_BLOCK:
// 对 block 做 release 操作
_Block_release(object);
break;
case BLOCK_FIELD_IS_OBJECT:
// 默认啥也不干,但在 _Block_use_RR() 中可能会被 Objc runtime 或者 CoreFoundation 设置一个 release 函数,里面可能会涉及到 runtime 的引用计数
_Block_release_object(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
break;
default:
break;
}
}
-
普通对象类型,交给系统
ARC处理; -
Block类型作为变量,调用_Block_release函数; -
使用
__block修饰的变量,调用_Block_byref_release函数,对byref对象做release操作。
_Block_byref_release
进入_Block_byref_release函数:
// 对 byref 对象做 release 操作,
// 堆上的 byref 需要 release,栈上的不需要 release,
// release 就是引用计数减 1,如果引用计数减到了 0,就将 byref 对象销毁
static void _Block_byref_release(const void *arg) {
struct Block_byref *byref = (struct Block_byref *)arg;
// dereference the forwarding pointer since the compiler isn't doing this anymore (ever?)
// 取得真正指向的 byref,如果 byref 已经被堆拷贝,则取得是堆上的 byref,否则是栈上的,栈上的不需要 release,也没有引用计数
byref = byref->forwarding;
// byref 被拷贝到堆上,需要 release
if (byref->flags & BLOCK_BYREF_NEEDS_FREE) {
// 取得引用计数
int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK;
os_assert(refcount);
// 引用计数减 1,如果引用计数减到了 0,会返回 true,表示 byref 需要被销毁
if (latching_decr_int_should_deallocate(&byref->flags)) {
if (byref->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
// dispose helper 藏在 Block_byref_2 里
(*byref2->byref_destroy)(byref);
}
free(byref);
}
}
}
- 和
_Block_byref_copy相似,由byref_destroy发起对象的release。
对应cpp代码:
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
-
调用
_Block_object_dispose函数,传入objc实例对象。 -
进入
_Block_object_dispose函数,命中普通对象类型的release逻辑。