Block 内存管理 | 青训营笔记

95 阅读3分钟

这是我参与「第四届青训营 」笔记创作活动的第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 ,因此形成了隐式的内存泄漏