iOS Block 详解

什么是 Block, 为什么引入 Block

Block 是 Objective-C 对 Closure(闭包)的实现, 本质上是一个带有自动变量(局部变量)的匿名函数。

比如说 void (^blk)(void) = ^{};

  1. block对象就是一个struct实例,更进一步的是一个OC对象。
  2. blk指针就是一个函数指针,指向了一个__main_block_impl_0对象。
  3. block调用就是把block对象中的函数指针取出来,然后把自己当参数调用一下。

Block 简单使用如下:

1
2
3
4
int (^blk)(int a) = ^(int a) {
return a;
};
blk(4);

也可以用 typedef 定义 Block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义
typedef int (^BlockName)(int a, int b);

// 属性
@property (nonatomic, copy) BlockName block;
// 不用 typedef
@property (nonatomic, copy) int (^block)(int a, int b);

// 赋值
self.block = ^int(int a, int b) {
return a + b;
};

// 调用
self.block(2, 5);

Block实现原理

比如如下一个 main.m,

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main()
{
void (^blk)(void) = ^{};

blk();

return 0;
}

执行

1
clang -rewrite-objc main.m

将其从oc转换成c++, 截取一段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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) {

}

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()
{
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;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

__main_block_impl_0 结构体

1
2
3
4
5
6
7
8
9
10
struct __main_block_impl_0 {
struct __block_impl impl; // 24 字节
struct __main_block_desc_0* Desc; // 8 字节
__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;
}
};

__main_block_impl_0结构体中有两个数据成员,一个构造函数:
第一个成员:__block_impl结构体, __block_impl可以认为是__main_block_impl_0的基类。

__block_impl

__block_impl其第一个数据是isa指针,OC对象的第一个成员也是isa指针,指向所在类的类对象,实质上,__block_impl是一个OC对象。成员 FuncPtr是一个函数指针,指向block实现函数。

1
2
3
4
5
6
struct __block_impl {
void *isa; // 8 字节
int Flags; // 4 字节
int Reserved; // 4 字节
void *FuncPtr; // 8 字节
};

__main_block_desc_0

第二个成员:__main_block_desc_0可以认为是子类中的数据。有两个成员,reserved 预留它用,暂时不表;Block_size 用来表示 __main_block_impl_0 的大小,通过 sizeof(struct __main_block_impl_0) 获取.

1
2
3
4
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)};

构造函数:__main_block_impl_0

构造函数:__main_block_impl_0,它有三个参数:函数指针fp,__main_block_desc_0指针和一个flags。
1、函数指针fp,用于构造__main_block_impl_0时,指向block的函数。
2、对象参数__main_block_desc_0保存对象私有信息。

Tips: __main_block_impl_0结构体名称怎么来的? 是Clang根据所在函数、函数中block定义的顺序自动生成的。

构造函数的定义:

1
2
3
4
5
6
__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;
}

impl对象的isa指针指向一个外部的全局对象:impl.isa = &_NSConcreteStackBlock;说明这个block是一个栈Block

1
2
extern "C" __declspec(dllexport) void *_NSConcreteGlobalBlock[32];
extern "C" __declspec(dllexport) void *_NSConcreteStackBlock[32];

Block对象的创建

接下来就是block对象的生成与赋值:

1
void (^blk)(void) = ^{};

生成 C 之后是

1
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

这句代码是通过构造函数 __main_block_impl_0创建一个实例,第一个参数是block实现代码的函数指针,第二个参数是一个__main_block_desc_0实例:__main_block_desc_0_DATA

blk是一个函数指针,指向了__main_block_impl_0实例的地址。

Block的执行

1
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

block对象的FuncPtr指向的是block的实现函数:

1
2
3
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

}

这个函数只有一个参数,就是block自己。

block调用的时候就把函数指针FuncPtr取出来并执行,参数传自己blk

Block对象大小

block对象就是一个__main_block_impl_0实例,在64位机器上,这个实例大小32字节,其中__block_impl有24字节,其中void*8字节,int4字节,__main_block_desc_0*是一个指针8字节,所以一个block对象是32字节。

