iOS 底层原理|Block 详解
一、Block 简介
块对象是 C 级语法和运行时功能。它们类似于标准 C 函数,但是除了可执行代码之外,它们还可能包含与自动(堆栈)或托管(堆)内存的变量绑定。因此,一个块可以维护一组状态(数据),当执行时它可以用来影响行为。
您可以使用块来构成函数表达式,这些函数表达式可以传递给API,可以选择存储并由多个线程使用。块作为回调特别有用,因为该块既包含要在回调上执行的代码,又包含执行期间所需的数据。
OS X v10.6 开发人员工具附带提供了 GCC 和 Clang 中的块。您可以在 OS X v10.6 和更高版本以及 iOS 4.0 和更高版本中使用块。块运行时是开源的,可以在 LLVM 的
compile-rt
子项目存储库中找到。块也已作为 N1370 提交给 C 标准工作组:Apple对C的扩展。由于 Objective-C 和 C++ 都从 C 派生而来,因此块被设计为可用于所有三种语言(以及 Objective-C++ )。
Block 本质是一个封装了函数调用以及函数调用环境的 OC 对象,它主要由一个 isa
指针和一个 impl
函数指针和一个 descriptor
组成。 有点类似 C 里面的函数指针。
二、Block 用法
Block 基础表达式:
/**
* return_type 表示返回的对象/关键字等(可以是 void,并省略)
* blockName 表示 block 的名称
* var_type 表示参数的类型(可以是 void,并省略)
* varName 表示参数名称
*/
return_type (^blockName)(var_type) = ^return_type (var_type varName) { ... };
blockName(var);
1. Block 语法
1.1 当返回类型为 void
void (^blockName)(var_type) = ^void (var_type varName) { ... };
blockName(var);
// 可省略成
void (^blockName)(var_type) = ^(var_type varName) { ... };
blockName(var);
1.2 当参数类型为 void
return_type (^blockName)(void) = ^return_type (void) { ... };
blockName();
// 可省略成
return_type (^blockName)(void) = ^return_type { ... };
blockName();
1.3 当返回类型和参数类型都为 void
void (^blockName)(void) = ^void (void) { ... };
blockName();
// 可省略成
void (^blockName)(void) = ^{ ... };
blockName();
1.4 匿名 Block
Block 实现时,等号右边就是一个匿名 Block,它没有 blockName
,称之为匿名Block。
^return_type (var_type varName) { ... };
1.5 typedef
简化 Block 的声明
typedef return_type (^BlockTypeName)(var_type);
例子
// 声明
typedef void(^ClickBlock)(NSInteger index);
// 使用 1
@property (nonatomic, copy) ClickBlock clickBlock;
// 使用 2
(void)methodHandle:(ClickBlock)handle { ... }
2. 应用场景
2.1 响应事件
2.2 传递数据
2.3 链式语法
核心思想为将 Block 作为方法的返回值,且返回值的类型为调用者本身,并将该方法以 setter
的形式返回,这样就可以实现了连续调用,即为链式编程。
在 CaculateMaker.h
文件中声明一个方法 add
// CaculateMaker.h
@interface CaculateMaker : NSObject
@property (nonatomic, assign) CGFloat result;
- (CaculateMaker *(^)(CGFloat num))add;
@end
在 CaculateMaker.m
文件中实现 add
方法
#import "CaculateMaker.h"
@implementation CaculateMaker
- (CaculateMaker *(^)(CGFloat num))add {
return ^CaculateMaker *(CGFloat num) {
_result += num;
return self;
};
}
@end
使用该类
CaculateMaker *maker = [[CaculateMaker alloc] init];
maker.add(20).add(30);
三、Block 深入探究
1. Block 组成
我们在 main.m
写入代码:
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
int age = 20;
void (^block)(void) = ^{
NSLog(@"age is %d", age);
};
block();
}
return 0;
}
单独编译一下 main.m 文件,在终端执行命令:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
得到 main.cpp
文件,我们从文件中提取有用信息,整理上面的代码得
int main(int argc, char * argv[]) {
/* @autoreleasepool */ {
__AtAutoreleasePool __autoreleasepool;
int age = 20;
void (*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
age));
block->FuncPtr(block);
}
return 0;
}
其中 __main_block_impl_0
、__main_block_func_0
内容为
struct __block_impl { // __block_impl impl
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static struct __main_block_desc_0 { // __main_block_desc_0* Desc
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
struct __main_block_impl_0 {
struct __block_impl impl; //
struct __main_block_desc_0* Desc; //
int age;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) { // 构造函数
impl.isa = &_NSConcreteStackBlock;
...
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_3j_fvy599js5nnglvw7jslq09t80000gn_T_main_cef707_mi_0, age);
}
从上面我们可以看出 Block 内部能够正常访问外部的变量,这就引到了 Block 变量捕获机制( capture )。
2. Block 的变量捕获
2.1 局部变量 auto
局部变量 auto
(自动变量) ,我们平时写的局部变量,默认就有 auto
(自动变量,离开作用域就销毁)。比如:
int age = 20; // 等价于 auto int age = 20;
2.2 局部变量 static
static 修饰的局部变量 ,不会被销毁。由下面代码可以看得出来,Block 外部修改static 修饰的局部变量,依然能影响 Block 内部的值:
static int height = 30;
int age = 20;
void (^block)(void) = ^{
NSLog(@"age is %d height = %d",age,height);
};
age = 25;
height = 35;
block();
// 执行结果
age is 20 height = 35
这是为什么会改变呢?
因为 age
是直接值传递,height
传递的是 *height
,也就是说直接把内存地址传进去进行修改了。大家可以根据上面讲解内容尝试编译成 cpp 文件,去了解详情。缺点是会永久存储,内存开销大。
2.3 全局变量
全局变量 不能也无需捕获到 Block 内部,因为全局变量是存放在全局静态区的,直接访问就完事了。缺点也是内存开销大。
3. Block 类型
block类型 | 环境 |
---|---|
__NSGlobalBlock__ |
没有访问auto变量 |
__NSStackBlock__ |
访问了auto变量 |
__NSMallocBlock__ |
__NSStackBlock__ 调用了copy |
每一种类型的 block 调用 copy 后的结果如下所示
Block 的类 | 副本源的配置存储域 | 复制效果 |
---|---|---|
_NSConcreteGlobalBlock |
程序的数据区域 | 什么也不做 |
_NSConcreteStackBlock |
栈 | 从栈复制到堆 |
_NSConcreteMallocBlock |
堆 | 引用计数增加 |
在 ARC 开启时候,只会有 _NSConcreteGlobalBlock
和 _NSConcreteMallocBlock
类型的 Block,因为在 ARC 模式下,编译器会根据情况自动将栈上的 Block 复制到堆上(Block 自动调用了 copy
方法)。情况如下:
- Block 作为函数返回值时;
- 将 block 赋值给
__strong
指针时; - Block 作为 Cocoa API 中方法名含有
usingBlock
的方法参数时; - Block 作为 GCD API 的方法参数时;
所以在 ARC 下建议 Block 的声明写法:
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);
3.1 Block 类型最终继承于 NSObject
我们通过一段代码验证一下:
void (^block)(void) = ^{
NSLog(@"Hello Block");
};
NSLog(@"%@", [block class]);
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
block();
打印结果是:
2021-04-10 01:24:01.844162+0800 studyOC[40328:4640930] Hello Block
2021-04-10 01:24:01.844502+0800 studyOC[40328:4640930] __NSGlobalBlock__
2021-04-10 01:24:01.844541+0800 studyOC[40328:4640930] NSBlock
2021-04-10 01:24:01.844566+0800 studyOC[40328:4640930] NSObject
Block 的类型都是继承于 NSBlock
,最终是继承于 NSObject
。上面是验证了__NSGlobalBlock__
,Block 其他类型自行验证,方法一样。
这里需要注意一下,不要通过 clang
方式去将 OC 代码转换成 C++ 代码去验证上述结论,在 llvm x.0
开始实际上编译的成的文件不是 C++ 文件,是一个中间文件。这里牵扯到了 Runtime,所以转成 C++ 代码会有些不准确,以运行时为准。
3.2 Block 内部访问对象类型的 auto
变量
如果 Block 是在栈上,不会对 auto
变量产生强引用;但是
如果 Block 被拷贝到堆上时,
-
会调用 Block 内部的
copy
方法; -
copy
方法内部会调用_Block_object_assign
函数; -
_Block_object_assign
函数会根据auto
变量的修饰符(__strong
、__weak
、__unsafe_unretained
)做出相应的操作,如引用计数加一,形成强引用(retain
)或者弱引用。_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
如果 Block 从堆上移除,
-
会调用 Block 内部的
dispose
函数; -
dispose
函数内部会调用_Block_object_dispose
函数; -
_Block_object_dispose
函数会自动释放引用的auto
变量(release
)_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
4. __block
修饰符
__block
会在编译时候将修饰的变量包装成一个对象。我们来验证一下,我们把以下代码转成 C++ 代码:
__block int age = 18;
void (^block)(void) = ^{
NSLog(@"Hello Block %d", age);
};
block();
可观察 int
类型的 age
被 __block
包转成了一个对象:
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding; // 指向自己的指针
int __flags;
int __size;
int age;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
...
}
__block
可以用于解决 Block 内部无法修改 auto
变量值的问题;但是 __block
不能修饰全局变量、静态变量(static)。
4.1 __block
的内存管理
-
当 Block 在栈上时,并不会对
__block
变量产生强引用; -
当 Block 被 copy 到堆时,
会调用 Block 内部的copy
方法;
copy
方法内部会调用_Block_object_assign
函数;
_Block_object_assign
函数会对__block
变量形成强引用(retain)。_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
-
当 Block 从堆中移除时,
会调用 Block 内部的dispose
方法;
dispose
方法内部会调用_Block_object_dispose
函数;
_Block_object_dispose
函数会自动释放引用的__block
变量(release);_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
4.2 __block
的 __forwarding
指针
5. Block 循环引用
很多人在使用 Block 的时候经常发生循环引用,那这个循环引用怎么发生的,又是如何解决的,我们来分析分析。
5.1 导致循环引用的原因
循环引用问题就是两个对象相互持有,导致两个对象无法被释放。
同理,Block 发生循环引用就是,Block 持有(强引用)对象,对象持有(强引用) Block,导致对象 A 在堆中将要被释放时,发现还持有 B 对象,同时 B 对象也持有 A 对象,僵持在那里,谁都无法被释放。如图:
5.2 如果解决循环引用问题
那我们应该如何去解决这个问题呢?
a. 通过 __weak
、__unsafe_unretained
使 Block 对对象弱引用
- 使用
__weak
解决循环引用__weak
修饰的对象释放后会自动致为nil
;__weak
修饰的对象注册到autoreleasepool
中;__weak
只能在 ARC 模式下使用,也只能修饰对象,不能修饰基本数据类型。
__weak typeof(self) weakSelf = self;
self.block = ^{
NSLog(@"Hello Block, %p", weakSelf);
};
self.block();
__weak
对性能有一定影响,一个对象有大量 __weak
引用时候,当对象被废弃,要给所有 __weak
引用过它的对象赋 nil
,消耗 CPU 资源。话虽如此,不过该用的时候就用。
关于 __weak
底层原理大家可以参看一下这篇文章:《Objective-C weak关键字实现源码解析》
- 使用
__unsafe_unretained
解决循环引用
__unsafe_unretained id weakSelf = self;
self.block = ^{
NSLog(@"Hello Block, %p", weakSelf);
};
self.block();
__weak
只支持 iOS 5.0+ 和 OS X Mountain Lion+ 作为部署版本。__unsafe_unretained
兼容性更好一些。
虽然 __unsafe_unretained
和 __weak
都能防止对象持有,但是对于 __weak
,指针的对象在它指向的对象释放的时候回转换为 nil
,这是一种特别安全的行为;而 __unsafe_unretained
会继续指向对象存在的那个内存,即使是在它已经销毁之后。这会导致因为访问那个已释放对象引起的崩溃,当然相对而言,__unsafe_unretained
对性能影响没那么大。
b. 用 __block
解决(必须要调用 Block)
__block id weakSelf = self;
self.block = ^{
NSLog(@"Hello Block, %p", weakSelf);
weakSelf = nil;
};
self.block();
通过使用 __block
,Block 和 对象之间的关系变成
当我们使用完之后,手动把弱引用的对象设置为 nil
,这样他们关系就变成
Block 和对象就不会相互持有。