这是我参与「第四届青训营 」笔记创作活动的第7天。
Block 内存管理
基础分类
| 类型 | 描述 | 环境 |
|---|---|---|
| NSGlobalBlock | 全局Block,保存在数据区(.data段) | 定义在全局区或者没有访问自动局部变量 |
| NSStackBlock | 栈Block,保存在栈区 | 访问了自动局部变量 |
| NSMallocBlock | 堆Block, 保存在堆区 | __NSStackBlock__调用了copy |
Objective-C 中 block 大致分为表中3类,分别存储在不同的内存区域中:从表中可以看出对应内存区域 block 的产生条件,下面可以看下实际的代码
int main(int argc, char * argv[]) {
@autoreleasepool {
// __NSGlobalBlock__
void(^globalBlock)(void) = ^{
NSLog(@"Hello, World!");
};
NSLog(@"%@", [globalBlock class]);
// __NSStackBlock__
int age = 18;
void(^stackBlock)(void) = ^{
NSLog(@"Hello, World! %d", age);
};
NSLog(@"%@", [stackBlock class]);
// __NSMallocBlock__
void(^mallocBlock)(void) = [stackBlock copy];
NSLog(@"%@", [mallocBlock class]);
}
return 0;
}
第4行声明的 globalBlock 因为没有访问任何自动变量,会被存储在 .data 段,所以是一个 NSGlobalBlock;第11行声明的 stackBlock 引用了临时变量 age,所以会被存储在栈上,是一个NSStackBlock;第17行的 mallocBlock 因为手动调用了 copy 方法,所以被存储在了堆上,是一个 NSMallocBlock
但是,第二个 block 预期是 NSStackBlock,打印出来的是 NSMallocBlock
Xcode 4.2 之后,引入了 ARC 机制,在一些默认的情况下系统会帮你自动调用 copy 操作:
- block 作为函数返回值
- 将 block 赋值给 strong 指针
- block 作为某些系统方法参数
系统的整体思路是如果block被释放,有潜在的异常风险时,手动”帮你“copy下
变量捕获
值捕获和引用捕获
- (void)changeValue {
int value = 1;
void (^oneBlock)(void) = ^{
NSLog(@"value = %d", value); // value1:1
};
value = 2;
oneBlock();
NSLog(@"value = %d", value); // value2:2
}
由于 oneBlock 在初始化的时候,按值捕获了 value,而后,当 value 更改后, block 内捕获的值并没有跟着一起变化;而 value2 处则是使用的改变后的变量
- (void)changeValue {
__block int value = 1;
void (^oneBlock)(void) = ^{
NSLog(@"value = %d", value); // value1:2
value = 3;
};
value = 2;
oneBlock();
NSLog(@"value = %d", value); // value2:3
}
上面的代码块和之前的很像,区别在于传入的变量前增加了一个关键字 __block,这个关键字标志了该变量允许在 block 中被修改
和之前不同,这里 block 内部对于 value 的捕获是引用捕获,也就是说 block 中的 value 就是外部的 value 对象,因此对应值的改变也会同步,内外互相影响
- (void)changeValue {
NSString *value = @"1";
void (^oneBlock)(void) = ^{
NSLog(@"value = %@", value); // value1:1
}
value = @"2";
oneBlock();
NSLog(@"value = %@", value); // value2:2
}
这里很好理解,取了之前的指针的“值”,当 value 被重新赋值时,block 捕获的是上一个指针值
@property (nonatomic, copy) NSString *name;
- (void)changeValue {
self.name = @"1";
void (^oneBlock)(void) = ^{
NSLog(@"value = %@", self.name); // value1:2
}
self.name = @"2";
oneBlock();
NSLog(@"value = %@", self.name); // value2:2
}
OC 是一门动态语言,所有方法都是在运行时通过发送消息 objc_msgSend 实现的,这里 block 其实只捕获了 self 对象,至于 name,不是取它的属性,而是向其发送了一个消息,告诉他我想要你的 name 属性,这里可以通过 llvm 将 oc 代码转化为 C++ 代码会看的比较清晰
因此,self 指针没变,在 block 向 self 发消息时,它就能正确的找到变化后的值
Block 的循环引用
// ViewController.m
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) void (^completionBlock)(void);
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.completionBlock = ^{
NSLog(@"%@", self.name);
};
}
@end
Xcode warning:Capturing 'self' strongly in this block is likely to lead to a retain cycle!
之前提到过,在 ARC 下,系统会在某些场景默认对 block 执行 copy 操作,使其变为 __NSMallocBlock__ ,此时 block 也会有自己的内存引用计数。因此,在上文代码中,VC 和 Block 产生了循环引用,导致了内存泄漏
循环引用的常规解决方案就是打破引用环,使用 weak
// ViewController.m
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) void (^completionBlock)(void);
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.completionBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
NSLog(@"%@", strongSelf.name);
};
}
@end
还有一种比较隐式的内存泄漏
// ViewController.m
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) void (^completionBlock)(void);
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.completionBlock = ^{
NSLog(@"%@", _name);
};
}
@end
block 内虽然没有直接引用 self,但是 _name 表示 viewController 的变量,要找到它就需要持有 self ,因此形成了隐式的内存泄漏