如果block自动截获变量,__main_block_impl_0结构体中会增加自动截获的变量,相应的会增加 block 对象的大小。也就是 block 对象大小最小是32字节,如果截获了自动变量,block 对象会变大。

Block 捕获外部变量

C语言中变量一般可以分为以下 5 种:

  • 自动变量
  • 函数参数
  • 静态变量
  • 静态全局变量
  • 全局变量

函数参数先不管, 自动变量我们知道需要用__block修饰才能修改值, 我们看看另外三个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int global_i = 1;

static int static_global_j = 2;

int main()
{
static int static_k = 3;
int val_l = 4;
void (^blk)(void) = ^{
global_i ++;
static_global_j ++;
static_k ++;
val_l;
};

blk();

return 0;
}

翻译下

1
clang -rewrite-objc main.m

__main_block_impl_0 结构定义变成了

1
2
3
4
5
6
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_k;
int val_l;

结构中捕获了 static_k 的指针

再看下Block 执行代码

1
2
3
4
5
6
7
8
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_k = __cself->static_k; // bound by copy

global_i ++;
static_global_j ++;
(*static_k) ++;
val_l;
}

可以看到全局变量是直接使用的, 静态变量是通过指针操作修改的.

构造函数

1
2
3
4
5
6
7
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val_l, int flags=0) : static_k(_static_k), val_l(_val_l) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

这个构造函数中,自动变量和静态变量被捕获为成员变量追加到了构造函数中.
Block 定义如下

1
2
3
static int static_k = 3;
int val_l = 4;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val_l));

val_l 的值直接传到了 __main_block_impl_0 中, 所以修改 val_l 无意义, OC 的编译器直接禁止了对 Block 捕获的自动变量做修改.

__block

使用 __block 修饰 var_l 后, 定义变为了

1
2
3
4
5
6
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_k;
__Block_byref_val_l_0 *val_l; // by ref
};

Block 执行变成了

1
2
3
4
5
6
7
8
9
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_l_0 *val_l = __cself->val_l; // bound by ref
int *static_k = __cself->static_k; // bound by copy

global_i ++;
static_global_j ++;
(*static_k) ++;
(val_l->__forwarding->val_l) ++;
}

val_l 是一个__Block_byref_val_l_0结构体(这个结构体就是代码中的int val_l = 4; 中的 val_l 变量, 只是包了一层), 定义如下

1
2
3
4
5
6
7
struct __Block_byref_val_l_0 {
void *__isa;
__Block_byref_val_l_0 *__forwarding;
int __flags;
int __size;
int val_l;
};

再看看结构体赋值的地方, 跟Block 定义地方在一起, 先看看 Block 定义

Block 的定义变成了

1
2
3
static int static_k = 3;
__attribute__((__blocks__(byref))) __Block_byref_val_l_0 val_l = {(void*)0,(__Block_byref_val_l_0 *)&val_l, 0, sizeof(__Block_byref_val_l_0), 4};
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, (__Block_byref_val_l_0 *)&val_l, 570425344));

其中__block int val_l = 4; 变成了__Block_byref_val_l_0结构体

1
2
3
4
5
6
7
__Block_byref_val_l_0 val_l = {
(void*)0,
(__Block_byref_val_l_0 *)&val_l,
0,
sizeof(__Block_byref_val_l_0),
4
};

__forwarding就是(__Block_byref_val_l_0 *)&val_l, 即结构体自己的内存地址, val_l->__forwarding->val_l 先是访问结构体, 然后访问自己, 再访问结构体中的val_l, 然后对结构体中的val_l的执行修改操作, 达到了修改自动变量的目的

为什么要引用 __forwarding

引入 __forwarding 目的是为了将 Block 从栈复制到堆上的时候, 会一起复制 __Block_byref_val_l_0 这个结构体(也是一个对象), 通过上面的分析, 我们知道

  1. val_l变量实际是存在 __Block_byref_val_l_0 结构体中的, 栈上的我们简称结构体 A,
  2. 如果Block 被复制了, 堆上也会复制出 结构体 B,
  3. 栈上面的 Block 修改结构体 A的的数据, 堆上的 Block 修改的是结构体 B的数据,
  4. 最终导致 val_l 的修改对象不一致, 逻辑上有 bug.

