探寻Block的本质(1)—— 基本认识markdown
探寻Block的本质(2)—— 底层结构iphone
你们应该都知道,若是想在block内部修改从外部捕获的auto
变量的值,能够在该auto
变量定义的时候,加上关键字__block
。代码案例以下学习
#import <Foundation/Foundation.h>
typedef void(^CLBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int a = 10;
int b = 30;
CLBlock myblock = ^{
a = 20;
NSLog(@"%d",b);
};
myblock();
NSLog(@"myblock执行完以后,a = %d",a);
}
return 0;
}
*********************运行结果*********************
2019-09-04 19:41:51.709406+0800 Block学习[29867:3904669] 30
2019-09-04 19:41:51.709706+0800 Block学习[29867:3904669] myblock执行完以后,a = 20
复制代码
__block
只能够用来做用于auto
变量,它的目的就是为了可以让auto
变量可以在block内部内修改。而全局变量和static
变量原本就能够从block内部进行修改,所以__block
对它们来讲没有意义,因此__block
被规定只能用于修饰auto
变量,这一点应该不难理解。spa
老套路,咱们先经过终端命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
来看一下__block
以及block在底层张什么样子。首先看看block的底层结构.net
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int b;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp,
struct __main_block_desc_0 *desc,
int _b,
__Block_byref_a_0 *_a,
int flags=0) : b(_b), a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
复制代码
为了比较,我特地加了一个int b
做为对比,顺便回顾一下,基本类型的auto
变量被block捕获的时候,就是经过值拷贝的形式把值赋给block内部相对应的基本类型变量。而案例里面的__block int a = 10
,咱们能够看到在底层,系统是把int a
包装到了一个叫__Block_byref_a_0
的对象里面。这个对象的结构以下设计
struct __Block_byref_a_0 {
void *__isa;//有isa,是一个对象
__Block_byref_a_0 *__forwarding;//指向自身类型对象的指针
int __flags;//不用关心
int __size;//本身所占大小
int a;//被封装的 基本数据类型变量
};
复制代码
看看在main
函数中__Block_byref_a_0
被赋了什么值3d
//__block int a = 10;
__Block_byref_a_0 a = {(void*)0,
&a,
0,
sizeof(__Block_byref_a_0),
10
};
复制代码
图中能够看出来,10被存储到了block内部__Block_byref_a_0
对象的成员变量int a上。__Block_byref_a_0
对象里面的成员变量__forwarding
实际上指向了__Block_byref_a_0
对象自身。 咱们来看block内的代码对于a的赋值是如何操做的
为何用a->__forwarding->a
,而不是a->a
直接拿到int a
,经过__forwarding
转一圈有什么用意?这个等会解答。
这样__block
的底层实现就说完了。
上面,咱们知道了经过 __block int a = 10
定义以后,这个a
底层是一个__Block_byref_a_0
对象,数值10存放在这个对象内部的成员变量int a
上面。可是咱们在写代码的时候,能够直接经过__Block_byref_a_0
对象a来赋值,那么在block定义初始化结束,完成变量捕获以后,oc代码中再次经过a
访问到的究竟是什么呢?例以下面
咱们先来看一份代码案例
**********************testVC.m**********************
#import "testVC.h"
@implementation testVC typedef void(^CLBlock)(void);
struct __Block_byref_a_0 {
void *__isa;
struct __Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
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;
struct __Block_byref_a_0 *a; // by ref
};
struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void *copy;
void *dispose;
};
- (void)viewDidLoad {
[super viewDidLoad];
[self blockTest];
}
- (void)blockTest {
__block int a = 10;
CLBlock myblock = ^{
a = 20;
NSLog(@"此时在在myblock内部的oc代码里直接经过a访问的内存空间是:%p",&a);
};
struct __main_block_impl_0 *tmpBlock = (__bridge struct __main_block_impl_0 *)myblock;
NSLog(@"myBlock通过初始化,完成变量捕获以后,其内部的[__Block_byref_a_0 *] a = %p",tmpBlock->a);
NSLog(@"此时在在myblock外部的oc代码里直接经过a访问的内存空间是:%p",&a);
myblock();
}
@end
*************************运行结果************************
2019-09-04 21:28:07.189104+0800 BT[30733:3968805] myBlock捕获完变量以后,[__Block_byref_a_0 *] a = 0x7ffeede8f840
2019-09-04 21:28:07.189221+0800 BT[30733:3968805] 此时在在block外部的oc代码里直接经过a访问的内存空间是:0x7ffeede8f858
2019-09-04 21:28:07.189296+0800 BT[30733:3968805] 此时在在block内部的oc代码里直接经过a访问的内存空间是:0x7ffeede8f858
复制代码
从打印咱们看到,myblock
内部的[__Block_byref_a_0 *] a
指向的地址是0x7ffeede8f840
,以后咱们在任意地方经过a
访问的内存地址是0x7ffeede8f858
,十六进制下它们地址相差了0x18
,也就是十进制下的24个字节。
从示意图能够看出,经过[__Block_byref_a_0 *] a
的地址往高地址走24
个字节,正好是它内部封装的那个int a
。也就是说咱们在oc代码里面完成了myblock
的初始化以及 __block变量的捕获以后,只能经过a
访问到被封装在 __ Block_byref_a_0 *
内部的这个int a
的内存空间。
苹果这么作的意图我猜想是想向开发者隐藏__ Block_byref_a_0 *
的存在,但愿开发者把__block int a
就当成一个普通的int a
来看待。苹果吗,老是这么小家子气,能够理解。(此处纯属自我发挥,还待大牛给出正解:)
咱们知道,若是block捕获一个基础类型的auto
变量,是不用考虑内存管理的。可是__block
的本质做用,是将所修饰的对象包装成一个__ Block_byref_xx_x *
,而后进行捕获,而__ Block_byref_xx_x *
本质上也是一个对象,所以确定须要处理它的内存管理问题。
咱们已经知道,若是一个block位于栈空间上,那么是不须要考虑被它所捕获的对象类型的
auto
变量的内存管理问题的。所谓的内存管理,是针对建立在堆空间上的oc对象而言的,由于咱们做为开发者,只可以管理堆上的空间。栈空间的内存是由系统管理的,不用咱们操心。
关于内存管理问题这里,咱们所讨论的问题须要考虑三个关键因素:__block
、__weak
、对象变量
、基本类型变量
,他们合法的组合有以下几种:
基本类型变量
对象变量
__weak
+ 对象变量
__block
+ 基本类型变量
__block
+ 对象变量
__block
+ __weak
+ 对象变量
我在【对象类型的auto变量捕获】这一篇里面详细分析了一个对象类型的auto
变量被block捕获时的内存管理过程,上面的一、二、3这三种场景已经获得了说明。下面咱们来分析一下四、五、6这三种场景。
__block
+ 基本类型变量
首先代码上一份
int main(int argc, const char * argv[]) {
@autoreleasepool {
CLBlock myblock;
{
__block int a = 10;
myblock = ^ {
a = 20;
};
}
myblock();
}
return 0;
}
复制代码
编译以后block相关的底层结构以下
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
};
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*/♥️♥️♥️♥️♥️♥️
);
}
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*);
}
复制代码
咱们知道__block int a = 10;
这句代码的做用,是将int a
包装在struct __Block_byref_a_0
内部,这样block实际上捕获的是这个struct __Block_byref_a_0
,它能够被看成一个对象来看待,因此内存管理上面,最终仍然是经过_Block_object_assign
和_Block_object_dispose
这两个函数来处理,可是能够看到这两个函数的最后一个参数是8
(对于对象类型的捕获,传递的参数是3
),这个参数代表了即将要处理的是一个struct __Block_byref_a_0
,由于它是没有__weak
和__strong
标记的,因此处理方式很简单,就是copy到堆上的时候,同时须要进行retain
,dispose的时候同时须要进行release
。
__block
+ 对象变量
上代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
CLBlock myblock;
{
CLPerson *person = [[CLPerson alloc] init];
__block CLPerson *blockPerson = person;
myblock = ^ {
blockPerson.age = 10;
};
}
myblock();
}
return 0;
}
复制代码
编译以后, __block CLPerson *blockPerson
的底层结构以下
struct __Block_byref_blockPerson_0 {
void *__isa;
__Block_byref_blockPerson_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
CLPerson *__strong blockPerson;
};
复制代码
从这个结构能够看出两点变化:
__block
修饰后,底层所生成的__Block_byref_xxx_x
结构体里面多了两个函数指针,__Block_byref_id_object_copy
和__Block_byref_id_object_dispose
。__Block_byref_xxx_x
内部之后,默认是被__strong
修饰的。上面发现的两个新函数指针__Block_byref_id_object_copy
和__Block_byref_id_object_dispose
就是当__Block_byref_xxx_x
被拷贝到堆空间的时候,以及将要被系统释放的时候调用的。咱们能够在main函数里面找到它们的最终赋值,分别是__Block_byref_id_object_copy_131
和__Block_byref_id_object_dispose_131
,它们的定义以下
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
复制代码
咱们考到,其实最终仍是调用了_Block_object_assign
和_Block_object_assign
这两个函数,二从参数能够看出,它们所要处理的对象就是__Block_byref_id_object_dispose
内部所封装的对象类型变量,也就是咱们代码中的CLPerson *blockPerson
,由于默认blockPerson
是被__strong
修饰的,因此接下来对于blockPerson
的内存管理方式就和咱们以前所分析过的是同样的。
__block
+ __weak
+ 对象变量
上代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
CLBlock myblock;
{
CLPerson *person = [[CLPerson alloc] init];
__block __weak CLPerson *weakBlockPerson = person;
myblock = ^ {
weakBlockPerson.age = 10;
};
}
myblock();
}
return 0;
}
复制代码
这里就直接给出编译以后的底层结构struct __Block_byref_xxx_x
来进行对比
struct __Block_byref_weakBlockPerson_0 {
void *__isa;
__Block_byref_weakBlockPerson_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
CLPerson *__weak weakBlockPerson;
};
复制代码
由于咱们显式地给对象变量加上了__weak
,所以struct __Block_byref_xxx_x
内部封装的就是一个指向对象的弱指针CLPerson *__weak weakBlockPerson
。根据上面的分析,最后一样进入到_Block_object_assign
和_Block_object_assign
这两个函数进行处理,处理方式再也不赘述。
最后在经过图例在梳理一下
最后再来解决那个咱们中篇遗留的问题:__forwarding
的做用 从上图能够很清晰的看出,当__Block_byref_xxx_x
(假设为A)从栈空间被拷贝到堆空间(假设堆上的那一份为B)的时候,栈上A的__forwarding
指针会被指向堆空间上的B,而B自己的__forwarding
仍然指向B本身,由于在底层访问__Block_byref_xxx_x
所封装的目标变量,是经过__Block_byref_xxx_x
->__forwarding
->目标变量,这样,不管咱们访问入口对象__Block_byref_xxx_x
是在栈上仍是在堆上,都能保证最终访问到的目标变量是堆空间上的那一份。这样的设计就正好契合了堆空间上的__Block_byref_xxx_x
对象存在的目的。
到此,关于__block
的底层实现就分析到这里。