Block使用及原理探究


出于需要,本文分析时引用苹果开源的libclosure-74 源码,请点击对比查看苹果libclosure源码

一、Block概念

1.1 名词解释

也成为匿名函数,是将函数及其执行上下文封装起来的对象。有参数和返回值。

Block的结构是:__block_impl

struct __block_impl {
void *isa; // isa 对象
int Flags;
int Reserved;
void *FuncPtr;//函数指针
}

1.2 分类

根据Block 所属的libclosure 所示,一共有6种。具体操作:进入苹果libclosure源码,选择最新的libclosure-74,下载后打开工程文件,搜索NSBlock得知具体6种如下:

void * _NSConcreteStackBlock[32] = { 0 };
void * _NSConcreteMallocBlock[32] = { 0 };
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 };
void * _NSConcreteWeakBlockVariable[32] = { 0 };

其中:NSConcreteStackBlockNSConcreteMallocBlockNSConcreteGlobalBlock 是常用到的栈区、堆区、全局区的block,其余3种为系统级别,实际开发较少会用到,所以先不展开说明。

1.2.1 NSGlobalBlock(全局Block)

当生成的block是独立的,不引用外部变量或者作为参赛参与其他函数,处于全局区,是为全局block

void (^block)(void) = ^{
NSLog(@"hello world!");
};
block();
NSLog(@"block %@", block);
2020-05-25 03:21:33.996335+0800 DDD[12987:411279] hello world!
2020-05-25 03:21:33.996513+0800 DDD[12987:411279] block <__NSGlobalBlock__: 0x10e714050>

1.2.2 NSStackBlock(栈区)

当block 被copy 前,会处于栈区

以下为相关示例:

__block int a = 1;

NSLog(@"%@", ^{
NSLog(@"aaa %d", a);
});

结果如下:

2020-05-25 11:36:10.746965+0800 DDD[14013:600887] <__NSStackBlock__: 0x7ffeeb06b188>

1.2.3 NSMallocBlock(堆区)

当block 引用外部变量,此时block 会从全局区移动到堆区

__block int a = 10;
void (^block)(void) = ^{
NSLog(@"a %d", a);
};
block();
NSLog(@"block %@", block);

2020-05-25 03:22:31.565749+0800 DDD[13022:421048] a       10
2020-05-25 03:22:31.565921+0800 DDD[13022:421048] block <__NSMallocBlock__: 0x600000ddc1b0>

1.3 Block 的常见使用

  1. 作为局部变量

    returnType (^blockName)(parameterTypes) = ^returnType(parameters) {
    // do sth..
    };
  2. 作为属性

    @property (nonatomic, copy, nullability) returnType (^blockName)(parameterTypes);c
  3. 作为函数声明中的参数

    - (void)someMethodThatTakesABlock:(returnType (^)(parameterTypes))blockName;
  4. 作为函数调用中的参数

    [someObject someMethodThatTakesABlock:^returnType (parameters) {
    // do sth
    }];
  5. 作为typedef

    typedef returnType (^TypeName)(parameterTypes);
    TypeName blockName = ^returnType(parameters) {
    // DO STH
    };

二、循环引用及解决

2.1 循环引用的产生

举一个常见的循环引用的例子

- (void)viewDidLoad {
[super viewDidLoad];

self.view.backgroundColor = [UIColor purpleColor];
self.name = @"hello";

// ① self 持有block
self.block = ^{
// ② block 又持有self
NSLog(@"__name %@", self.name);
};
self.block();
}

-(void)dealloc{
NSLog(@"进入dealloc");
}

当示例代码中①处 self 持有了属性 block,而 ②种 block 代码块又持有了 self,这样会导致相互引用,最终系统在执行 dealloc 方法时,向任何一个变量发送release消息时,两者都因为引用其他变量而无法释放,最终会引起内存泄漏。示意图如下:

2.2 循环引用的解决

此时我们只讨论由block 引起的循环引用

2.2.1 使用weak 修饰

可以考虑对当前持有block 的对象,进行weak 修饰,创建一个弱引用指向它(原理为加入到弱引用计数表,而非引用技术表),但是并不会增加引用计数。

当前案例中,引用block 的是self,所以创建一个weak 对象 weakSelf。

