那些关于iOS Blocks的坑(一)
程序员文章站
2022-03-09 21:38:45
...
- 很多时候,我们都只关心怎么把一些语法用得非常熟练,记住所有的坑点,比如说Blocks,什么循环引用的坑,属性声明用copy...等等。但是我们总是懒于去深究,为什么会有这些问题。源码面前,没有秘密。今天就一起来分析下关于Blocks的底层。
这个专题主要介绍Blocks的底层实现,分为以下几个部分,将通过多篇博文一一阐述。
1.Blocks的本质
2.Blocks为什么截取自动变量值
3.__block说明符
4.关于Blocks的存储
5.关于__block变量的存储
6.Blocks的循环引用问题
Blocks的本质
- Blocks究竟是什么?我们先用clang(LLVM编译器)将我们的OC代码转化C++源代码,所谓源码面前,了无秘密。
//block.m
int main() {
int count = 10;
void (^blk)(void) = ^{
printf("%d\n", count);
};
blk();
return 0;
}
<strong>clang -rewrite-objc 源代码文件名</strong>
#define BLOCK_IMPL
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int count;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _count, int flags=0) : count(_count) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int count = __cself->count; // bound by copy
printf("%d\\n", count);
}
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)};
int main() {
int count = 10;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
以上便是那份OC代码block.m文件编译出来摘取的关键代码。几行代码瞬间变成了几十行代码。看着很吓人,其实不难分析。
先来看main函数中的调用声明和调用block的代码,转化成了什么。
int main() {
int count = 10;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
- int count = 10; 局部变量count,赋值为10;
- void (*blk)(void), 这个东西有C语言函数指针基础的童鞋肯定一眼就认出来。其实void (^blk)(void) 就是转化为指针名字为blk的函数指针。我们对block赋值,实则是对该函数指针赋值。
- 其次我们对void (*blk)(void)函数指针进行赋值,赋值对象为结构体struct __main_block_impl_0,<strong>通过它的构造方法对相应的参数进行赋值。</strong>
- impl.isa = &_NSConcreteStackBlock;isa指针其实是指向其父类class_t的地址,后面会有讨论。
- impl.FuncPtr = fp; 这一句很关键,该FuncPtr是指向调用方法的指针,也就是我们执行block表达式时,去调用的方法,这里传的参数是方法__main_block_func_0;
- 最后一句代码就是通过调用__block_impl指针中的FuncPtr指向的方法去执行block;
__cself参数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int count = __cself->count; // bound by copy
printf("%d\\n", count);
}
- 执行blk()调用的函数中,参数__cself类型便是上面我们分析的这个结构体,大家肯定一下子就明白了,这是参数相当于self, 在C++中相当于this指针。即是当前类的对象。
- <strong>__main_block_impl_0</strong>结构体中包含以下这些成员变量。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int count;
}
- <strong>struct __block_impl</strong>
结构体中的第一个参数所对应的结构体 struct __block_impl,根据名称可以联想到某些标志,今后版本升级所需的区域,以及函数指针。
struct __block_impl {
void *isa;
int Flags; //标志
int Reserved; //今后版本升级所需的区域
void *FuncPtr; //函数指针
};
- <strong>struct __main_block_desc_0* Desc</strong>
static struct __main_block_desc_0 {
size_t reserved; //今后版本升级所需的区域
size_t Block_size; //Block的大小
}
- <strong>__main_block_impl_0结构体中最后一个成员变量count</strong>
不难发现,这是个类成员变量,是存在于block内部的。这也是为什么block会截取局部变量的原因,它是从外部定义的变量count中,通过值传递拷贝到block内部的。所以也就导致,在外部修改了count值,是没办法在block中也对应修改的原因。接下来会详细介绍。
__main_block_func_0函数
int count = __cself->count; // bound by copy
printf("%d\\n", count);
- 到这里大家应该豁然开朗了,这里打印的count值,是通过__cself指针获取到block中的那个类成员变量count。以上便是这样一个block的底层代码分析。
Blocks为什么截取自动变量值
- 从上面的分析中,我们知道,count变量在block定义的时候,便作为形式参数,通过__main_block_impl_0构造函数进行了值拷贝。
何为值拷贝,何为地址拷贝?
- 所谓值拷贝,<strong>就是重新开辟一块内存将值拷贝存进这块新的内存中。它和被拷贝的值所在的地址是不同的。</strong>
- 所谓地址拷贝,<strong>就是开辟一个指针的内存,将指针的指向赋值为被拷贝的值所在的那块内存。</strong>
- 通过值拷贝,无法通过修改被拷贝的值进而修改拷贝的那个值,这也造成了所谓的<strong>Blocks截取自动变量值</strong>的效果。而通过地址拷贝可以做到,这也是接下来__block将要做的事情。
__block关键字
- 前面一个例子中,如果我们在Block中进行局部变量的修改,那么编译器就不乐意了。
//block.m
int main() {
int count = 10;
void (^blk)(void) = ^{
count = 11;//编译错误
};
blk();
return 0;
}
- 产生以下编译错误
error: variable is not assignable (missing __block type specifier)
count = 11;
- 显然,编译器是拒绝这么做的,并告诉我们 count变量缺少 <__block类型说明符 >(missing __block type specifier)
解决方案
- 对于这个问题,有两种解决方案。
- 第一种就是编译器提示我们的,对count加入<strong>__block声明</strong>
- 第二种则是将count声明为<strong>静态变量,静态全局变量,或者全局变量。</strong>
- 直接看__block声明之后,编译器为我们做了些什么。
//block.m
int main() {
__block int count = 10;
void (^blk)(void) = ^{
count = 11;
};
blk();
return 0;
}
clang -rewrite-obj 编译文件
struct __Block_byref_count_0 {
void *__isa;
__Block_byref_count_0 *__forwarding;
int __flags;
int __size;
int count;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_count_0 *count; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_count_0 *_count, int flags=0) : count(_count->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_count_0 *count = __cself->count; // bound by ref
(count->__forwarding->count) = 11;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->count, (void*)src->count, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->count, 8/*BLOCK_FIELD_IS_BYREF*/);}
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};
int main() {
__attribute__((__blocks__(byref))) __Block_byref_count_0 count = {
(void*)0,
(__Block_byref_count_0 *)&count,
0,
sizeof(__Block_byref_count_0),
10
};
void (*blk)(void) = ((void (*)())&__main_block_impl_0(
(void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_count_0 *)&count, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
- 每次编译出来是不是都感觉压力山大,哈哈。慢慢剖析一下通过__block说明符编译出来的源码。
- 先来看看__block变量count是怎么转化过来的。
__attribute__((__blocks__(byref))) __Block_byref_count_0 count = {
(void*)0,
(__Block_byref_count_0 *)&count,
0,
sizeof(__Block_byref_count_0),
10
};
- 通过前面的介绍,我们可以看到,这个过程又转化成了我们相对比较熟悉的结构体了。__block变量如同Blocks一样变成了__Block_byref_count_0结构体类型的自动变量,即在栈上生成的__Block_byref_count_0的结构体实例。
__Block_byref_count_0
struct __Block_byref_count_0 {
void *__isa;
__Block_byref_count_0 *__forwarding;
int __flags;
int __size;
int count;
};
相比起我们一开始编译出来没有使用__block说明符声明的代码中,多出了我们不太熟悉的一个指针
__forwarding
//持有指向该实例自身的指针。而我们之所以能在block中修改外部变量的原因就在于此。
//原理就是,通过修改成员变量__forwarding访问成员变量count。(成员变量count是该实例自身持有的变量,它相当于block中的原自动变量)
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_count_0 *count = __cself->count; // bound by ref
(count->__forwarding->count) = 11;
}
// 通过以上这段代码,大家就知道了,__block说明符就是通过修改__forwarding指针中的count变量从而达到修改外部变量的效果,多数时候我们可以推理出,能够修改一个地方达到修改其他地方的这种场景,无异于对同一块内存上的内容进行了修改,而能够达到这种效果的,便是指针的操作。
至于静态变量,静态全局变量和全局变量也能达到这个效果的办法,请大家自己编译源码查看下,其实原理也是通过修改指针来达到这个效果的。而这些变量的值是存在于__main_block_impl_0结构体中的
关于Blocks的存储
从上面编译出来的代码中,我们前面提到的isa指针,在初始化Block中,impl.isa = &_NSConcreteStackBlock;所谓结构体类型的自动变量,即栈上生成的该结构体的实例。
-
以上的Block的类转化为_NSConcreteStackBlock,虽然并没有出现转化过后对应的源代码,但还有几个与之类似的类.
- _NSConcreteStackBlock //将该类的对象Block设置在栈上
- _NSConcreteGlobalBlock //与全局变量一样,设置在数据区域(.data区)中
- _NSConcreteMallocBlock //设置在malloc函数分配的内存块中(即堆)
分析如何创建对应存储区的Block
- _NSConcreteStackBlock : 通常情况下,在方法内部手动声明定义的Block,均为栈上分配的。
- _NSConcreteGlobalBlock : 将Block声明在函数外部,作为全局变量的Block则是分配在数据区域。
- _NSConcreteMallocBlock :
下一篇: 设计模式学习(一)--简单工厂模式