李峰峰博客

Block 实现原理

2020-04-10

1、block 的底层结构

假设对于以下 block:

1
2
3
4
5
int a = 10;
void (^myblock)(void) = ^{
NSLog(@"myblock is %d",a); // 注意,block 内部访问了外部变量
};
myblock();

将以上代码转换成对应的 C++ 代码,可以看到,Block 底层的结构如下:

(备注:上图 __main_block_func_0 函数里实际上还有 block 里 NSLog(@"myblock is %d",a);相关代码,为了看起来方便,这里直接去掉了。)

  • impl 是 block 的实现,其中包含了 isa 指针以及 block 函数实现。
  • Desc 是 block 的描述信息,包含了 block 大小等信息。
  • 如果 block 内部还访问了局部变量,block 结构体中还会包含捕获的局部变量。

block 结构体的第一个成员变量是结构体(不是结构体指针) __block_impl impl,所以,block 结构体相当于就是:

从 block 的结构体包含 isa 指针可以看出,block 的本质就是一个对象,NSBlock 对象也是继承自 NSObject 对象的。并且如果 block 访问了外部变量的话,还会把外部变量包装到自己内部。

.cpp 中关于执行 block 的逻辑如下:

可以看出,执行 block,实际上就是 block->FuncPtr(),也就是通过 block 对象执行其实现函数。

2、外部变量的捕获

当在 block 内访问外部变量时,block 对变量的捕获方式如下:

例如,对于以下代码:

1
2
3
4
5
6
7
8
void test()
{
int a = 1; // auto
static int b = 2; // static
block = ^{
NSLog(@"a is %d, b is %d", a, b);
};
}

其中有一个 auto 变量 a(对于一个局部变量,无论是基本数据类型还是 OC 对象类型,默认就是 auto 变量)、一个 static 变量 b,将代码转换成 C++ 代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
struct __test_block_impl_0 {
struct __block_impl impl;
struct __test_block_desc_0* Desc;
int a;
int *b;
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

可以发现,对于局部变量,访问 auto 变量,block 捕获的是其值,而访问 static 变量,block 捕获的是指针,所以在 block 内部可以直接修改 static 局部变量的值,但不可以修改 auto 变量的值。

对于全局变量,可以保证变量的生命周期足够长,block 内部也就没有必要再去捕获,block 内部访问全局变量直接访问即可,也可以在 block 内部直接修改全局变量。

如果在 block 内部访问了 self,block 也会捕获 self,因为 self 是局部变量。如果访问了 self 内部的属性,捕获的也是 self,而不是内部具体的属性。为什么 self 是局部变量呢?我们在方法内能访问 self,是因为 OC 方法在底层转换为 C 语言函数后,会默认自动带上 self_cmd 两个参数,我们在方法内部访问的 self 实际上就是参数传进来的 self,方法的参数也是一种局部变量。

当 block 内部访问了对象类型的 auto 变量时:

  • 如果 block 是在栈上,将不会对 auto 变量产生强引用

  • 如果 block 被拷贝到堆上

    • 会调用 block 内部的 copy 函数
    • copy 函数内部会调用 _Block_object_assign 函数
    • _Block_object_assign 函数会根据 auto 对象类型变量的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
  • 如果 block 从堆上移除

    • 会调用 block 内部的 dispose 函数
    • dispose 函数内部会调用 _Block_object_dispose 函数
    • _Block_object_dispose 函数会自动释放引用的 auto 变量(release)

3、block 类型

block 有 3 种类型,可以通过调用 class 方法(例如:[myBlock class])或者 isa 指针查看具体类型,最终都是继承自 NSBlock 类型。
Block 的三种类型:

  • 全局 block
    __NSGlobalBlock___NSConcreteGlobalBlock
  • 栈 block
    __NSStackBlock___NSConcreteStackBlock
  • 堆 block
    __NSMallocBlock___NSConcreteMallocBlock

在 MRC 情况下:

在 ARC 情况下:
在 ARC 环境下,为了延长 block 的生命周期,避免栈 block 那样作用域结束 block 就被废弃的情况,编译器会根据需要自动将栈上的 block 复制到堆上(变成堆 block),复制到堆上调用的就是 block 的 copy 方法,比如以下情况:

  • block 作为函数返回值时
  • 将 block 赋值给 __strong 指针时
  • block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时
  • block 作为 GCD API 的方法参数时

MRC 下 block 属性的建议写法:

1
@property (nonatomic, copy) void (^block)(void);

ARC 下 block 属性的建议写法:

1
2
@property (nonatomic, strong) void (^block)(void);
@property (nonatomic, copy) void (^block)(void);

4、__block 修饰符

如果我们直接在 block 中修改 auto 变量,编译器会报错,要求使用 __block 修饰 auto 变量,如下:

当使用 __block 修饰了 auto 变量后,就不再报错,并且成功修改了 auto 变量的值:

备注:
向下面这样就不需要使用 __block 修饰仍然可以生效,因为 block 相当于只是使用了变量,而不是直接修改了对象的值:

(1) __block 修饰符作用

  • __block 可以用于解决 block 内部无法修改 auto 变量值的问题
  • __block 不能修饰全局变量、静态变量(static)
  • 编译器会将 __block 修饰的变量包装成一个对象

接下来将前述的代码转成 C++ 代码,看下 __block 修饰 auto 变量后的逻辑:


从上述 C++ 代码可知,当使用 __block 修饰了 auto 变量 age 后:

  • 会在当前 block 结构体创建 __Block_byref_age_0 结构体类型的指针成员变量。
    __Block_byref_age_0 *age;

  • __Block_byref_age_0 结构体中包含 isa 指针,所以也是一个对象。
    void *__isa;

  • __Block_byref_age_0 结构体中包含一个指向自己的指针 __forwarding。
    __Block_byref_age_0 *__forwarding;

  • __Block_byref_age_0 结构体中包含了之前在 block 中访问的、被 __block 修饰的 auto 变量。
    int age;

  • 当在 block 中修改被 __block 修饰的 auto 变量时,是通过 __forwarding 间接修改的。
    (age->__forwarding->age) = 200;

  • 并且,当 block 访问了__block 修饰的 auto 变量后,block 结构体中的 Desc 结构体多了 copydispose 两个和内存管理有关的两个函数。

(2) __forwarding 指针的作用

从上述可知,当在 block 中修改被 __block 修饰的 auto 变量时,是通过 __forwarding 间接修改的。
现在将对应的 C++ 代码中结构体拿出来,对 block 强制转换成对应结构体类型:

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
44
45
struct __Block_byref_age_0 {
void *__isa;
struct __Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};

struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(void);
void (*dispose)(void);
};

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_age_0 *age;
};

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

__block int age = 100;

void (^block)(void) = ^{
age = 200;
NSLog(@"my age is %d", age);
};

struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block;

block();

}
return 0;
}

断点调试结果如下,验证了前述的结论:

为什么 block 在修改 __block 修饰的 age 变量时候,要通过 __Block_byref_age_0 结构体中指向结构体自身的 __forwarding 指针去间接找到自己成员变量 age 然后去修改呢?

这样设计的目的是为了方便内存管理。block 被复制到堆上时,会将 block 中引用的变量也复制到堆中。

重新看下源码:

1
2
3
4
5
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
(age->__forwarding->age) = 200;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_jm_dztwxsdn7bvbz__xj2vlp8980000gn_T_main_b05610_mi_0,(age->__forwarding->age));
}

通过源码可以知道,当修改 __block 修饰的变量时,是根据变量生成的结构体 __Block_byref_age_0 找到其中 __forwarding 指针,__forwarding 指针指向的是结构体自己,因此可以找到 age 变量进行修改。

当 block 在栈中时,__Block_byref_age_0 结构体内的 __forwarding 指针指向结构体自己。

而当 block 被复制到堆中时,栈中的 __Block_byref_age_0 结构体也会被复制到堆中一份,而此时栈中的 __Block_byref_age_0 结构体中的 __forwarding 指针指向的就是堆中的 __Block_byref_age_0 结构体,堆中 __Block_byref_age_0 结构体内的 __forwarding 指针依然指向自己。

此时当对 age 进行修改时:

1
2
3
4
5
// 栈中的age
__Block_byref_age_0 *age = __cself->age; // bound by ref
// age->__forwarding获取堆中的age结构体
// age->__forwarding->age 修改堆中age结构体的age变量
(age->__forwarding->age) = 200;

通过 __forwarding 指针巧妙的将修改的变量赋值在堆中的 __Block_byref_age_0 中。