如下所示,即可跳出 self - block - self 的循环引用

__weak typeof(self) weakSelf = self;
self.block = ^{
NSLog(@"__name %@", weakSelf.name);
};
self.block();

2.2.2 weak-strong-dance(多级block)

在多级block 的场景下,弱引用会造成一些意外的情况。

对上面的例子做一些小改动,对block代码块执行的NSLog 函数,做一个延迟3秒执行操作,并让这个执行在页面消失之后,也就是dealloc 之后,看看是否还能打印 weakSelf.name

新的代码如下:

- (void)viewDidLoad {
[super viewDidLoad];

self.view.backgroundColor = [UIColor purpleColor];
self.name = @"hello";

__weak typeof(self) weakSelf = self;
self.block = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"__name %@", weakSelf.name);
});
};
self.block();
}

-(void)dealloc{
NSLog(@"进入dealloc");
}

打印结果如下:

2020-05-25 12:43:05.876909+0800 DDD[14542:686386] 进入dealloc
2020-05-25 12:43:06.475894+0800 DDD[14542:686386] __name (null)

可见,在进入dealloc 之后,添加的弱引用,已经被释放掉了,而设计的3秒后执行 weakSelf.name 也因为 weakSelf 为 nil 了,所以打印为null

解决:在代码块内,添加临时的强引用,将weakSelf 添加到强引用表里,会让dealloc 延迟执行,而当执行完之后block 会对临时的强引用进行释放,避免了循环引用的产生。

代码如下:

- (void)viewDidLoad {
[super viewDidLoad];

self.view.backgroundColor = [UIColor purpleColor];
self.name = @"hello";
NSLog(@"初始name %@", self.name);
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"__name %@", strongSelf.name);
});
};
self.block();
}

-(void)dealloc{
NSLog(@"进入dealloc");
}

打印结果及顺序:

2020-05-25 12:49:42.141133+0800 DDD[14610:698670] 初始name  hello
2020-05-25 12:49:47.634568+0800 DDD[14610:698670] __name hello
2020-05-25 12:49:47.635013+0800 DDD[14610:698670] 进入dealloc

从结果可以看出,12:49:42 秒进入本页面,47秒后 进入打印strongSelf.name 成功,同时立刻执行dealloc方法。可见修改是成功的。

2.2.3 用临时VC 变量替代

在上文中,都是通过weak 对self修饰,来避免循环引用,实际上,还可以对self 所代替的Controller 引入临时变量,来解决循环引用,话不多说,实战一下。

__block SecViewController *vc = self;
self.block = ^{

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"__name %@", vc.name);
vc = nil;
});
};
self.block();
  1. 创建一个临时变量vc,使用__block 修饰,因为后面会对他做修改。
  2. 在block内部进行打印属性,完毕后置空,跳出循环引用。

打印结果如下:

2020-05-25 12:54:28.262738+0800 DDD[14645:705623] 初始name  hello
2020-05-25 12:54:33.263402+0800 DDD[14645:705623] __name hello
2020-05-25 12:54:33.263773+0800 DDD[14645:705623] 进入dealloc

可见通过ViewController 来替代self 也算可用性的

2.2.4 将VC 作为block 变量替代

如果block 是可以引入参数的,可以将控制器self 传入进去,作为临时变量,打印后block 会自动销毁,无需担心循环引用,具体如下:

创建block ,引入当前VC 的引用,的代码如下

@property (copy, nonatomic) void (^block)(SecViewController *);

执行函数为:

self.block  = ^(SecViewController *vc){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"__name %@", vc.name);
});
};
self.block(self);

此时通过self.block(self) 传入self 的引用,block 内部会自动创建一个新的临时变量指向self,该临时变量使用后会销毁。打印机如果如下;

2020-05-25 12:57:17.929178+0800 DDD[14671:710117] 初始name  hello
2020-05-25 12:57:22.929717+0800 DDD[14671:710117] __name hello
2020-05-25 12:57:22.930072+0800 DDD[14671:710117] 进入dealloc

三、底层探索

3.1 Block 的本质

Block 是一种匿名函数,它的类型是一种对象,本质是结构体。

为了证明这一点,对main.m 进行cpp 源码分析,现在main.m 生成一个block