解决办法就是 引入了 __forwarding

Block 从栈复制到堆上后, 栈上的结构体 A__forwarding 指向了堆上的 Block, 对栈上的val_l的修改实际改的是堆上的val_l, 即 val_l(栈)->__forwarding(指向堆)->val_l(堆上的val_l).

同时使用结构体代替 var_l 还有一个好处是, 栈的作用域结束, val_l就被销毁了, 但是 结构体可以一起被复制到堆上, 保证作用域结束后, 堆上的 Block 仍然能访问到 var_l.

Block 种类

OC中,一般Block就分为以下3种,_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock。先来说明一下3者的区别。

从捕获外部变量的角度上来看

  • _NSConcreteStackBlock:

    • 只用到外部局部变量、成员属性变量,且没有强指针引用的block都是StackBlock。
    • StackBlock的生命周期由系统控制的,一旦返回之后,就被系统销毁了。
  • _NSConcreteMallocBlock:

    • 有强指针引用或copy修饰的成员属性引用的block会被复制一份到堆中成为MallocBlock,没有强指针引用即销毁,生命周期由程序员控制
  • _NSConcreteGlobalBlock:

    • 没有用到外界变量或只用到全局变量、静态变量的block为_NSConcreteGlobalBlock,生命周期从创建到应用程序结束。

从持有对象的角度上来看

  • _NSConcreteStackBlock是不持有对象的。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //以下是在MRC下执行的
    NSObject * obj = [[NSObject alloc]init];
    NSLog(@"1.Block外 obj = %lu",(unsigned long)obj.retainCount);

    void (^myBlock)(void) = ^{
    NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
    };

    NSLog(@"2.Block外 obj = %lu",(unsigned long)obj.retainCount);

    myBlock();

    // 输出
    1.Block外 obj = 1
    2.Block外 obj = 1
    Block中 obj = 1
  • _NSConcreteMallocBlock是持有对象的。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    //以下是在MRC下执行的
    NSObject * obj = [[NSObject alloc]init];
    NSLog(@"1.Block外 obj = %lu",(unsigned long)obj.retainCount);

    void (^myBlock)(void) = [^{
    NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
    } copy]; // 这里对 Block copy 了一次

    NSLog(@"2.Block外 obj = %lu",(unsigned long)obj.retainCount);

    myBlock();

    [myBlock release];

    NSLog(@"3.Block外 obj = %lu",(unsigned long)obj.retainCount);

    // 输出
    1.Block外 obj = 1
    2.Block外 obj = 2
    Block中 obj = 2
    3.Block外 obj = 1
  • _NSConcreteGlobalBlock也不持有对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //以下是在MRC下执行的
    void (^myBlock)(void) = ^{

    NSObject * obj = [[NSObject alloc]init];
    NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
    };

    myBlock();

    // 输出
    Block 中 obj = 1

在ARC环境下,Block也是存在__NSStackBlock的时候的,平时见到最多的是_NSConcreteMallocBlock,是因为我们会对Block有赋值操作,所以ARC下,block 类型通过 = 进行传递时,会导致调用objc_retainBlock->_Block_copy->_Block_copy_internal方法链。并导致 NSStackBlock 类型的 block 转换为 NSMallocBlock 类型。

1
2
3
4
5
6
7
8
9
10
11
12
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {

__block int temp = 10;

NSLog(@"%@",^{NSLog(@"*******%d %p",temp ++,&temp);});

return 0;
}
// 输出
<__NSStackBlock__: 0x7fff5fbff768>

这种情况就是ARC环境下Block是__NSStackBlock的类型。

以下4种情况,系统都会默认调用copy方法把Block复制

  1. 手动调用copy
  2. Block是函数的返回值
  3. Block被强引用,Block被赋值给__strong或者id类型
  4. 调用系统API入参中含有usingBlcok的方法

