block 实现原理
block 源码剖析
在main函数中实现一个简单的block调用
int main(){
int num = 0;
void (^block)(void) = ^{
NSLog(@"num: %d", num);
};
block();
return 0;
}
复制代码
使用命令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
将 main.m 转换成 main.cpp 文件查看block的源码实现
下面的是main.cpp文件中与block实现有关的c++代码
// block 对象类
struct __block_impl {
void *isa; //isa 指针 指向block的具体的类型
int Flags;
int Reserved;
void *FuncPtr; //函数指针
};
// 把具体的block对象和捕获到的上下文变量 包装成一个对象
struct __main_block_impl_0 {
struct __block_impl impl; //具体的block实现
struct __main_block_desc_0* Desc;
int num; //从上下文中捕获的变量
// 构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock; //block的具体类型
impl.Flags = flags;
impl.FuncPtr = fp; //函数指针
Desc = desc;
}
};
//block 函数实现
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int num = __cself->num; // bound by copy
NSLog((NSString*)&__NSConstantStringImpl__var_folders_z6_mpw8gfyn44vft3h3h82bz3hc0000gn_T_main_ad7ede_mi_0, num);
}
//block 描述信息
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size; //block的所需的内存空间
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
//main 函数
int main(){
int num = 0;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, num));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
复制代码
重点分析
观察上面的代码 我们可以看到几个重要的对象以及它们内部几个重要成员:
__block_impl
-
isa指针 : 观察 __block_impl 内部结构 可以看到第一个就是isa指针,它和oc对象中的isa作用一样,block中的isa也指向其具体的类型。之所以认为block实际是一个对象就是因为其内部包含了一个isa指针。isa指针指向的具体类型我们会在下面分析。
-
FuncPtr指针 : 指向block中具体的函数实现。
__main_block_impl_0
- __block_impl对象 : 具体block对象
- __main_block_desc_0 : 描述对象 包含__main_block_desc_0在内存中的大小信息
- num : 捕获到的外部变量num
分析: __main_block_impl_0 的内部结构,发现 __main_block_impl_0 对象的作用实际上就是把具体的block对象和捕获到的上下文环境变量进行了封装,以便于block的调用访问捕获的变量。
我们其实可以把 __main_block_impl_0 看成是 __block_impl 子类。至于为什么可以这么看,我会在下面进行说明。
main函数分析
在上面的main函数中我们做了两个工作一个是创建block一个是调用block。
分析main函数的实现部分,可以看出我们创建block的时候,实际是建一个 __main_block_impl_0 结构体对象。
在我们调用block的过程实际是通过调用 __block_impl 对象中的函数指针 并把 __main_block_impl_0 对象作为函数的参数,传入到该函数中。
观察block的调用过程
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
复制代码
从中可以看出,调用过程会将block对象转成__block_impl类型的对象然后调用__block_impl对象中的函数指针,把block对像传进去。而这个过程是不是很像子类调用父类的方法,所以我们其实可以把 __main_block_impl_0 看成是 __block_impl 子类。
这里有个疑问,为什么block对象能够访问到 __block_impl 对象中的函数指针,因为实际上 __main_block_impl_0 斌不是 __block_impl 子类。它是如何做到的?
这是涉及对象内部在内存中的布局。比如一个数组:int list[10]
,我们是如何访问里面第一个和第十位置上的元素的?答案就是通过下标 list[0];list[9]
。可为什么下标能正确访问呢?是因为数组中元素在内存中的分布是连续的,我们通过偏移指针就能够正确访问到偏移位置上的数据。list[9]
实际就是对 list指针做了9个单位的偏移 然后访问他第10个元素。而第一个位置上的元素内存地址和数组的内容地址是一样的,我们可以直接通过 *list
拿到第1个位置上的内容。如同数组的分布,对象在内存中的分布也是如此。而且__block_impl成员是在__main_block_impl_0结构中的第一个位置,所以__main_block_impl_0指针和__block_impl指针指向的地址是一样的,这就为什么__main_block_impl_0地址能够正确访问到FuncPtr函数指针的原因。
三种 block 类型
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock; //block的具体类型
impl.Flags = flags;
impl.FuncPtr = fp; //函数指针
Desc = desc;
}
复制代码
查看__main_block_impl_0的构造函数,它将impl对象的isa指针指向了_NSConcreteStackBlock对象。这说这个block类型是一个栈类型的block。除了栈类型block还有全局类型 和 堆类型,这取决与isa指向什么类型的对象。
int main() {
//1.
void (^global_block)(void) = ^{
NSLog(@"global_block");
};
NSLog(@"global_block: %@ -> %@ -> %@ -> %@",[global_block class], [[global_block class] superclass], [[[global_block class] superclass] superclass], [[[[[global_block class] superclass] superclass] superclass] superclass]);
//2.
__block int age = 1;
void (^stack_block)(void) = ^{
NSLog(@"stack_block %d", age++);
};
NSLog(@"stack_block: %@ -> %@ -> %@ -> %@",[stack_block class], [[stack_block class] superclass], [[[stack_block class] superclass] superclass], [[[[[stack_block class] superclass] superclass] superclass] superclass]);
//3.
void (^malloc_block)(void) = [stack_block copy];
NSLog(@"malloc_block: %@ -> %@ -> %@ -> %@",[malloc_block class], [[malloc_block class] superclass], [[[malloc_block class] superclass] superclass], [[[[[malloc_block class] superclass] superclass] superclass] superclass]);
}
复制代码
输出结果
MRC输出日志:
global_block: __NSGlobalBlock__ -> __NSGlobalBlock -> NSBlock -> NSObject
stack_block: __NSStackBlock__ -> __NSStackBlock -> NSBlock -> NSObject
malloc_block: __NSMallocBlock__ -> __NSMallocBlock -> NSBlock -> NSObject
ARC输出日志:
global_block: __NSGlobalBlock__ -> __NSGlobalBlock -> NSBlock -> NSObject
stack_block: __NSMallocBlock__ -> __NSMallocBlock -> NSBlock -> NSObject
malloc_block: __NSMallocBlock__ -> __NSMallocBlock -> NSBlock -> NSObject
复制代码
block的类有三种,就是上面打印的结果:__NSGlobalBlock__、__NSStackBlock__、__NSMallocBlock__
。有人可能会疑惑,打印第2个block在ARC和MRC下的输出会不一样。这是因为在ARC模式下,如果一个__NSStackBlock__类型的block被一个强指针引用,那系统会自动对这个block进行一次copy操作将这个block变成__NSMallocBlock__类型,这样会影响运行的结果。
全局类型 (NSGlobalBlock)
如果一个block里面没有访问普通局部变量(也就是说block里面没有访问任何外部变量或者访问的是静态局部变量或者访问的是全局变量),那这个block就是__NSGlobalBlock__。__NSGlobalBlock__类型的block在内存中是存在数据区的(也叫全局区或静态区,全局变量和静态变量是存在这个区域的)。__NSGlobalBlock__类型的block调用copy方法的话什么都不会做。
栈类型 (NSStackBlock)
如果一个block里面访问了普通的局部变量,那它就是一个__NSStackBlock__,它在内存中存储在栈区,栈区的特点就是其释放不受开发者控制,都是由系统管理释放操作的,所以在调用__NSStackBlock__类型block时要注意,一定要确保它还没被释放。如果对一个__NSStackBlock__类型block做copy操作,那会将这个block从栈复制到堆上。
堆类型 (NSMallocBlock)
一个__NSStackBlock__类型block做调用copy,那会将这个block从栈复制到堆上,堆上的这个block类型就是__NSMallocBlock__,所以__NSMallocBlock__类型的block是存储在堆区。如果对一个__NSMallocBlock__类型block做copy操作,那这个block的引用计数+1。
在ARC环境下,编译器会根据情况,自动将栈上的block复制到堆上。有一下4种情况会将栈block复制到堆上:
- block作为函数返回值时
- 将block赋值给强指针时
- 当block作为函数参数时
- 当block作为GCD的参数时
block 循环引用
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.age = 20;
void (^block)(void) = ^{
NSLog(@"age--- %ld",person.age);
};
block();
}
return 0;
}
复制代码
转成C++查看
// 底层结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__strong person;
};
复制代码
可以看到和基本数据类型不同的是,person对象被block捕获后,在结构体中多了一个修饰关键字__strong。
如果使用 _weak 修饰变量之后在引用
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.age = 20;
__weak Person *weakPerson = person;
void (^block)(void) = ^{
NSLog(@"age--- %ld",weakPerson.age);
};
block();
}
return 0;
}
复制代码
// 底层block
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__weak weakPerson;
};
复制代码
可见此时block中weakPerson的关键字变成了__weak。
在block中修饰被捕获的对象类型变量的关键字除了__strong、__weak外还有一个__unsafe_unretained。那这结果关键字起什么作用呢?
当block被拷贝到堆上时是调用的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数就会根据这3个关键字来进行操作。
如果关键字是__strong,那block内部就会对这个对象进行一次retain操作,引用计数+1,也就是block会强引用这个对象。也正是这个原因,导致在使用block时很容易造成循环引用。
如果关键字是__weak或__unsafe_unretained,那block对这个对象是弱引用,不会造成循环引用。所以我们通常在block外面定义一个__weak或__unsafe_unretained修饰的弱指针指向对象,然后在block内部使用这个弱指针来解决循环引用的问题。
__block修饰符的作用
在介绍__block之前,我们先来看下下面这段代码:
- (void)test{
int age = 10;
void (^block)(void) = ^{
age = 20;
};
}
复制代码
这段代码有什么问题吗?编译器会直接报错,在block中不可以修改这个age的值。为什么呢?
因为age是一个局部变量,它的作用域和生命周期就仅限在是test方法里面,而前面也介绍过了,block底层会将大括号中的代码封装成一个函数,也就相当于现在是要在另外一个函数中访问test方法中的局部变量,这样肯定是不行的,所以会报错。
如果我想在block里面更改age的值要怎么做呢?我们可以将age定义成静态局部变量static int age = 10;。虽然静态局部变量的作用域也是在test方法里面,但是它的生命周期是和程序一样的,而且block捕获静态局部变量实际是捕获的age的地址,所以block里面也是通过age的地址去更改age的值,所以是没有问题的。
但我们并不推荐这样做,因为静态局部变量在程序运行过程中是不会被释放的,所以还是要尽量少用。那还有什么别的方法来实现这个需求呢?这就是我们要讲的__block关键字。
- (void)test1{
__block int age = 10;
void (^block)(void) = ^{
age = 20;
};
block();
NSLog(@"%d",age);
}
复制代码
当我们用__block关键字修饰后,底层到底做了什么让我们能在block里面访问age呢?下面我们来看下上面代码转成c++代码后block的存储结构是什么样的。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
};
struct __Block_byref_age_0 {
void *__isa; // isa指针
__Block_byref_age_0 *__forwarding; // 如果这block是在堆上那么这个指针就是指向它自己,如果这个block是在栈上,那这个指针是指向它拷贝到堆上后的那个block
int __flags;
int __size; // 结构体大小
int age; // 真正捕获到的age
};
复制代码
复制代码我们可以看到,age用__block修饰后,在block的结构体中变成了__Block_byref_age_0 *age ;而__Block_byref_age_0是个结构体,里面有个成员int age;,这个才是真正捕获到的外部变量age,实际上外部的age的地址也是指向这里的,所以不管是外面还是block里面修改age时其实都是通过地址找到这里来修改的。
所以age用__block修饰后它就不再是一个test1方法内部的局部变量了,而是被包装成了一个对象,age就被存储在这个对象中。之所以说是包装成一个对象,是因为__Block_byref_age_0这个结构体的第一个成员就是isa指针。
__block修饰变量的内存管理:
__block不管是修饰基础数据类型还是修饰对象数据类型,底层都是将它包装成一个对象(我这里取个名字叫__blockObj),然后block结构体中有个指针指向__blockObj。既然是一个对象,那block内部如何对它进行内存管理呢?
当block在栈上时,block内部并不会对__blockObj产生强引用。
当block调用copy函数从栈拷贝到堆中时,它同时会将__blockObj也拷贝到堆上,并对__blockObj产生强引用。
当block从堆中移除时,会调用block内部的dispose函数,dispose函数内部又会调用_Block_object_dispose函数来释放__blockObj。
delegate 和 block的区别
-
从源头上理解和区别block和delegate
delegate运行成本低,block的运行成本高。
block出栈需要将使用的数据从栈内存拷贝到堆内存,当然对象的话就是加计数,使用完或者block置nil后才消除。delegate只是保存了一个对象指针,直接回调,没有额外消耗。就像C的函数指针,只多做了一个查表动作。 -
从使用场景区别block和delegate
有多个相关方法。假如每个方法都设置一个 block, 这样会更麻烦。而 delegate 让多个方法分成一组,只需要设置一次,就可以多次回调。当多于 3 个方法时就应该优先采用 delegate。当1,2个回调时,则使用block。
delegate更安全些,比如: 避免循环引用。使用 block 时稍微不注意就形成循环引用,导致对象释放不了。这种循环引用,一旦出现就比较难检查出来。而 delegate 的方法是分离开的,并不会引用上下文,因此会更安全些。