void (^block)(void) = ^{
};
block();

进入项目文件下,输入编译命令:xcrun -sdk iphonesimulator clang -rewrite-objc main.m 得到main.cpp

打开查看相关cpp 实现如下:

  • OC 代码如下:

    void (^block)(void) = ^{
    };
    block();
  • C++ 源码如下:

    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    将代码精简一下成为:

    void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
    (block)->FuncPtr)(block);

可见主函数为__main_block_impl_0,在C++ 源码中的部分为:

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;
}
};

可见,我们平常使用的代码块block代码块是一个结构体,有以下几个部分组成

  • block 实现 - __block_impl,这个是个指针函数,生成后会返回
  • block 描述信息 - __main_block_desc_0
  • 内部构造函数 - __main_block_impl_0。主要执行了 生成薪的impl 的函数赋值(如isa,flags,funptr)

继续挖, __block_impl 类型的结构体,实现如下:

struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

因为有熟悉的isa 的存在,可知其也算个对象类的结构体。

3.2 Block 自动捕获外部变量

接下来看block 是如何捕获外界变量的,一共分为3重大的类型:

  • 局部变量
  • 局部静态变量
  • 全局变量和全局静态变量

3.2.1 局部变量捕获

局部变量的捕获,是指的捕获

此时生成一个变量 int a,在代码块对其进行引用,如下:

int a = 10;
void (^block)(void) = ^{
NSLog(@"%d", a);
};
block();

继续进行cpp 编译,发现int main 函数下,变成了如下

int a = 10;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

通过精简,其内容如下:

int a = 10;
block = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a));
block->FuncPtr(block);

可见__main_block_impl_0传入了新的参数a,接下来看如何实现,新的 main_block_impl_0 结构体如下:

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

可见这里比起之前的结构体,生成了一个新的属性a ,在构造函数里,发现了__main_block_impl_0中也多了一句a(_a) 的代码,可以猜测是生成了一个新的a,将传入的内部属性_a 赋值给 a

继续看main_block_func_0 的实现如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_ym1km6hs1_510g123chch8bm0000gp_T_main_b68344_mi_0, a);
}

此时静态函数__main_block_func_0的内部对传入的block 进行了处理——接受了其内部的变量a——通过自身新生成一个a,就是这行代码

int a = __cself->a; // bound by copy

自动生成的注释很清晰的说明了原理,通过copy绑定新的,所以说是值捕获。最终通过NSLog 打印新的变量a

3.2.2 局部静态变量捕获

局部静态变量的捕获,是指针捕获

接下来尝试一下,在block内部生成一个用static修饰的局部静态变量,看看如何捕获

static NSInteger num = 3;

NSInteger (^block)(NSInteger) = ^(NSInteger n){

return n * num;
};

num = 1;
NSLog(@"%ld", (long)block(2));

进入项目文件下,输入编译命令:xcrun -sdk iphonesimulator clang -rewrite-objc main.m 得到main.cpp

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSInteger *num;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger *_num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static long __main_block_func_0(struct __main_block_impl_0 *__cself, NSInteger n) {
NSInteger *num = __cself->num; // bound by copy


return n * (*num);
}

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, char * argv[]) {
NSString * appDelegateClassName;
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

static NSInteger num = 3;

NSInteger (*block)(NSInteger) = ((long (*)(NSInteger))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &num));
// 精简为 *block = __main_block_impl_0(&__main_block_func_0, &__main_block_desc_0_DATA, &num);

num = 1;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_ym1km6hs1_510g123chch8bm0000gp_T_main_ef866b_mi_0, (long)((NSInteger (*)(__block_impl *, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 2));

appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
}
return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

变化:

  • 生成了新的整形指针NSInteger *num;

  • 将传入的NSInteger *_num 赋值给num

  • 代码块调用有所不同,如下:

    // 原函数
    NSInteger (*block)(NSInteger) = ((long (*)(NSInteger))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &num));

    // 精简后
    __main_block_impl_0(&__main_block_func_0, &__main_block_desc_0_DATA, &num)

    可以清晰的看出来,此时传入的num 是一个指针,可见,是进行了指针拷贝,而不是值拷贝,所以这里num 得到了改变,打印结果会是2

