引言

在Objective - C开发中,Block是一个强大且灵活的特性,它允许开发者将代码块作为对象进行传递和存储。无论是在异步操作、回调机制还是数据处理中,Block都发挥着重要作用。然而,要真正掌握Block的使用,理解其底层原理是必不可少的。本文将深入探讨Block的底层结构、内存管理以及循环引用问题的解决方法。

一、Block的底层结构

1.1 Block的本质

Block本质上是一个结构体,它封装了一段代码以及该代码执行时所需的上下文信息。在底层,Block结构体包含以下几个关键部分:

1.1.1 isa指针

类似于Objective - C对象,Block也有一个isa指针,它指向Block所属的类。根据isa指针的不同,Block可以分为三种类型:

  • NSGlobalBlock:存储在全局数据区,当Block不捕获任何外部变量时,会被创建为全局Block。例如:
    1
    2
    3
    void (^globalBlock)(void) = ^{
    NSLog(@"This is a global block.");
    };
  • NSStackBlock:存储在栈区,当Block捕获了外部变量,但没有进行复制操作时,会被创建为栈Block。栈Block的生命周期与所在的栈帧相同,当栈帧销毁时,栈Block也会失效。例如:
    1
    2
    3
    4
    int num = 10;
    void (^stackBlock)(void) = ^{
    NSLog(@"Captured number: %d", num);
    };
  • NSMallocBlock:存储在堆区,当对栈Block进行复制操作时,会将其复制到堆区,成为堆Block。堆Block的生命周期由开发者管理,需要手动释放。例如:
    1
    2
    3
    4
    5
    int num = 10;
    void (^stackBlock)(void) = ^{
    NSLog(@"Captured number: %d", num);
    };
    void (^heapBlock)(void) = [stackBlock copy];

1.1.2 函数指针

Block结构体中包含一个函数指针,指向Block实际执行的代码块。当调用Block时,实际上是通过这个函数指针来执行代码。

1.1.3 捕获的变量

如果Block捕获了外部变量,这些变量会被存储在Block结构体中。捕获的方式根据变量的类型有所不同:

  • 值捕获:对于基本数据类型的局部变量,Block会捕获其值。在Block定义时,会将变量的值复制到Block结构体中,后续变量值的改变不会影响Block内部的值。
    1
    2
    3
    4
    5
    6
    int num = 10;
    void (^block)(void) = ^{
    NSLog(@"Captured num: %d", num);
    };
    num = 20;
    block(); // 输出: Captured num: 10
  • 引用捕获:对于__block修饰的变量和对象类型的变量,Block会捕获其引用。在Block内部可以修改__block修饰的变量的值,并且对象的引用计数会相应增加。
    1
    2
    3
    4
    5
    6
    __block int num = 10;
    void (^block)(void) = ^{
    num++;
    NSLog(@"Modified num: %d", num);
    };
    block(); // 输出: Modified num: 11

1.2 Block的结构体定义

以下是简化后的Block结构体定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
// 捕获的变量
};

struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
  • isa:指向Block所属的类。
  • flags:包含一些标志位,用于描述Block的特性。
  • invoke:指向Block实际执行的函数。
  • descriptor:指向Block的描述信息结构体,包含Block的大小、复制和释放函数等。

二、Block的内存管理

2.1 复制操作

对不同类型的Block进行复制操作会有不同的效果:

  • NSGlobalBlock:复制操作不会产生新的Block,仍然返回原来的Block。因为全局Block存储在全局数据区,其生命周期与程序的生命周期相同,不需要额外的内存管理。
  • NSStackBlock:复制操作会将栈Block复制到堆区,生成一个NSMallocBlock。这是因为栈Block存储在栈区,其生命周期受栈帧的限制,复制到堆区可以延长其生命周期。
  • NSMallocBlock:复制操作会增加引用计数。堆Block的内存管理遵循引用计数原则,当引用计数为0时,会自动释放内存。

2.2 释放操作

当Block的引用计数为0时,会自动调用其dispose函数进行释放操作。对于捕获了对象的Block,dispose函数会对捕获的对象进行释放,减少对象的引用计数。

2.3 示例代码

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
@interface MyClass : NSObject
@property (nonatomic, copy) void (^myBlock)(void);
@end