通过一张图展示 __forwarding 指针的作用:
upload successful
因此 block 内部拿到的变量实际就是在堆上的。当 block 进行 copy 被复制到堆上时,_Block_object_assign 函数内做的这一系列操作。

(3)__block 的内存管理

__block 变量的引用:

  • 当 block 在栈上时,并不会对 __block 变量产生强引用
  • 当 block 被 copy 到堆时
    • 会调用 block 内部的 copy 函数
    • copy 函数内部会调用 _Block_object_assign 函数
    • 利用 _Block_object_assign 函数对 __block 变量的进行相应持有
      • 编译器会将 __block 变量包装成一个结构体对象 A(例如前述的 struct __Block_byref_age_0 *age,并对这个结构体对象 A 进行强引用。
      • 对于 OC 对象,_Block_object_assign 函数会根据所指向对象的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,在结构体对象 A 中形成强引用(retain)或者弱引用(注意:这里仅限于 ARC 时会 retain,MRC 时不会 retain

简而言之,block 访问了 __block 修饰的 auto 变量,编译器会自动生成对应的结构体对象,block 会对这个结构体对象进行强引用,但是这个结构体对象内部对 auto 变量是强引用还是弱引用,取决于这个 auto 变量对象修饰符(__strong__weak__unsafe_unretained)。

例如:

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
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int number = 20;
__block int age = 10;

NSObject *object = [[NSObject alloc] init];
__weak NSObject *weakObj = object;

Person *p = [[Person alloc] init];
__block Person *person = p;
__block __weak Person *weakPerson = p;

Block block = ^ {
NSLog(@"%d",number); // 局部变量
NSLog(@"%d",age); // __block修饰的局部变量
NSLog(@"%p",object); // 对象类型的局部变量
NSLog(@"%p",weakObj); // __weak修饰的对象类型的局部变量
NSLog(@"%p",person); // __block修饰的对象类型的局部变量
NSLog(@"%p",weakPerson); // __block,__weak修饰的对象类型的局部变量
};
block();
}
return 0;
}

将上述代码转化为 C++ 代码查看不同变量之间的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;

int number;
NSObject *__strong object;
NSObject *__weak weakObj;
__Block_byref_age_0 *age; // by ref
__Block_byref_person_1 *person; // by ref
__Block_byref_weakPerson_2 *weakPerson; // by ref

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _number, NSObject *__strong _object, NSObject *__weak _weakObj, __Block_byref_age_0 *_age, __Block_byref_person_1 *_person, __Block_byref_weakPerson_2 *_weakPerson, int flags=0) : number(_number), object(_object), weakObj(_weakObj), age(_age->__forwarding), person(_person->__forwarding), weakPerson(_weakPerson->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

上述 __main_block_impl_0 结构体中看出,没有使用 __block 修饰的变量(objectweakObj)则根据他们本身被 block 捕获的指针类型对他们进行强引用或弱引用。而一旦使用 __block 修饰的变量,__main_block_impl_0 结构体内一律使用强指针引用生成的结构体。

接着我们来看 __block 修饰的变量生成的结构体有什么不同:

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
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};

struct __Block_byref_person_1 {
void *__isa;
__Block_byref_person_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
Person *__strong person;
};

struct __Block_byref_weakPerson_2 {
void *__isa;
__Block_byref_weakPerson_2 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
Person *__weak weakPerson;
};

如上面分析的那样,__block 修饰对象类型的变量生成的结构体内部多了 __Block_byref_id_object_copy__Block_byref_id_object_dispose 两个函数,用来对对象类型的变量进行内存管理的操作。而结构体对对象的引用类型,则取决于 block 捕获的对象类型的变量。weakPerson 是弱指针,所以 __Block_byref_weakPerson_2weakPerson 就是弱引用,person 是强指针,所以 __Block_byref_person_1person 就是强引用。

__block 变量的释放:
当 block 从堆中移除时

  • 会调用 block 内部的 dispose 函数
    + dispose 函数内部会调用 _Block_object_dispose 函数
  • _Block_object_dispose 函数会自动释放引用的 __block 变量(release

5、block 的循环引用

(1) Block 内访问 self 导致的循环引用

在 Block 内部访问 访问 self 可能会导致循环引用,例如:

1
2
3
4
5
- (void)testFunc {
self.TestBlock = ^{
self.name = @"lifengfeng";
};
}

解决办法是使用 __weak 修饰 self,为了延长 self 生命周期,避免 Block 在执行过程中 self 被销毁,在 block 内部使用 __strong 修饰的 self

1
2
3
4
5
6
7
- (void)testFunc {
__weak typeof(self) weakSelf = self;
self.TestBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
strongSelf.name = @"lifengfeng";
};
}