2020-05-27 00:11:06.899579+0800 DDD[25215:2116517] 2

3.3.3 全局变量、全局静态变量的捕获

全局变量、全局静态变量的捕获是直接取值

这次来点刺激的,把所有用的到的都写到block 里面,再验证下cpp 会发生什么

static NSInteger num3 = 300;            // 静态变量
NSInteger num4 = 3000; // 全局静态变量

int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {

NSInteger num = 30; // 局部变量
static NSInteger num2 = 3; // 局部静态变量

__block NSInteger num5 = 30000; // __block 修饰的变量

void(^block)(void) = ^{
NSLog(@"%ld", (long)num);
NSLog(@"%ld", (long)num2);
NSLog(@"%ld", (long)num3);
NSLog(@"%ld", (long)num4);
NSLog(@"%ld", (long)num5);
};

block();

appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

进入项目文件下,输入编译命令:xcrun -sdk iphonesimulator clang -rewrite-objc main.m 得到main.cpp

不管了,就先一股脑儿的贴上来吧,接下来分析:

static NSInteger num3 = 300;		// 全局变量
NSInteger num4 = 3000; // 静态全局变量

struct __Block_byref_num5_0 { // __block 修饰的变量
void *__isa;
__Block_byref_num5_0 *__forwarding;
int __flags;
int __size;
NSInteger num5;
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSInteger num;
NSInteger *num2;
__Block_byref_num5_0 *num5; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger _num, NSInteger *_num2, __Block_byref_num5_0 *_num5, int flags=0) : num(_num), num2(_num2), num5(_num5->__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_num5_0 *num5 = __cself->num5; // bound by ref
NSInteger num = __cself->num; // bound by copy
NSInteger *num2 = __cself->num2; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_ym1km6hs1_510g123chch8bm0000gp_T_main_8fd68e_mi_0, (long)num);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_ym1km6hs1_510g123chch8bm0000gp_T_main_8fd68e_mi_1, (long)(*num2));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_ym1km6hs1_510g123chch8bm0000gp_T_main_8fd68e_mi_2, (long)num3);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_ym1km6hs1_510g123chch8bm0000gp_T_main_8fd68e_mi_3, (long)num4);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_ym1km6hs1_510g123chch8bm0000gp_T_main_8fd68e_mi_4, (long)(num5->__forwarding->num5));

}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->num5, (void*)src->num5, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->num5, 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, char * argv[]) {
NSString * appDelegateClassName;
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

NSInteger num = 30;
static NSInteger num2 = 3;

__attribute__((__blocks__(byref))) __Block_byref_num5_0 num5 = {(void*)0,(__Block_byref_num5_0 *)&num5, 0, sizeof(__Block_byref_num5_0), 30000};

void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, num, &num2, (__Block_byref_num5_0 *)&num5, 570425344));

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
}
return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

分析:我们看到了,num、num2、num5 在block 的构造函数__main_block_impl_0 中都有了各自的实现,有的是取值,有的是通过forwarding 传递指针。

但是num3,和num4 呢?通篇下来,没有做任何处理,在block 函数内部,通过NSLog 直接取值了……

NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_ym1km6hs1_510g123chch8bm0000gp_T_main_8fd68e_mi_2, (long)num3);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_ym1km6hs1_510g123chch8bm0000gp_T_main_8fd68e_mi_3, (long)num4);

结论:静态变量、全局静态变量,因为他们存在全局存储区,占用静态的单元,所以block 调用时是直接把值取过来用,并不需要指针引用。

3.3.4 小结

捕获变量的流程,除了静态变量和全局变量以外,都是block 在内部自动生成一个新的变量,用来接收外部变量。

3.3 Block 为什么要捕获Block()

block() 这种写法和普通的函数不一样,通过上文可以看到,最终实现的代码为

block->FuncPtr(block);

分析一下,这里是 block 结构体执行了其内部函数指针指向的函数,即声明一个函数式的属性,在任何想要调用的的地方调用。最终我们的确是调用了。

3.4 __block 的原理

为了让外部变量在block 内部得到更改,我们通常在外部对变量使用__block 的修饰符,究竟发生了什么呢?

那么这次,对外部加上__block 试试