@implementation MyClass

- (instancetype)init {
self = [super init];
if (self) {
__block int num = 10;
self.myBlock = ^{
num++;
NSLog(@"Modified num: %d", num);
};
}
return self;
}

- (void)dealloc {
NSLog(@"MyClass deallocated.");
}

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
MyClass *obj = [[MyClass alloc] init];
obj.myBlock(); // 输出: Modified num: 11
obj = nil; // 触发dealloc
}
return 0;
}

在上述示例中,MyClassmyBlock属性使用了copy修饰符,这会将栈Block复制到堆区。当obj被置为nil时,MyClassdealloc方法会被调用,同时myBlock的引用计数减为0,会自动释放内存。

三、Block的循环引用问题及解决方法

3.1 循环引用的产生

循环引用是指两个或多个对象之间相互持有对方的强引用,导致引用计数无法降为0,从而造成内存泄漏。在使用Block时,循环引用通常发生在Block捕获了包含它的对象,并且该对象又持有该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
@interface MyClass : NSObject
@property (nonatomic, copy) void (^myBlock)(void);
@end

@implementation MyClass

- (instancetype)init {
self = [super init];
if (self) {
self.myBlock = ^{
[self doSomething];
};
}
return self;
}

- (void)doSomething {
NSLog(@"Doing something.");
}

- (void)dealloc {
NSLog(@"MyClass deallocated.");
}

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
MyClass *obj = [[MyClass alloc] init];
obj.myBlock();
obj = nil; // 不会触发dealloc
}
return 0;
}

在上述示例中,MyClassmyBlock属性持有了一个Block,而该Block又捕获了self(即MyClass对象),形成了循环引用。当obj被置为nil时,由于循环引用的存在,MyClass对象的引用计数无法降为0,dealloc方法不会被调用,导致内存泄漏。

3.2 解决循环引用的方法

3.2.1 使用__weak修饰符

使用__weak修饰符可以创建一个弱引用,不会增加对象的引用计数。在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
@interface MyClass : NSObject
@property (nonatomic, copy) void (^myBlock)(void);
@end

@implementation MyClass

- (instancetype)init {
self = [super init];
if (self) {
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
[weakSelf doSomething];
};
}
return self;
}

- (void)doSomething {
NSLog(@"Doing something.");
}

- (void)dealloc {
NSLog(@"MyClass deallocated.");
}

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
MyClass *obj = [[MyClass alloc] init];
obj.myBlock();
obj = nil; // 触发dealloc
}
return 0;
}

在上述示例中,使用__weak typeof(self) weakSelf = self;创建了一个弱引用weakSelf,并在Block内部使用weakSelf代替self,从而避免了循环引用。当obj被置为nil时,MyClass对象的引用计数降为0,dealloc方法会被调用。

3.2.2 使用__strong修饰符

在某些情况下,使用__weak修饰符可能会导致在Block执行过程中对象被提前释放。可以在Block内部使用__strong修饰符创建一个强引用,确保在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
@interface MyClass : NSObject
@property (nonatomic, copy) void (^myBlock)(void);
@end

@implementation MyClass

- (instancetype)init {
self = [super init];
if (self) {
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf doSomething];
}
};
}
return self;
}

- (void)doSomething {
NSLog(@"Doing something.");
}

- (void)dealloc {
NSLog(@"MyClass deallocated.");
}

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
MyClass *obj = [[MyClass alloc] init];
obj.myBlock();
obj = nil; // 触发dealloc
}
return 0;
}

在上述示例中,在Block内部使用__strong typeof(weakSelf) strongSelf = weakSelf;创建了一个强引用strongSelf,并在使用self之前先判断strongSelf是否为nil,确保在Block执行期间对象不会被释放。

四、总结

Block是Objective - C中一个强大而灵活的特性,其底层原理涉及到结构体、内存管理和循环引用等多个方面。通过深入理解Block的底层结构和内存管理机制,开发者可以更好地使用Block,避免内存泄漏和循环引用问题。同时,掌握解决循环引用的方法可以确保代码的健壮性和稳定性。希望本文能帮助开发者更深入地探索Objective - C中Block的奥秘。