但是当Block为函数参数的时候,就需要我们手动的copy一份到堆上了。这里除去系统的API我们不需要管,比如GCD等方法中本身带usingBlock的方法,其他我们自定义的方法传递Block为参数的时候都需要手动copy一份到堆上。

对 Block 执行 copy

  • 对_NSConcreteStackBlock进行copy,会深拷贝到堆区,创建一个新的_NSConcreteMallocBlock。
  • 对_NSConcreteMallocBlock进行copy,只会增加引用计数,不会分配新内存。

Block 带来的问题

Block 的出现方便了程序逻辑, 避免 delegate 导致各种逻辑被分在了代码不同的位置(比如 viewDidAppear 触发请求, 在 delegate 回调方法才能得到请求的回调, 如果这样的逻辑一多, 代码会很复杂), 但是 Block 也带来的一个问题循环引用.

什么是循环引用

对象A持有对象B,调用B的block参数方法,在里面使用了self。在使用block我们都会默认在里面使用weakself,网上搜了很多解释都是为了防止循环引用,以防self被持有导致内存泄露。

什么情况会循环引用

1
2
3
4
5
6
7
8
9
10
// 内存泄露
// 调用B的block参数方法,B内部保存block,A保存B对象
- (void)B_Method2_RetainB {
B *b = [[B alloc] init];
[b method2:^{
[self func];
NSLog(@"invoke B Method2, and retain B");
}];
self.b = b;
}

在这个例子, A -> b -> Block -> A, 才会循环引用(method2 是类方法, Class B 保存了 Block 也会导致循环).
我们一般会将 A 设成 weak, 来打破这个循环链,
还有个比较好的习惯, Block 执行完之后, 解除 b 对 Block的引用(如果只用一次的话), 也能避免循环引用.

Block 内调用 super 引发的循环引用

不仅 self 会触发循环引用, super 也会触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@interface TestObj : NSObject

@property (nonatomic, copy) void(^captureSelf)(void);

@property (nonatomic, assign) BOOL capture;

@end

@implementation TestObj

- (instancetype)init {
if (self = [super init]) {
// Q1:这样会不会泄漏
self.captureSelf = ^() {
_capture = YES;
};
}
return self;
}

@end

@interface TestChildObj : TestObj

@end

@implementation TestChildObj

- (instancetype)init {
if (self = [super init]) {

@weakify(self);
// Q2:这样会不会导致泄漏?
self.captureSelf = ^() {
@strongify(self);
super.capture = YES;
};
}
return self;
}

@end

Q1:

这个有点经验的都知道泄漏了,因为隐式包含调用了 self->_xxx ,还是通过 self 再进行偏移量获取的。这里就不再进行分析了,看完分析 Q2 之后你也可以尝试分析 Q1 的问题。
解法也很简单,注意因为是读取偏移量,如果不对 self 判空的话,当 self 为 null 时,进行 self->_capture 的访问是会出现 MACH 类型的崩溃的。

1
2
3
4
5
6
7
@weakify(self);
self.captureSelf = ^() {
@strongify(self);
if (!self) return;
self->_capture = YES;
// _capture = YES;
};

Q2:

实际是会内存泄露, 并且编译器会提示 @strongify(self); 未使用.
但是我们知道 super 会被替换为 objc_msgSendSuper(self), 既然会被换成 self ,那就会被 weak/strong dance 所替换,然后就万事大吉了. 难道 宏替换 与 objc_msgSendSuper 不是在同一个步骤中进行的嘛?

结论: 宏的替换在 Preprocessed 阶段,super 的变更在 compile 阶段,因此对 self 进行 weak/strong dance 并解决不了 super 导致的循环引用问题 (Rewrite C++ 有问题, 实际没有把 super 换成 self)。关于 objc_msgSend Stub 可以参考 【WWDC22 110363】App 包大小优化和 Runtime 上的性能提升

怎么发现循环引用

对于内存泄漏,苹果提供了两种常用的检测方式:

  • 静态检测,即使用XCode分析功能,Product -> Analyze。
  • 动态检测,即使用Instruments工具中的Allocation。