__block int a = 10;
void (^block)(void) = ^{
a++;
NSLog(@"%d", a);
};
block();

3.4.1 CPP分析

可以看到,构造函数结构体有了变化;

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

与之前的变量不一样,生成了一个__Block_byref_a_0 *a; // by ref 的结构体指针对象,点击查看,结构如下:

struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};

可见和类的结构类似,有isa、数据a、内存大小_size、类似链表的__forwarding指针。结合__main_block_impl_0 的结构,可以清楚的看到,内部创建的一个结构体指针__Block_byref_a_0 *a,而 __a 的指针指向了 a。即,这里产生了一次指针拷贝。

接下来看看函数的实现:

  (a->__forwarding->a)++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_ym1km6hs1_510g123chch8bm0000gp_T_main_acd650_mi_0, (a->__forwarding->a));
}
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*/);}

3.4.2 源码分析

在上文中,我们看到关键函数是_Block_object_assign,打开libclosure 查看相应的函数实现,得到如下:

// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
// to do the assignment.
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void **dest = (const void **)destArg;
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_OBJECT:
/*省略*/
break;

case BLOCK_FIELD_IS_BLOCK:
/*省略*/
*dest = _Block_copy(object);
break;

case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
/*******
// copy the onstack __block container to the heap
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__block ... x;
__weak __block ... x;
[^{ x; } copy];
********/

*dest = _Block_byref_copy(object);

注释简单的翻译是:当block 或者block 引用持有对象时,他们的拷贝常规帮助者会使用它们的入口点来做分配工作。

里面的是现实对持有对象类型的switch,重点关注BLOCK_FIELD_IS_BYREF,注释已经说了

copy the onstack __block container to the heap

拷贝栈内__block 容器到堆

太棒了, 这就是我们需要的实现函数,即对__block 的方法做出相应,拷贝到堆区,再来看函数 _Block_byref_copy(object) 具体的实现:

static struct Block_byref *_Block_byref_copy(const void *arg) {
struct Block_byref *src = (struct Block_byref *)arg;

if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;

// 问题 - __block 修饰变量 block具有修改能力
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy; // patch stack to point to heap copy

copy->size = src->size;
}

在这里可以看到,具体的实现了:

  1. 拷贝新的对象到堆区,并置空isa

    struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
    copy->isa = NULL;
  2. 将新的拷贝对象的flag 与之前栈区对象的flags的相等

    // byref value 4 is logical refcount of 2: one for caller, one for stack
    copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
  3. 将新的对象指向原对象,即进行指针拷贝——这个是__block 的关键,新的指向原对象,与原地址指向同一个内存空间,故具备修改能力!

    // 问题 - __block 修饰变量 block具有修改能力
    copy->forwarding = copy; // patch heap copy to point to itself
    src->forwarding = copy; // patch stack to point to heap copy
  4. 新对象大小与原对象大小相等

    copy->size = src->size;

总结:可见生成的a 是一个__Block_byref_a_0 类型结构体引用,而不是值拷贝的引用。**__block 在作用于内部生成了变量的指针,通过改变指针指向的地址,来即改变原变量。**

原理图如下:

四、签名

4.1 block 结构分析

block 的结构如下:

struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
BlockInvokeFunction invoke;
struct Block_descriptor_1 *descriptor; //
// imported variables
};
typedef void(*BlockInvokeFunction)(void *, ...);

分析如下:

  • isa- 指向block父类的指针

  • 用来描述block 对象的flags

    // Values for Block_layout->flags to describe block objects
    enum {
    BLOCK_DEALLOCATING = (0x0001), // runtime
    BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
    BLOCK_NEEDS_FREE = (1 << 24), // runtime
    BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
    BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
    BLOCK_IS_GC = (1 << 27), // runtime
    BLOCK_IS_GLOBAL = (1 << 28), // compiler
    BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
    BLOCK_HAS_SIGNATURE = (1 << 30), // compiler
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
    };
  • 预留数值

  • 函数调用invoke

  • 描述信息 struct Block_descriptor_1 *descriptor; //

    此处的描述信息,是动态的,如果flags 中BLOCK_HAS_COPY_DISPOSE 值存在,则存在Block_descriptor_2,其结构如下;

    // 可选
    #define BLOCK_DESCRIPTOR_2 1
    struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    BlockCopyFunction copy;
    BlockDisposeFunction dispose;
    };

    包含的是拷贝函数copy以及销毁函数 dispose

    如果flags 中BLOCK_HAS_SIGNATURE 存在,即block 包含有签名存在,则存在Block_descriptor_3

    方法签名就在3里面:

    • 方法签名
    • 方法布局
    #define BLOCK_DESCRIPTOR_3 1
    struct Block_descriptor_3 {
    // requires BLOCK_HAS_SIGNATURE
    const char *signature;
    const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
    };
  • // 引入的变量等

