前言
在<<iOS 与OS X多线程和内存管理>>笔记:Blocks中我写的都是我们日常开发过程中所用到的Blocks.这里我们深层次的看一下Blocks的相关实现.
把OC代码转换为C++结构体代码
为了使我们更方便看清Block内部的运行,我们需要把OC代码代码转化为带有结构体的C++代码.这里我们就需要使用到clang -rewrite-objc指令.步骤有如下两步.
- 打开终端,使用cd指令进入需要转化的文件目录下,比如我要对桌面上的Test工程下的main.m文件进行转化.终端指令类似于下图所示.
- 然后执行如下的终端命令 clang -rewrite-objc main.m,如下所示.
然后在当前文件夹下就会出现后缀为.cpp的C++执行文件.如下所示.
Block的实现
首先,我们在main函数中写一个简单block匿名函数并且进行调用,如下所示.
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^blk)(void) = ^{printf("Block\n");};
blk();
}
return 0;
}
然后,我们通过 clang -rewrite-objc main.m指令把mian.m转变为C++文件.里面代码较多,我们下拉到文件的最底部.
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;
__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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");
}
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 argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
return 0;
}
我们可以看到,我们写的block已经被转化为一个C++语言的函数,如下所示.
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");
}
概念函数的参数__cself相当于C++实例方法中指向实例自身的变量this,或是Objective-C实例方法中指向对象自身的变量self,也就是说参数____cself为指向Block值的变量.可是我们发现____cself并没有在这里使用,这里我们先不做研究,我们先看一下参数____cself的本质.
struct __main_block_impl_0 *__cself
-
Block的结构体
我们看到参数____cself是__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;
}
};
通过<<iOS 与OS X多线程和内存管理>>我们可以了解到两个成员变量各包含什么信息.
-
Block结构体的成员变量
我们先看一下成员变量impl的结构体(在.cpp文件的顶部位置).如下所示.
struct __block_impl {
void *isa;
int Flags;
int Reserved;//今后版本升级所需的区域
void *FuncPtr;//函数指针
};
第二个成员变量Desc主要是存储今后版本升级所需的区域和Block大小.具体如下所示.
static struct __main_block_desc_0 {
size_t reserved; //今后版本升级所需的区域
size_t Block_size; //Block大小
}
-
Block的构造
接下来我们就看一下__main_block_impl_0的构造函数是如何构造的.在main函数中调用的源码如图所示.
书中为了方便大家理解这句代码调用,进行了如下的转换.也就是说blk其实上是指向类型为__main_block_impl_0的tmp结构体指针.
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &tmp;
接下来我们看一下结构体的构造函数的参数.首先是__main_block_desc_0_DATA这个参数.我们在代码中找到了它的赋值过程.如下所示.
static struct __main_block_desc_0 __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
通过上面的构造函数,__main_block_impl_0的值就会如下所示.
impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.Reserved = 0;
impl.FuncPtr = ___main_block_func_0;
Desc = &__main_block_desc_0_DATA;
-
Block的调用过程
接下来我们看一下使用block的代码是如何实现的.
blk();
找到.cpp文件对应的代码如下所示.
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
我们去掉转化部分.简化代码之后如下所示.这句代码是什么意思呢?这就是使用函数的指针调用函数.正如我们刚刚所示的一样.正如上一个模块所说的那样,___main_block_func_0的函数指针被赋值到了结构体的FuncPtr中了.另外___main_block_func_0的所需参数是__main_block_impl_0的类型,也就是blk.所以有以下的函数调用.
(*blk->FuncPtr)(blk);
-
Block的实质
这时候我们需要回过头来说明__main_block_impl_0结构体成员变量 impl中的isa指针.
我们知道isa指针在构造函数中被赋值为&_NSConcreteStackBlock.如下图所示.
其实Block就是Objective-C对象.为什么这么说呢?首先我们看一下什么叫做Objective-C对象.
在Objective-C中,任何类的定义都是对象。类和类的实例(对象)没有任何本质上的区别。任何对象都有isa指针。
假定我们创建一个如下的对象.
@interface MyObject : NSObject
{
int val0;
int val1;
}
@end
那么基于Objective-C对象的结构体就应该如下所示.
struct MyObject
{
Class isa;
int val0;
int val1;
}
其中的isa指针指向如下所示.具体可查看书中的98页.
通过比较我们知道Block的结构体中有isa指针._NSConcreteStackBlock就相当于上图的class_t结构体实例.也就是说Block即为Objective-C的对象.
Block截获自动变量值的实现
对于Block截获自动变量值,在<<iOS 与OS X多线程和内存管理>>笔记:Blocks中我们已经说过了,现在我们列举一下例子.来看一下是如何实现截获自动变量值这一过程的.
int number = 1;
void (^blk)(void) = ^{
printf("value:%d\n",number);
};
number = 3;
blk();
运行程序.打印结果如下所示.
通过clang -rewrite-objc main.m指令编译成C++文件.其中核心代码如下所示.
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 number;//新增成员变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _number, int flags=0) : number(_number) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int number = __cself->number; // bound by copy
printf("value:%d\n",number);
}
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 argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int number = 1;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, number));
number = 3;
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
return 0;
}
这时候我们把Block的结构体拿出来看一下.我们发现新增了一个成员变量number以及构造方法发生新增了对number的赋值.
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int number;//新增成员变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _number, int flags=0) : number(_number) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
然后看一下main函数中__main_block_impl_0构造函数的构造过程.
int number = 1;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, number));
这一步我们就知道在__main_block_impl_0结构体构造的时候已经把number的值存储到了自身成员变量number中了,所以后面number如何改变,那么Block在构造完成之后打印的number值就不会发生改变了.
通过上面的表述,我们可以就了解为什么在不能Block中直接修改变量的值?(面试题).例如下图所示.
这是为什么呢?我们看一下__main_block_func_0函数的实现,如下所示.我们可以知道传递的是__main_block_impl_0结构体的成员变量的值.而不是指针(其实就算是指针也没有任何的关系),跟原来的number变量无任何关系.所以我们不能在函数中直接修改number变量变量.
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int number = __cself->number; // bound by copy
printf("value:%d\n",number);
}
__block说明符的实现
上面一个模块最后我们说到如果直接在block中给变量赋值会报错,我们发现根本原因就是Block结构体中传递的是变量值,而不是指针,那么如何解决这一问题呢?这时候__block说明符就出现了.我们看一下C语言代码,如下所示.
__block int number = 1;
void (^blk)(void) = ^{
printf("value:%d\n",number);
number = 6;
};
blk();
但是通过clang -rewrite-objc main.m指令转变的C++代码去发生了很大的变化.核心代码如下所示.
//numbr变量已经通过__block的修饰变成了结构体
struct __Block_byref_number_0 {
void *__isa;
__Block_byref_number_0 *__forwarding;
int __flags;
int __size;
int number;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_number_0 *number; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_number_0 *_number, int flags=0) : number(_number->__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_number_0 *number = __cself->number; // bound by ref
printf("value:%d\n",(number->__forwarding->number));
(number->__forwarding->number) = 6;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->number, (void*)src->number, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->number, 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(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_number_0 number = {(void*)0,(__Block_byref_number_0 *)&number, 0, sizeof(__Block_byref_number_0), 1};
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_number_0 *)&number, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
return 0;
}
我们看一下主要改变的部分.int number = 1;
变成__block int number = 1;
之后,C++代码如下所示.代码量提升了不是一倍两倍呀~
struct __Block_byref_number_0 {
void *__isa;
__Block_byref_number_0 *__forwarding;//指向自身的指针
int __flags;
int __size;
int number;
};
然后我们看一下在main函数中的构造代码.如下所示.
__attribute__((__blocks__(byref))) __Block_byref_number_0 number = {(void*)0,(__Block_byref_number_0 *)&number, 0, sizeof(__Block_byref_number_0), 1};
简化代码之后,如下所示.
__Block_byref_number_0 number = {
0,
&number,
0,
sizeof(__Block_byref_number_0),
1
};
这时候Block结构体的构造函数和新增成员变量也发生了改变.成员变量变成了指向__Block_byref_number_0类型的结构体.
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_number_0 *number; //新增成员变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_number_0 *_number, int flags=0) : number(_number->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
那么在block中进行赋值的时候是如何操作的呢?这主要是通过__Block_byref_number_0的成员变量__forwarding来完成的.__forwarding是指向本身的指针.我们可以通过__forwarding来找到成员变量number的值.所以在__main_block_func_0函数实现中有如下的代码.
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_number_0 *number = __cself->number; // bound by ref
printf("value:%d\n",(number->__forwarding->number));
(number->__forwarding->number) = 6;
}
对于__Block_byref_number_0结构体中的__forwarding指针,我们可以看下面的示意图.
Block存储域
通过下面一张表我们了解到Block和__block变量时存储在栈区的结构体类型自动变量(一般情况下).
名称 | 实质 |
---|---|
Block | 栈上Block的结构体实例 |
__block | 栈上__block变量的结构体实例 |
接下来我们还是来研究Block结构体的isa指针,在前面的例子中,isa指针是指向_NSConcreteStackBlock的.其实还有很多类似的类.我们先用一张表格来说明每个类的不同点
类 | 设置对象的存储域 | 副本源的配置存储域 | 复制效果 |
---|---|---|---|
_NSConcreteStackBlock | 栈 | 栈 | 从栈区复制到堆区 |
_NSConcreteMallocBlock | 堆 | 堆 | 引用计数增加 |
_NSConcreteGlobaBlock | 全局区 | 全局区 | 什么也不做 |
通过上面的表格,我们就可以知道两个面试题的答案,
问: Block的类一共有几种?
答: 三种,分别是 _NSConcreteStackBlock 、_NSConcreteMallocBlock、_NSConcreteGlobaBlock
问: Block为什么用copy修饰?
答: block在定义成属性的时候应该使用copy修饰,平常我们使用的block主要是存放在栈区的(有的也会存放在全局区).栈区的block出了作用域之后就会被释放掉,如果我们在block释放掉之后还继续调用,那么就会出现crash.理论上,在全局区的block我们是不需要进行copy的.但是大部分的block是存储在栈区的,为了统一规范管理,所以我们都使用copy对block属性进行修饰.
__block变量存储域
上一个模块是对Block进行了说明,那么对于使用__block变量的Block从栈上复制到堆上是,__block变量会有什么影响呢?
__block变量的配置存储域 | Block从栈区复制到堆时的影响 |
---|---|
栈 | 从栈复制到堆并被Block持有 |
堆 | 被Block持有 |
上面这张表是表达了什么意思呢? 也就是说:
- 如果有一个Block使用某个__block变量,那么__block变量会从栈复制到堆并被Block持有.
- 如果有多个Block使用某个__block变量,那么在第一个Block中__block变量会从栈复制到堆并被第一个Block持有.从第二个Block时是持有__block变量,也就是只会增加__block变量的引用计数.
对于__forwarding指针(指向自身的指针),我们曾经说过,"不管__block变量配置在栈上还是堆上,都能正确访问该变量."我们可以通过下面的例子来说明一下情况.
__block int val = 0;
void (^blk)(void) = [^{ ++val; } copy];
++val;
blk();
NSLog(@"%d",val);
通过blk这个Block的copy操作, 被__block修饰的val变量成功的从栈上复制到了堆上了.
所以^{ ++val; }
和++val;
都可以被转化为以下的形式.
++(val.__forwarding->val);
我们可以通过下面的示意图来表示上面的转变过程.
截获对象的实现
我们曾经说过截获变量值,现在我们说一下截获对象的实现.演示源码如下所示.
void (^blk)(id obj);
{//array的作用域
id array = [[NSMutableArray alloc] init];
blk = [^(id obj){
[array addObject:obj];
NSLog(@"array count = %ld",[array count]);
} copy];
}//array的作用域已经结束
blk([NSObject new]);
blk([NSObject new]);
blk([NSObject new]);
我们知道array的作用域已经结束了(到达注释位置时候),可以我们调用block仍然可以访问到array.如下所示,这是为什么呢?
实际上在blk的实现过程中.已经持有了array对象.<<iOS 与OS X多线程和内存管理>>是有以下代码的.
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
id __strong array; //强引用的array成员变量
};
在Objective-C中,C语言结构体并不能含有__strong修饰符的变量.因为编译器不知道应该何时进行C语言结构体的初始化和废弃操作.不能很好的管理内存.Objective-C的运行时库可以很好的把握Block从栈上复制到堆以及堆上的Block被废弃的时机.从而有效管理成员变量的持有和释放.为此,在__main_block_desc_0就增建了两个成员变量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->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
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};
可是我在实际过程中并没有__strong修饰词.个人猜想是已经进行了缺省操作了.省略了__strong的修饰符.源码截图如下所示.大家可以自行试验操作.
循环引用的本质
上一个模块我们说了.Block可以持有对象.如果一个对象中含有某个Block的成员属性(strong修饰).在Block中直接使用self,会造成循环引用,原因就出现__main_block_impl_0结构体中的obj.__main_block_impl_0对obj是强引用,self对Block变量是强引用,两者相互引用,最终造成循环引用.
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
id __strong obj; //强引用的obj成员变量
};
示意图如下所示.
结束
这一篇Block的实现总共写了三天,加上自己验证,收获良多,希望这一篇博客对大家有所帮助.还是希望大家来看一下<<iOS 与OS X多线程和内存管理>>原书,自己敲一遍实现源码,这样帮助很大,会加深印象.最后感谢各位看官查看本篇文章.如果有任何问题,欢迎联系骚栋.欢迎指导批斗.
<<iOS 与OS X多线程和内存管理>>的PDF版传送门🚪