苹果提供的检测方式需要人工观察,功能很强大,就是比较麻烦。通常对于一般的工程来说,内存泄漏的场景中还是以UI居多,第三方工具MLeaksFinder针对这种场景就孕育而生,在运行过程自动检测UIViewController和其子UIView对象的内存泄漏,然后通过控制台以及弹框告知开发者并去修改。

MLeaksFinder

MLeaksFinder工具检测的出发点是从UIViewController入手,利用UIViewController的生命周期来判断UIViewController以及view等内存有没有被释放,UIViewController的生命周期如下图:

退出UIViewController,要么被pop,要么就是被dismiss的,退出后包括它的view,view的subviews等都将会很快被释放(除非设计成单例,或者强引用持有,一般很少这样做)。所以只需在UIViewController被pop或dismiss一段时间后,判断UIViewController,它的view,view的subviews等是否还存在即可。

判断这些对象是否还存在,可以为基类NSObject添加方法-(BOOL)willDealloc,该方法的作用是,先用一个弱指针指向self,并在一段时间(2秒)后,通过这个弱指针调用-(void)assertNotDealloc方法,而assertNotDealloc这方法的作用就是提示开发者哪个对象泄漏了,如果对象泄漏了,弱指针调用assertNotDealloc方法就会执行相应逻辑,对应的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (BOOL)willDealloc {
NSString *className = NSStringFromClass([self class]);
if ([[NSObject classNamesWhitelist] containsObject:className])
return NO;

NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey);
if ([senderPtr isEqualToNumber:@((uintptr_t)self)])
return NO;

__weak id weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
__strong id strongSelf = weakSelf;
[strongSelf assertNotDealloc];
});

return YES;
}

UIViewController显示时会调用viewWillAppear,不显示时会调用viewDidDisappear,不显示包括退出,被移出,以及被覆盖的。所以就可以在viewWillAppear方法处做一个标记kHasBeenPopedKey,表示这个UIViewController没有被退出,值为NO,然后在viewDidDisappear方法处判断这个标记kHasBeenPopedKey,如果标记值还是为NO,就不处理内存是否还存在的逻辑,如果是YES的,就要过两秒判断下这些对象还存不存在,如果存在即泄漏。然后在UIViewController的pop或dismiss时设置标记kHasBeenPopedKey为YES,代码实现上,使用了AOP技术,通过hook掉UIViewController以下三个方法,加入自己的标记逻辑:

1
2
3
[self swizzleSEL:@selector(viewDidDisappear:) withSEL:@selector(swizzled_viewDidDisappear:)];
[self swizzleSEL:@selector(viewWillAppear:) withSEL:@selector(swizzled_viewWillAppear:)];
[self swizzleSEL:@selector(dismissViewControllerAnimated:completion:) withSEL:@selector(swizzled_dismissViewControllerAnimated:completion:)];

对于UIViewCtroller的pop和dismiss就要hood掉UINavigationController以下几个方法:

1
2
3
4
[self swizzleSEL:@selector(pushViewController:animated:) withSEL:@selector(swizzled_pushViewController:animated:)];
[self swizzleSEL:@selector(popViewControllerAnimated:) withSEL:@selector(swizzled_popViewControllerAnimated:)];
[self swizzleSEL:@selector(popToViewController:animated:) withSEL:@selector(swizzled_popToViewController:animated:)];
[self swizzleSEL:@selector(popToRootViewControllerAnimated:) withSEL:@selector(swizzled_popToRootViewControllerAnimated:)];

在这几个方法中,会将标记kHasBeenPopedKey设置为YES,然后在viewDidDisappear就会触发willDealloc逻辑判断内存存在性

怎么 Hook Block Hook Objective-C Block with Libffi

参考文档

  1. 追踪 Objective-C 方法中的 Block 参数对象
  2. Storing Blocks in an Array
  3. 深入研究 Block 捕获外部变量和 __block 实现原理
  4. Objective-C & Swift 最轻量级 Hook 方案
  5. 深入理解 OC/C++ 闭包
  6. Block 内调用 super 引发的循环引用
  7. BlockHook
  8. Hook Objective-C Block with Libffi
-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道