4.2 block签名的访问

上文中提到的Block_descriptor_2Block_descriptor_3,都可以通过 Block_descriptor_1 的内存偏移进行访问到,block 的签名为@?

4.2.1 代码部署:

先设定一个最简单的代码块块用来检查:

void(^block)(void) = ^{
};
block();

4.2.2 汇编断点配置

  • 连接真机。配置汇编监控打开:Xcode——Debug——Debug Workflow——Always show Dissembly

  • 对block 代码块打断点,运行程序

  • 会跳转到汇编的main 步骤,通过Step Over键进入到 objc_retainBlock函数下

  • 打印当前x0寄存器:register read x0得到当前运行的block 函数的位置为0x0000000104afc028

  • 打印地址为0x0000000104afc028,得到block的类型、签名和内部实现地址:

    <__NSGlobalBlock__: 0x104afc028>
    signature: "v8@?0"
    invoke : 0x104afa01c (/private/var/containers/Bundle/Application/3F9F1F87-526B-499C-93CE-784F88CE7DB4/DDD.app/DDD`__main_block_invoke)

    具体的细节请见下图

    查看方法签名

4.2.1 访问 Block_descriptor_2

当block 的block_layout结构中flag 参数BLOCK_HAS_COPY_DISPOSE 值为1 即可通过内存偏移访问到,相关原理可以见下方代码:

static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
uint8_t *desc = (uint8_t *)aBlock->descriptor;
desc += sizeof(struct Block_descriptor_1);
return (struct Block_descriptor_2 *)desc;
}

函数实现中,第3行desc += sizeof(struct Block_descriptor_1); 即内存地址偏移Block_descriptor_1 的内存位,即可访问到Block_descriptor_2

4.2.2 访问 Block_descriptor_3

同理,在该block的flag 值 BLOCK_HAS_SIGNATURE 为正时,亦可以通过Block_descriptor_1的内存偏移而访问到Block_descriptor_3,当然,如果此时BLOCK_HAS_COPY_DISPOSE 值也为1,必须叠加Block_descriptor_2 的偏移位

static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock)
{
if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
uint8_t *desc = (uint8_t *)aBlock->descriptor;
desc += sizeof(struct Block_descriptor_1);
if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
desc += sizeof(struct Block_descriptor_2);
}
return (struct Block_descriptor_3 *)desc;
}

五、block的copy 分析

5.1 源码

libclosure 源码中,runtime.cpp这一页,block_copy 的函数实现如下所示:

void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;

if (!arg) return NULL;

// The following would be better done as a switch statement
aBlock = (struct Block_layout *)arg;
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
else {
// Its a stack block. Make a copy.
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
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
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
result->isa = _NSConcreteMallocBlock;
return result;
}
}

5.2 分析

我们知道,基于block 的性质,在block 的使用中,当block被copy 时,会从堆区到栈区进行copy,具体发生了什么,不妨读一读源码,看看苹果工程师是如何设计blockcopy实现:

  1. 先判断block 的类型:先强转arg获取block_layout ,判断其flags

    aBlock = (struct Block_layout *)arg;
  2. 如果是堆block,即BLOCK_NEEDS_FREE:创建的对象需要程序员手动释放——由于此时block 空间已经初始化,所以不做拷贝,只增加引用计数即可,如下;

    if (aBlock->flags & BLOCK_NEEDS_FREE) {
    // latches on high
    latching_incr_int(&aBlock->flags);
    return aBlock;
    }
  3. 如果是全局block –BLOCK_IS_GLOBAL,直接返回

    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
    return aBlock;
    }
  4. 如果是栈区block:进行一次拷贝操作,拷贝到——>堆区

        else {
    // Its a stack block. Make a copy.
    struct Block_layout *result =
    (struct Block_layout *)malloc(aBlock->descriptor->size);
    if (!result) return NULL;
    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
    result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
    result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
    _Block_call_copy_helper(result, aBlock);
    // Set isa last so memory analysis tools see a fully-initialized object.
    result->isa = _NSConcreteMallocBlock;
    return result;
    }
    1. 创建一个新的Block_layout

      struct Block_layout *result =
      (struct Block_layout *)malloc(aBlock->descriptor->size);
    2. 如果有函数指针调用,将新的Block_layout 指针指向原函数实现,即替换原方法实现:

      #if __has_feature(ptrauth_calls)
      // Resign the invoke pointer as it uses address authentication.
      result->invoke = aBlock->invoke;
      #endif
    3. 把原来aBlock 的数据通过内存拷贝到堆区

      memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
    4. 重置引用计数

      result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
      result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
      _Block_call_copy_helper(result, aBlock);
    5. 重置isa 指向到一个新的堆block

      result->isa = _NSConcreteMallocBlock;

5.3 小结

5.3.1 操作copy 结果

  1. 对栈block进行copy:拷贝一份到堆区
  2. 对全局block进行copy:仍是全局block
  3. 对堆block进行copy:增加引用计数

5.3.2 总结图

俗话说的好,有图有真相,将上述流程汇总成一张图,是这样的

六、release 与dispose

当block 需要释放时,系统内做了哪些工作呢?先看看_Block_object_dispose 函数如下:

void _Block_object_dispose(const void *object, const int flags) {
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
// get rid of the __block data structure held in a Block
_Block_byref_release(object);
break;
case BLOCK_FIELD_IS_BLOCK:
_Block_release(object);
break;
case BLOCK_FIELD_IS_OBJECT:
_Block_release_object(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
break;
default:
break;
}
}

可以看到,除了BLOCK_FIELD_IS_BYREF类型的情况,此种情况为通过__block修饰变量进入,其他情况直接调用release 或者不用操作即可。

BLOCK_FIELD_IS_BYREF 的情况下,需要调用release 方法,即_Block_byref_release(object),继续查看如下

static void _Block_byref_release(const void *arg) {
struct Block_byref *byref = (struct Block_byref *)arg;

// dereference the forwarding pointer since the compiler isn't doing this anymore (ever?)
byref = byref->forwarding;

if (byref->flags & BLOCK_BYREF_NEEDS_FREE) {
int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK;
os_assert(refcount);
if (latching_decr_int_should_deallocate(&byref->flags)) {
if (byref->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
(*byref2->byref_destroy)(byref);
}
free(byref);
}
}
}

释放的过程比较简单:

  1. isa 指回原对象

    byref = byref->forwarding;
  2. 生成一个新的Block_byref_2 指针``byref2,指向byref`下个内存位

    struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
  3. 通过byref2 向原blog引用发送摧毁消息

    (*byref2->byref_destroy)(byref);
  4. 释放原block(byref)

    free(byref);

七、总结

本片篇幅较多,主要从block 的结构、block 循环引用的原理解决、block捕获外部变量,__block 究竟做了什么,以及源码层面上做了大量的分析。总的来说block是很巧妙的一项涉及,合理地用好,可以帮我们极大地提高工作效率。


文章作者: 李佳
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 李佳 !
评论
 上一篇
计算机视觉【02-OpenGL概念篇】 计算机视觉【02-OpenGL概念篇】
一、计算机视觉简介1.1 概念:计算机视觉(Computer Vision)是一门跨学科科学,用于处理计算机如何从数码相片或视频中获取高层次理解。从工程学的角度来讲,它的目的是理解并将人类视觉系统的工作任务自动化。 计算机视觉(Compu
2020-07-06 李佳
下一篇 
【数据结构与算法】查找算法(二)平衡二叉树 【数据结构与算法】查找算法(二)平衡二叉树
一、概念 平衡二叉树 (Self-Balancing Banary Search Tree 或者 Height-Balanced Binary Search Tree),是一种二叉排序树,其中每一个结点的左子树和右子树的高度差至多等于1。
2020-05-15 李佳
  目录