但是,为什么 Block 内部使用 __strong 修饰 self 后,为什么不会重新循环引用呢?
很简单,Block 外部使用 __weak 修饰 self,会使 Block 捕获 self 时候对 self 进行弱引用,内部再使用 __strong 修饰 self,相当于在 Block 对应函数实现里对 self 进行一次引用计数 +1,延长了 self 的生命周期,但 Block 对捕获的 self 仍然是弱引用的。

(2) NSTimer 导致的循环引用

例如,使用 NSTimer 可能产生循环引用:

1
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];

NSTimer 循环引用的原因:
NSTimer 被添加到主线程的 RunLoop 中,在停止 NSTimer 之前,NSTimer 会被 RunLoop 一直持有。而 NSTimer 又持有了 Target,导致了 Target 无法释放。

其解决办法主要有下面三种方式:

a、使用 block

1
2
3
4
__weak typeof(self) weakself = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakself timerTest];
}];

b、使用代理对象

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
// XXXProxy.h
@interface XXXProxy : NSObject

+ (instancetype)proxyWithTarget:(id)target;
// 注意这里是 weak
@property (weak, nonatomic) id target;

@end


// XXXProxy.m
#import "XXXProxy.h"

@implementation XXXProxy

+ (instancetype)proxyWithTarget:(id)target
{
XXXProxy *proxy = [[XXXProxy alloc] init];
proxy.target = target;
return proxy;
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
return self.target;
}

@end

使用方式:

1
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[XXXProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];

这样就避免了循环引用。

这里使用的是我们自己继承自 NSObject 实现的代理对象,但是系统提供了一个效率更高的代理对象:NSProxy,使用该对象做代理时,不再从自己方法列表查找方法,直接走消息转发流程,而且该类不是继承自 NSObject 的,而是实现 <NSObject> 协议的基类:

1
2
3
@interface NSProxy <NSObject> {
Class isa;
}

使用方式如下:

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
// XXXProxy.h
@interface XXXProxy : NSProxy

+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;

@end


// XXXProxy.m
#import "XXXProxy.h"

@implementation XXXProxy

+ (instancetype)proxyWithTarget:(id)target
{
// NSProxy 对象不需要调用 init,因为它本来就没有 init 方法
XXXProxy *proxy = [XXXProxy alloc];
proxy.target = target;
return proxy;
}

// NSProxy 没有 forwardingTargetForSelector: 方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
}

@end

接下来看一个关于NSProxy 比较有意思的示例,以下代码如何打印?

1
2
3
4
ViewController *vc = [[ViewController alloc] init];
// XXXProxy 是继承自 NSProxy
XXXProxy *proxy = [XXXProxy proxyWithTarget:vc];
NSLog(@"%d",[proxy isKindOfClass:[ViewController class]]);

按照以往理解,以上代码应该打印 0,但实际结果是打印 1,因为 NSProxy 不是任何类的子类,也是一个基类,内部调用的所有方法,都将直接进行转发。所以上面
[proxy isKindOfClass:[ViewController class]]
等价于
[vc isKindOfClass:[ViewController class]]

而如果 XXXProxy 继承自 NSObject,那么结果就是打印 0 了。

c、使用 GCD 替代

另外需要注意的是,由于 NSTimer 是基于 Runloop 的,所以如果 Runloop 循环期间任务很重的话,很容易出现 NSTimer 计时不准问题,解决办法就是使用 GCD 实现定时器,GCD 是直接跟系统内核挂钩的,不会受 Runloop 影响:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 队列
dispatch_queue_t queue = dispatch_get_main_queue();

// 创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

// 设置时间
uint64_t start = 2.0; // 2秒后开始执行
uint64_t interval = 1.0; // 每隔1秒执行
dispatch_source_set_timer(timer,
dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
interval * NSEC_PER_SEC, 0);

// 设置回调
dispatch_source_set_event_handler(timer, ^{
NSLog(@"1111");
});
// dispatch_source_set_event_handler_f(timer, myTimerFireMethod);

// 启动定时器
dispatch_resume(timer);

self.timer = timer;