前言
写本篇文章的目的就是要搞明白以下几个问题?
- block的底层实现原理
- block的几种类型?
- __block的作用是什么?有什么使用注意点?
- block的循环引用问题解决?
什么是Block
Block是将函数及其上下文封装起来的对象。
Block的底层实现原理
Block的底层实现是一个结构体
。
void blockTest()
{
void (^block)(void) = ^{
NSLog(@"Hello");
};
block();
}
int main(int argc, char * argv[]) {
@autoreleasepool {
blockTest();
}
}
可以通过clang命令查看编译器是如何实现Block的,进入main.m的目录,在终端输入xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
,然后会在当前目录生成main.cpp的C++文件,代码如下:
///Block的C++实现
struct __blockTest_block_impl_0 {
struct __block_impl impl;
struct __blockTest_block_desc_0* Desc;
///构造函数
__blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __block_impl {
///isa指针,指向一个类对象,有三种类型:_NSConcreteStackBlock、_NSConcreteGlobalBlock和_NSConcreteMallocBlock
void *isa;
///block的负载信息(引用计数和类型信息),按位存储
int Flags;
///保留变量
int Reserved;
///指向Block执行时调用的函数,也就是Block需要执行的代码块
void *FuncPtr;
};
///Block 执行时调用的函数
static void __blockTest_block_func_0(struct __blockTest_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_59_c8zx7n553c34b8d791v8l7ww0000gn_T_main_ab6df5_mi_0);
}
static struct __blockTest_block_desc_0 {
///Block版本升级所需要的预留区空间
size_t reserved;
///Block的大小=sizeof(struct __blockTest_block_impl_0)
size_t Block_size;
} __blockTest_block_desc_0_DATA = { 0, sizeof(struct __blockTest_block_impl_0)};
void blockTest() {
///block变成了一个指针,指向一个通过__blockTest_block_impl_0构造函数实例化的结构体实例
///__blockTest_block_func_0表示Block块的函数指针
///__blockTest_block_desc_0_DATA作为静态全局变量初始化__main_block_desc_0的结构体实例指针
void(*block)(void) = ((void (*)())&__blockTest_block_impl_0((void *)__blockTest_block_func_0, &__blockTest_block_desc_0_DATA));
///调用Block
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
blockTest();
}
}
画个图来表示各个结构体之间的关系就是:
Block的底层数据结构也可以用一张图表示:
探索Block的变量捕获
Block根据其类型可以分为三类:全局区Block、栈区Block、堆区Block。然而通过上面Block的C++底层实现,可以看到__block_impl有一个属性isa,而这个isa指向的对象有三种类型,也就是这三种Block。通过这个也可以看出,Block的也是一个对象。
截获auto变量值
void blockTest()
{
int age = 20;
void (^block)(void) = ^{
NSLog(@"Hello==%d", age);
};
block();
}
通过Clang指令生成C++代码:
struct __blockTest_block_impl_0 {
struct __block_impl impl;
struct __blockTest_block_desc_0* Desc;
int age;
__blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到__blockTest_block_impl_0多了一个成员变量age,并且构造函数也多了一个参数age,传的仅仅是a ge的值。可以得出Block捕获的值,所以要想在Block内部修改局部变量的值是不行的。
使用static修饰变量
把上面的age改成使用static修饰
void blockTest()
{
static int age = 20;
void (^block)(void) = ^{
NSLog(@"Hello==%d", age);
};
block();
}
通过Clang指令生成C++代码:
struct __blockTest_block_impl_0 {
struct __block_impl impl;
struct __blockTest_block_desc_0* Desc;
int *age;
__blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看出捕获的不再是变量的值,而是变量的指针地址,所以也可以在Block内部修改age的值。并且static修饰的局部变量叫作静态局部变量,是存储在静态存储区,这块内存只有在程序结束才会销毁,但是只是在声明它的代码块可见,所以传入变量的指针也不用担心变量销毁的问题。
全局变量
int age = 20;
void blockTest()
{
void (^block)(void) = ^{
NSLog(@"Hello==%d", age);
};
block();
}
通过Clang指令生成C++代码:
struct __blockTest_block_impl_0 {
struct __block_impl impl;
struct __blockTest_block_desc_0* Desc;
__blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看出并没有把全局变量age捕获,是直接访问全局变量。
进而也可以得出如下结论:
__block修饰变量
如果想修改局部变量的值,可以通过__block修饰实现。
void blockTest()
{
__block int age = 20;
void (^block)(void) = ^{
age = 26;
NSLog(@"Hello==%d", age);
};
block();
}
通过Clang指令生成C++代码:
__blockTest_block_impl_0多出来一个成员变量__Block_byref_age_0 *age,我们看到经过__block修饰的变量类型变成了结构体__Block_byref_age_0,block捕获的是__Block_byref_age_0类型指针。调用函数的时候先通过__forwarding找到age指针,然后去取出age值。
并且可以看到这次的C++代码多出了两个函数:
__blockTest_block_copy_0
中调用的是_Block_object_assign
,__blockTest_block_dispose_0
中调用的是_Block_object_dispose
并且这个两个函数都有个参数8,看注释说是这个枚举值BLOCK_FIELD_IS_BYREF,在Block_private.h 中可以查看到:
这些枚举值表示的含义分别为:
- BLOCK_FIELD_IS_OBJECT:OC对象类型
- BLOCK_FIELD_IS_BLOCK:是一个block
- BLOCK_FIELD_IS_BYREF:在栈上被__block修饰的变量
- BLOCK_FIELD_IS_WEAK:被__weak修饰的变量,只在Block_byref管理内部对象内存时使用
- BLOCK_BYREF_CALLER:处理Block_byref内部对象内存的时候会加的一个额外标记(告诉内部实现不要进行retain或者copy)
Block copy流程
// 拷贝 block
// 如果原来就在堆上,就将引用计数加 1;
// 如果原来在栈上,会拷贝到堆上,引用计数初始化为 1,并且会调用 copy helper 方法(如果存在的话
// 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
// 参数 arg 就是 Block_layout 对象,
// 返回值是拷贝后的 block 的地址
// 运行?stack -》malloc
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;
const char *signature = _Block_descriptor_3(aBlock)->signature;
// 如果现在已经在堆上
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 相同大小的内存
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
// 开辟失败,返回 NULL
if (!result) return NULL;
// 将 aBlock 内存上的数据全部复制新开辟的 result 上
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
// 将 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源码中,从栈区copy到堆区的过程中,_Block_call_copy_helper(result, aBlock)的调用时为了复制栈区的Block里面的成员变量,给堆区的Block。其实最终会发现调用的是这个函数_Block_object_assign
,根据参数 flags 的类型(对象、block、byref...),做了不同的处理
- 对象类型,增加对象的引用计数;
- block类型,会对该block执行一次block_copy操作;
- __block修饰,会调用_Block_byref_copy;
__block
和Block
类似,如果在栈区,会重新malloc
一份,进行深拷贝操作,但这两个的forwarding
都会指向堆区的,如果已经在堆区,只会将其引用计数+1。上面也有提到关于__block修饰变量。
Block_release 流程
void _Block_release(const void *arg) {
// 1. 将指针转换为Block_layout结构
struct Block_layout *aBlock = (struct Block_layout *)arg;
if (!aBlock) return;
// 2. 如果是全局block,那么直接返回
if (aBlock->flags & BLOCK_IS_GLOBAL) return;
// 3. 如果不是堆block,那么直接返回
if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;
// 4. 处理堆block的引用计数
if (latching_decr_int_should_deallocate(&aBlock->flags)) {
// 5. 释放block捕获的变量
_Block_call_dispose_helper(aBlock);
_Block_destructInstance(aBlock);
// 6. 释放堆block
free(aBlock);
}
}
Block的内存管理
之前提到Block的类型有三种,也就是__block_impl中的isa指向的对象有三种类型:
那在ARC环境下,哪些情况下编译器会自动把栈区Block拷贝到堆上
当把Block拷贝到堆上,会有哪些变化
typedef void(^Block)(void);
int main(int argc, char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc]init];
Block block = ^{
NSLog(@"%p",obj);
};
block();
}
}
通过Clang指令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m 转成C++代码
可以看到捕获的对象被强引用了,进行了copy操作,在copy函数内部的_Block_object_assign会根据对象修饰符strong
或者weak
而对其进行强引用或者弱引用。
总结:
-
当Block内部访问了对象类型的auto对象时,如果Block是在栈上,将不会对auto对象产生强引用。
-
如果Block被拷贝到堆上,会调用Block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign会根据auto对象的修饰符(__strong,__weak,__unsafe_unretained)做出相应的操作,当使用的是__strong时,将会对auto对象的引用计数加1,当为__weak时,引用计数不变。
-
如果Block从堆上移除,会调用block内部的dispose函数,内部会调用_Block_object_dispose函数,这个函数会自动释放引用的auto对象。
解决Block的循环引用
我们知道产生循环引用的条件是相互持有,就像下面这个图画的一样
要想解决这个问题,就是打破这个循环,通过__weak修饰对象
__unsafe_unretained也可以解决循环引用,不安全,指向的对象销毁时,指针存储的地址值不变
__block修饰对象,不用的时候把对象置为null,也一样可以打破循环
以上就是最近我整理的关于Block的知识点,文章中如有纰漏,希望大家指正。