【iOS底层分析】Swift闭包&OC闭包
基础
- Block是⼀个自包含的(捕获了上下⽂的常量或者是变量的)函数代码块,可以在代码中被传递和使用。
- 全局和嵌套函数实际上也是特殊的闭包,闭包采用如下三种形式之一:
- 全局函数是一个有名字但不会捕获任何值的闭包
- 嵌套函数是一个有名字并可以捕获其封闭函数域内值的闭包
- 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包
OC-Block
分类
NSGlobalBlock
- 位于全局区
- 在Block内部不使用外部变量,或者只使用静态变量和全局变量
NSMallocBlock
- 位于堆区
- 被强持有
- 在Block内部使用局部变量或OC属性,可以赋值给强引用/copy修饰的变量
NSStackBlock
- 位于栈区
- 没有被强持有
- 在Block内部使用局部变量或OC属性,不能赋值给强引用/copy修饰的变量
如下简单demo code所示
```objectivec int a = 10; // 局部变量
void(^Global)(void) = ^{ NSLog(@"Global"); };
void(^Malloc)(void) = ^{ NSLog(@"Malloc,%d",a); };
void(^__weak Stack)(void) = ^{ NSLog(@"Stack,%d",a); };
NSLog(@"%@",Global); // <NSGlobalBlock: 0x101aa80b0> NSLog(@"%@",Malloc); // <NSMallocBlock: 0x600003187900> NSLog(@"%@",Stack); // <NSStackBlock: 0x7ff7b12c22f0> ```
下面重点介绍堆Block。
NSMallocBlock
Block拷贝到堆Block的时机:
- 手动copy
- Block作为返回值
- 被强引用/copy修饰
- 系统API包含using Block
所以总结一下堆Block判断依据:
- Block内部有没有使用外部变量
- 使用的变量类型?局部变量/OC属性/全局变量/静态变量
- 有没有被强引用/copy修饰
源码探究
我们创建一个捕获了局部变量的block
```objectivec
import
void test() { int a = 10;
void(^Malloc)(void) = ^{
NSLog(@"%d",a);
};
} ```
执行clang -rewrite-objc main.m -o main.cpp
命令,查看main.cpp文件可以看到Malloc闭包的结构如下。
```cpp struct __test_block_impl_0 { struct __block_impl impl; struct __test_block_desc_0* Desc;
// 内部存储了变量a
int a;
/// 初始化函数。包含三个参数
// - Parameters:
/// - fp: 函数指针 /// - desc: 描述 /// - _a: flag
__test_block_impl_0(void fp, struct __test_block_desc_0 desc, int _a, int flags=0) : a(_a) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
// 创建Malloc闭包,传入参数如下 // fp: (void )__test_block_func_0 // desc: &__test_block_desc_0_DATA // _a: 变量a的值(值拷贝) void(Malloc)(void) = ((void ()())&__test_block_impl_0((void )__test_block_func_0, &__test_block_desc_0_DATA, a));
// __test_block_func_0实现如下 static void __test_block_func_0(struct __test_block_impl_0 *__cself) { int a = __cself->a; // bound by copy NSLog(···); } ```
打开llvm可以看到,该block原本是在栈上,调用了objc_retainBlock
方法,而在该方法中实际调用了_Block_copy
方法。
在Block.h的源码中可以找到_Block_copy
方法,其官方注释是“创建一个基于堆的Block副本,或者简单地添加一个对现有Block的引用。”,从而将这个栈block拷贝到了堆上,下面我们根据该方法的源码来探究一下堆Block的原理。(只截取重点代码)
```cpp void _Block_copy(const void arg) { return _Block_copy_internal(arg, true); }
static void _Block_copy_internal(const void arg, const bool wantsOne) { struct Block_layout *aBlock;
···
// 类型强转为Block_layout
aBlock = (struct Block_layout *)arg;
···
// Its a stack block. Make a copy.
// 分配内存
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return NULL;
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// reset refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
// isa重新标记为Malloc Block
result->isa = _NSConcreteMallocBlock;
_Block_call_copy_helper(result, aBlock);
return result;
} ```
Block底层结构为Block_layout
struct Block_layout {
void *isa; // isa指针
volatile int32_t flags; // contains ref count
int32_t reserved; // 保留位
void (*invoke)(void *, ...); // call out funtion
struct Block_descriptor_1 *descriptor;
};
总结:
Block在运行时才会被copy,在堆上开辟内存空间。
循环引用
解决方案
-
__weak
+__strong
思路: 在block里短暂持有self的生命周期。(
weak
自动置空)```objectivec self.name = @"YK";
__weak typeof(self) weakSelf = self; self.block = ^{ __strong typeof(self) strongSelf = weakSelf; strongSelf.callFunc(); }; ```
-
__block
思路: 值拷贝。(手动置空)
我们有如下代码,生成cpp文件看一下
```objectivec
import
void test() { __block int a = 10;
void(^Malloc)(void) = ^{
a++;
NSLog(@"%d",a);
};
Malloc();
} ```
```cpp // 可以看到传入的第三个参数,是__Block_byref_a_0结构体类型的a变量地址,而不是上面讲过的直接存储int类型 void(Malloc)(void) = ((void ()())&__test_block_impl_0((void )__test_block_func_0, &__test_block_desc_0_DATA, (__Block_byref_a_0 )&a, 570425344));
// __test_block_impl_0结构体中存储的变量也是__Block_byref_a_0类型 struct __test_block_impl_0 { struct __block_impl impl; struct __test_block_desc_0 Desc; __Block_byref_a_0 a; // by ref __test_block_impl_0(void fp, struct __test_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如下 attribute((blocks(byref))) __Block_byref_a_0 a = {(void)0, (__Block_byref_a_0 )&a, 0, sizeof(__Block_byref_a_0), 10};
// __Block_byref_a_0结构体 struct __Block_byref_a_0 { void __isa; __Block_byref_a_0 __forwarding; // 指针指向原始值 int __flags; int __size; int a; // 值拷贝存储 }; ```
总结 __block
原理
- 创建
__Block_byref_a_0
结构体 - 传给block指针地址
- block內修改的是与原始值同一片的内存空间
注意点
根据上述分析我们可以得出结论,如果在OC的block中捕获了没有加__block
的外部变量,在编译时就会将变量值传入(值拷贝),如果捕获了加__block
的外部变量,则会获取到变量指针对应的内存空间的地址。代码验证如下
```cpp int a = 1; __block int b = 2;
void(^Malloc)(void) = ^{ NSLog(@"a,%d",a); NSLog(@"b,%d",b); };
a = 3; b = 4; Malloc();
// 输出结果如下 // a,1 // b,4 ```
Swift-Closure
- Swift 的闭包表达式拥有简洁的风格,并鼓励在常见场景中进行语法优化,主要优化如下:
- 利用上下文推断参数类型和返回值类型
- 隐式返回单表达式闭包(单表达式闭包可以省略
return
关键字) - 参数名称缩写,可以用$0,$1表示
- 尾随闭包语法:如果函数的最后一个参数是闭包,则闭包可以写在形参小括号的外面。为了增强函数的可读性。
- Swift 的闭包是一个引用类型,验证如下。我们知道Swift的引用类型在创建时都会调用
swift_allocObject
方法
```swift // 未调用swift_allocObject let closure1 = { () -> () in print("closure1") }
// 调用swift_allocObject let a = 10 let closure2 = { () -> () in print("closure2 (a)") } ```
捕获值
- 在闭包中如果通过
[variable1, variabla2]
的形式捕获外部变量,捕获到的变量为let
类型,即不可变 - 在闭包中如果直接捕获外部变量,获取的是指针,也就是说在闭包内修改变量值的话,原始变量也会被改变。
- 如果捕获的是指针类型(
Class
),无论是否用[],在闭包内对该变量进行修改,都会影响到原始变量
简单验证如下:
```swift var variable = 10 let closure = { () -> () in variable += 1 print("closure (variable)") }
closure() // closure 11 print(variable) // 11 ```
可见直接获取变量的话,会修改到原始值。
如果改成下面这样会编译报错”可变运算符的左侧不可变”
```swift var variable = 10 let closure = { [variable] () -> () in variable += 1 print("closure (variable)") }
closure() print(variable) ```
捕获指针类型验证
```swift class YKClass { var name = "old" }
let demoS = YKStruct() let demoC = YKClass()
let closure1 = { [demoC] () -> () in demoC.name = "new" print("closure1 (demoC.name)") }
closure1() // closure1 new print(demoC.name) // new
let closure2 = { () -> () in demoC.name = "new2" print("closure2 (demoC.name)") }
closure2() // closure2 new2 print(demoC.name) // new2 ```