iOS开发基础75-Block深度解析

iOS Block 深度解析:从底层结构到内存管理与循环引用

Block 是 iOS 和 macOS 开发中最常用、最高效的特性之一。它本质上是一个封装了代码和捕获变量的对象,为回调、异步操作、枚举遍历等场景提供了简洁优雅的解决方案。但 Block 的底层结构、三种内存类型、变量捕获机制、循环引用等问题,也是面试高频考点和开发中的常见坑。本文将从最基础的语法讲起,深入底层结构和内存管理,全面拆解 Block。


一、Block 是什么

1. 基本概念

一句话解释:Block 是一个封装了代码逻辑和其执行时所需上下文(捕获的变量)的对象,类似于其他语言中的闭包(Closure)或 lambda 表达式。

Block 的核心特点:

  • 封装了一段代码,可以在任意时候调用
  • 可以捕获外部变量,并在执行时使用
  • 本身是一个对象(有 isa 指针),可以被持有、作为参数传递、作为返回值
  • 是 C 语言的扩展,兼容 C、Objective-C、C++

2. 基本语法

Block 的语法看起来有点绕,记住一个公式:

返回值类型 (^Block名称)(参数类型) = ^(参数) { 代码体 };
// 无返回值、无参数
void (^block1)(void) = ^{
    NSLog(@"Hello, Block!");
};

// 无返回值、有参数
void (^block2)(NSString *name) = ^(NSString *name) {
    NSLog(@"你好,%@", name);
};

// 有返回值、有参数
int (^addBlock)(int a, int b) = ^int(int a, int b) {
    return a + b;
};

// 调用
block1();
block2(@"张三");
int result = addBlock(10, 20); // 30

小技巧:返回值类型可以省略,编译器会根据 return 语句自动推断。^int(int a, int b){ return a+b; } 可以简写为 ^(int a, int b){ return a+b; }

3. typedef 简化 Block 声明

Block 类型写起来很长,用 typedef 定义别名可以大幅简化:

// 定义 Block 类型
typedef void (^CompletionBlock)(BOOL success, NSString *message);
typedef int (^CalculateBlock)(int a, int b);

// 使用
@property (nonatomic, copy) CompletionBlock completion;

- (void)requestWithCompletion:(CompletionBlock)completion {
    // ...
}

二、Block 的三种类型(重点)

Block 有三种类型,区别在于内存存储的位置不同。可以通过 NSLog(@"%@", [block class]) 查看 Block 的类型。

类型 类名 存储位置 生命周期
全局 Block _NSConcreteGlobalBlock 数据段(.text 区附近) 程序运行期间一直存在
栈 Block _NSConcreteStackBlock 栈空间 作用域结束后自动销毁
堆 Block _NSConcreteMallocBlock 堆空间 引用计数为 0 时销毁

1. 全局 Block(_NSConcreteGlobalBlock)

判断条件:Block 不捕获任何外部自动变量(局部变量),就是全局 Block。

// 全局作用域定义,不捕获外部变量 → 全局 Block
void (^globalBlock)(void) = ^{
    NSLog(@"I'm a global block");
};

int main() {
    // 函数内定义,但不捕获外部变量 → 也是全局 Block
    void (^globalBlock2)(void) = ^{
        NSLog(@"I'm also a global block");
    };
    
    NSLog(@"%@", [globalBlock2 class]); // _NSConcreteGlobalBlock
}

全局 Block 存储在程序的数据段,生命周期和程序一致,不需要管理内存,copy 操作对它无效(还是返回自身)。

2. 栈 Block(_NSConcreteStackBlock)

判断条件:Block 捕获了外部自动变量(局部变量),且定义在函数/方法内部,在 MRC 下是栈 Block。

- (void)method {
    int a = 10; // 自动变量(局部变量)
    
    // 捕获了外部变量 a → 栈 Block(MRC 下)
    void (^stackBlock)(void) = ^{
        NSLog(@"a = %d", a);
    };
    
    NSLog(@"%@", [stackBlock class]); 
    // MRC: _NSConcreteStackBlock
    // ARC: 可能是 _NSConcreteMallocBlock(ARC 会自动 copy,见后文)
}

栈 Block 存储在栈上,作用域结束后会被自动销毁。如果在作用域外调用栈 Block,会导致 EXC_BAD_ACCESS 崩溃(这是 MRC 时代的经典 bug)。

注意:在 ARC 下,大多数情况下栈 Block 会被自动 copy 到堆上,所以 ARC 下很少能直接看到 _NSConcreteStackBlock。但理解栈 Block 的概念对理解底层原理很重要。

3. 堆 Block(_NSConcreteMallocBlock)

判断条件:对栈 Block 执行 copy 操作后,就变成了堆 Block。

- (void)method {
    int a = 10;
    
    void (^stackBlock)(void) = ^{
        NSLog(@"a = %d", a);
    };
    
    // 对栈 Block 调用 copy → 堆 Block
    void (^heapBlock)(void) = [stackBlock copy];
    
    NSLog(@"%@", [heapBlock class]); // _NSConcreteMallocBlock
}

堆 Block 存储在堆上,生命周期由引用计数管理(ARC 自动管理),作用域结束后不会销毁,只要有强引用就一直存在。

4. 三种类型的 copy 行为

Block 类型 copy 操作结果
全局 Block 什么都不做,返回自身(还是全局 Block)
栈 Block 从栈 copy 到堆,返回堆 Block
堆 Block 引用计数 +1,返回自身(还是堆 Block)

5. 如何查看 Block 类型

void (^block)(void) = ^{
    NSLog(@"test");
};
NSLog(@"Block 类型: %@", [block class]);
// 输出可能是 _NSConcreteGlobalBlock / _NSConcreteStackBlock / _NSConcreteMallocBlock

三、Block 的底层结构(重点)

Block 的本质是一个结构体。通过 LLVM 编译后,一个 Block 的底层结构如下:

struct Block_layout {
    void *isa;                           // ① isa 指针,指向 Block 的类对象
    int flags;                           // ② 标志位,存储 Block 的特性信息
    int reserved;                        // ③ 保留字段
    void (*invoke)(void *, ...);         // ④ 函数指针,指向 Block 的实现代码
    struct Block_descriptor *descriptor; // ⑤ 描述符,存储 Block 大小等信息
    // ⑥ 捕获的变量(紧跟在结构体后面)
};

struct Block_descriptor {
    unsigned long int reserved;          // 保留
    unsigned long int size;              // Block 结构体的大小
    void (*copy_helper)(void *dst, const void *src);  // copy 辅助函数(捕获了对象时才有)
    void (*dispose_helper)(const void *src);           // dispose 辅助函数(捕获了对象时才有)
};

下面逐个字段解释。

1. isa 指针

和所有 Objective-C 对象一样,Block 的第一个字段是 isa 指针,指向 Block 的类对象。根据 Block 类型不同,isa 分别指向:

  • _NSConcreteGlobalBlock(全局 Block)
  • _NSConcreteStackBlock(栈 Block)
  • _NSConcreteMallocBlock(堆 Block)

这就是为什么 Block 是一个对象——它有 isa 指针,可以被当作 id 类型传递。

2. flags(标志位)

flags 是一个 int,每一位代表不同的含义:

标志位 含义
BLOCK_DEALLOCATING (1<<0) Block 正在释放
BLOCK_REFCOUNT_MASK 引用计数(低 3 位,实际引用计数存在这里)
BLOCK_NEEDS_FREE (1<<24) Block 是否需要释放(堆 Block 需要)
BLOCK_HAS_COPY_DISPOSE (1<<25) 是否有 copy/dispose 辅助函数(捕获了 OC 对象时设置)
BLOCK_HAS_CTOR (1<<26) 是否有 C++ 构造函数
BLOCK_IS_GLOBAL (1<<28) 是否是全局 Block
BLOCK_USE_STRET (1<<29) 返回值是否是结构体(需要 stret 调用)
BLOCK_HAS_SIGNATURE (1<<30) 是否有类型签名(包含参数和返回值类型信息)

3. invoke(函数指针)

invoke 是一个函数指针,指向 Block 代码体编译后生成的 C 函数。调用 Block 时,本质上就是调用这个函数指针。

// 这个 Block:
int (^addBlock)(int a, int b) = ^int(int a, int b) {
    return a + b;
};

// 编译后大致变成:
int addBlock_invoke(struct Block_layout *block, int a, int b) {
    return a + b;
}
// block->invoke = addBlock_invoke

调用 addBlock(10, 20) 本质上就是 block->invoke(block, 10, 20),第一个参数是 Block 自身(类似 OC 方法的 self),这样函数内部才能访问捕获的变量。

4. descriptor(描述符)

描述符存储 Block 的附加信息:

  • size:Block 结构体的总大小(包括捕获变量的空间)
  • copy_helper:Block 从栈 copy 到堆时调用,负责 retain 捕获的 OC 对象
  • dispose_helper:Block 释放时调用,负责 release 捕获的 OC 对象

只有当 Block 捕获了 OC 对象(或 __block 变量)时,descriptor 才会包含 copy_helper 和 dispose_helper。如果只捕获了基本数据类型(int、float 等),则不需要这两个函数。

5. 捕获的变量

捕获的变量紧跟在 Block_layout 结构体后面,作为结构体的一部分存储。每个捕获的变量在 Block 内部都有一个副本。

int a = 10;
NSString *name = @"张三";

void (^block)(void) = ^{
    NSLog(@"a = %d, name = %@", a, name);
};

// 编译后 Block 的内存布局大致是:
// struct Block_layout {
//     isa, flags, reserved, invoke, descriptor,
//     int a;          // 捕获的基本类型变量(值拷贝)
//     NSString *name; // 捕获的 OC 对象(指针拷贝,ARC 下会 retain)
// };

6. Block 调用的完整底层过程

以这个 Block 为例:

- (void)test {
    int multiplier = 3;
    
    int (^multiplyBlock)(int) = ^int(int x) {
        return x * multiplier;
    };
    
    int result = multiplyBlock(5); // 15
}

底层执行过程:

  1. 编译器在栈上创建 Block_layout 结构体,设置 isa = _NSConcreteStackBlock,invoke 指向编译生成的函数,捕获的 multiplier = 3 存在结构体末尾。
  2. 调用 multiplyBlock(5) 时,编译器转为 block->invoke(block, 5)
  3. invoke 函数内部通过 block 指针找到捕获的 multiplier = 3,计算 5 * 3 = 15,返回结果。
  4. 方法作用域结束,栈上的 Block 被自动销毁(MRC 下;ARC 下如果被强引用会先 copy 到堆)。

四、Block 的变量捕获(重点)

Block 可以捕获外部变量,但不同类型的变量捕获方式不同。这是理解 Block 的核心,也是面试高频考点。

1. 自动变量(局部变量)—— 值捕获

自动变量就是函数/方法内定义的局部变量(非 static)。Block 对自动变量是值捕获——在 Block 定义时,把变量的值拷贝一份存到 Block 内部,之后外部变量的变化不影响 Block 内部的值。

int a = 10;

void (^block)(void) = ^{
    NSLog(@"Block 内: a = %d", a); // 捕获时的值 10
};

a = 20; // 外部修改 a

block(); // 输出:Block 内: a = 10(不是 20!)

为什么是值捕获? 因为自动变量存储在栈上,作用域结束后就销毁了。如果 Block 是引用捕获,Block 执行时变量可能已经销毁,导致野指针。所以 Block 在定义时把值拷贝一份存到自己的结构体里,保证安全。

Block 内不能修改自动变量

int a = 10;
void (^block)(void) = ^{
    // a = 20; // 编译报错!Variable is not assignable (missing __block type specifier)
    NSLog(@"%d", a); // 只能读,不能写
};

因为 Block 内部的 a 是值拷贝的常量,修改它没有意义(外部的 a 不会变),所以编译器直接禁止。

2. 静态变量(static)—— 引用捕获

static 修饰的局部变量存储在静态存储区(全局数据区),生命周期和程序一致。Block 对静态变量是引用捕获——存储变量的地址,Block 内可以读写,且和外部共享同一个变量。

static int a = 10;

void (^block)(void) = ^{
    NSLog(@"Block 内: a = %d", a);
    a = 30; // 可以修改
};

a = 20;
block(); // 输出:Block 内: a = 20(引用捕获,外部修改生效)
NSLog(@"Block 外: a = %d", a); // 输出:Block 外: a = 30(Block 内修改也生效)

3. 全局变量 / 静态全局变量 —— 不捕获

全局变量和静态全局变量存储在全局数据区,任何地方都可以直接访问,Block 不需要捕获,直接使用即可。

int globalVar = 10; // 全局变量
static int staticGlobalVar = 20; // 静态全局变量

void (^block)(void) = ^{
    NSLog(@"%d, %d", globalVar, staticGlobalVar); // 直接访问,不捕获
    globalVar = 30; // 可以修改
};

4. self 的捕获

在 OC 方法中,self 是一个隐藏的自动变量(指向当前对象)。Block 内使用 self 的属性或方法时,会捕获 self(强引用,ARC 下会 retain self)。

@interface MyClass : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) void (^block)(void);
@end

@implementation MyClass

- (void)test {
    self.name = @"张三";
    
    // Block 内使用了 self.name → 捕获了 self(强引用)
    self.block = ^{
        NSLog(@"%@", self.name); // 这里捕获了 self
    };
}

@end

这就是循环引用的根源——self 持有 block(属性),block 又持有 self(捕获),形成循环。详见后文"循环引用"章节。

如果 Block 内只用到了某个属性,也可以只捕获属性的值(在 Block 定义时取出),避免捕获 self:

- (void)test {
    NSString *name = self.name; // 提前取出,值捕获
    self.block = ^{
        NSLog(@"%@", name); // 只捕获了 name 变量,没有捕获 self
    };
}

5. 变量捕获方式总结

变量类型 存储位置 捕获方式 Block 内能否修改 外部变化是否影响 Block 内
自动变量(局部) 值捕获(拷贝值) 不能 不影响
静态变量(static 局部) 静态区 引用捕获(拷贝地址) 影响
全局变量 全局区 不捕获,直接访问 影响
静态全局变量 全局区 不捕获,直接访问 影响
self(方法内) 值捕获(拷贝指针,ARC 下 retain) 能修改属性 影响
__block 变量 栈→堆 引用捕获(通过 forwarding 指针) 影响

五、__block 底层原理深入(重点+高频面试题)

1. __block 的作用

__block 修饰符允许 Block 内部修改外部的自动变量:

__block int a = 10;

void (^block)(void) = ^{
    a = 20; // 可以修改了
    NSLog(@"a = %d", a);
};

block(); // a = 20
NSLog(@"a = %d", a); // a = 20(外部也变了)

2. __block 变量的底层结构

__block 变量在编译后会被包装成一个结构体 Block_byref

// __block int a = 10; 编译后大致变成:
struct Block_byref_int {
    void *isa;                // isa 指针(__block 变量也是一个对象)
    struct Block_byref_int *forwarding; // 指向自身或堆上的副本
    int flags;                 // 标志位
    int size;                  // 结构体大小
    int a;                     // 实际存储的变量值
};

struct Block_byref_int a = {
    .isa = _NSConcreteStackBlock, // 栈上的 __block 变量
    .forwarding = &a,              // 指向自身
    .flags = 0,
    .size = sizeof(struct Block_byref_int),
    .a = 10                        // 初始值
};

关键点:

  • __block 变量不再是一个简单的 int,而是一个结构体。
  • 结构体里有一个 forwarding 指针,初始时指向栈上的自己。
  • 实际的值 a 存储在结构体内部。

3. forwarding 指针的作用(核心)

当 Block 从栈 copy 到堆时,Block 捕获的 __block 变量也会从栈 copy 到堆。此时:

  • 栈上的 __block 变量的 forwarding 指针被修改为指向堆上的副本
  • 堆上的 __block 变量的 forwarding 指针指向自身
copy 之前(都在栈上):
栈上 __block变量.forwarding → 栈上 __block变量(指向自己)

copy 之后(Block 和 __block 变量都到了堆上):
栈上 __block变量.forwarding → 堆上 __block变量(指向堆上的副本!)
堆上 __block变量.forwarding → 堆上 __block变量(指向自己)

这样,无论代码是在 Block 内部(访问堆上的变量)还是 Block 外部(访问栈上的变量),通过 forwarding 指针最终都指向堆上的同一个副本,保证了值的一致性。

__block int a = 10;

void (^block)(void) = ^{
    a = 20; // 内部通过 forwarding 指针访问堆上的 a
};

a = 30; // 外部也通过 forwarding 指针访问堆上的 a(Block copy 后)

block();
// 最终 a = 20,内外一致

这就是 __block 变量能在 Block 内外共享的核心原理——通过 forwarding 指针统一指向堆上的副本。

4. __block 变量的内存管理

  • Block copy 到堆时,会同时 copy 它捕获的 __block 变量到堆,并 retain(如果是 OC 对象)。
  • 多个 Block 捕获同一个 __block 变量时,堆上的 __block 变量有引用计数,最后一个 Block 释放时才释放 __block 变量。
  • __block 修饰的 OC 对象变量,在 ARC 下会被 Block retain(和普通捕获的 OC 对象一样),所以也可能导致循环引用。

5. __block 与循环引用

在 MRC 时代,常用 __block 打破循环引用:

// MRC 下的写法
__block id weakSelf = self; // __block 修饰的对象不会被 retain
self.block = ^{
    [weakSelf doSomething]; // weakSelf 是 __block,不被 retain,不循环引用
};

但在 ARC 下,__block 修饰的对象会被 Block retain,所以 ARC 下不能用 __block 打破循环引用,必须用 __weak

// ARC 下 __block 不能打破循环引用!
__block id blockSelf = self;
self.block = ^{
    [blockSelf doSomething]; // ARC 下 blockSelf 会被 retain,仍然循环引用!
};

六、Block 的内存管理

1. MRC 下的 Block 内存管理

MRC 下需要手动管理 Block 的内存:

  • 栈 Block:作用域结束后自动销毁,不能在作用域外使用。
  • 如果需要在作用域外使用 Block,必须调用 copy 将其 copy 到堆。
  • 堆 Block 使用完需要 release
  • Block 作为属性必须用 copy 修饰(不能用 retain,因为 retain 栈 Block 不会把它移到堆上)。
// MRC 下
@property (nonatomic, copy) void (^block)(void); // 必须 copy

- (void)test {
    int a = 10;
    self.block = ^{ // 赋值给 copy 属性时会自动 copy
        NSLog(@"%d", a);
    };
}

2. ARC 下的 Block 自动 copy

ARC 下,编译器会在以下情况自动将栈 Block copy 到堆:

场景 是否自动 copy
Block 作为函数/方法返回值 自动 copy
Block 赋值给 strong/copy 修饰的属性或变量 自动 copy
Block 作为参数传递给 NSArray/NSDictionary 的方法(如 addObject) 自动 copy
Block 作为参数传递给 GCD 方法(dispatch_async 等) 自动 copy(GCD 内部会 copy)
Block 作为参数传递给 enumerateObjectsUsingBlock: 等方法 自动 copy(方法内部会 copy)
Block 直接调用(不传递、不赋值) 不 copy,仍在栈上
Block 作为方法参数传递(普通方法,方法内部不 copy) 不 copy,仍在栈上

简单来说:只要 Block 被传递到作用域之外(赋值、返回、存入集合、传入 GCD 等),ARC 就会自动 copy 到堆。 只有在当前作用域内直接调用时,才可能停留在栈上。

所以 ARC 下你几乎看不到 _NSConcreteStackBlock,大部分 Block 都被自动 copy 成了堆 Block。

3. Block 作为属性用 copy 还是 strong?

修饰符 MRC ARC
copy 必须用 copy 推荐用 copy(习惯延续,效果和 strong 一样)
strong 不能用(retain 栈 Block 不会移到堆) 可以用,效果和 copy 一样(ARC 会自动 copy)
weak/assign 不能用 不能用(Block 会被立即释放)

结论

  • MRC 下必须用 copy
  • ARC 下用 copystrong 都可以,但约定俗成用 copy,这样代码在 MRC/ARC 下都安全,也更清晰地表达了"Block 需要 copy 到堆"的语义。
// 推荐写法
@property (nonatomic, copy) void (^completionBlock)(void);
@property (nonatomic, copy) NSString *name; // NSString 也用 copy,同理

4. Block 作为方法参数

Block 作为方法参数时,不需要用 copy 修饰(参数是局部的,方法内部如果需要持有会自己 copy):

- (void)doSomethingWithCompletion:(void (^)(BOOL success))completion {
    // 方法内部如果需要异步持有 completion,会自己 copy
    self.completion = completion; // 赋值给 copy 属性时自动 copy
}

5. Block 的 copy 操作做了什么

对一个栈 Block 调用 copy 时:

  1. 在堆上分配一块和 Block 大小相同的内存。
  2. 将栈 Block 的内容(isa、flags、invoke、descriptor、捕获的变量)拷贝到堆上。
  3. 将堆 Block 的 isa 改为 _NSConcreteMallocBlock,flags 设置 BLOCK_NEEDS_FREE
  4. 如果捕获了 OC 对象或 __block 变量,调用 copy_helper
    • retain 捕获的 OC 对象。
    • __block 变量也 copy 到堆,并修改其 forwarding 指针。
  5. 返回堆 Block 的地址。

Block 释放时调用 dispose_helper,release 捕获的 OC 对象和 __block 变量。


七、Block 的循环引用(重点+高频面试题)

1. 循环引用产生的原因

循环引用(Retain Cycle)是指两个对象互相强引用,导致引用计数永远不为 0,内存无法释放。

Block 导致循环引用的经典场景:

@interface MyClass : NSObject
@property (nonatomic, copy) void (^block)(void); // self 强引用 block
@end

@implementation MyClass

- (void)test {
    self.block = ^{
        NSLog(@"%@", self); // block 强引用 self(捕获了 self)
    };
}
// self → block → self,形成循环引用,self 无法释放

@end

关系图:

self ──strong──▶ block
  ▲                  │
  └────strong────────┘ (block 捕获了 self)

2. 哪些场景会循环引用,哪些不会?

会循环引用的场景

场景一:self 持有 Block 属性,Block 内引用 self

// 会循环引用
self.block = ^{
    [self doSomething];
};

场景二:NSTimer + Block(iOS 10+ 的 block 形式 API)

// 会循环引用:timer 强引用 self(block 捕获 self),self 强引用 timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    [self doSomething];
}];

场景三:__block 修饰对象(ARC 下)

// ARC 下会循环引用(__block 对象会被 retain)
__block MyClass *blockSelf = self;
self.block = ^{
    [blockSelf doSomething];
};

不会循环引用的场景

场景一:Block 作为方法参数(GCD、UIView 动画等)

// 不会循环引用:GCD 执行完就释放 block,不长期持有 self
dispatch_async(dispatch_get_main_queue(), ^{
    [self doSomething]; // self 被 block 强引用,但 block 是临时的,执行完就释放
});

// 不会循环引用:UIView 动画 block 执行完就释放
[UIView animateWithDuration:0.3 animations:^{
    self.view.alpha = 0.5;
}];

判断标准:self 是否长期持有这个 Block? 如果 Block 是临时的(作为参数传入,执行完就释放),就不会循环引用;如果 self 把 Block 存为属性长期持有,且 Block 内引用了 self,就会循环引用。

场景二:Block 内不引用 self

// 不会循环引用:block 没有捕获 self
self.block = ^{
    NSLog(@"hello"); // 没用到 self
};

场景三:提前取出属性值(值捕获)

// 不会循环引用:只捕获了 name 变量,没有捕获 self
NSString *name = self.name;
self.block = ^{
    NSLog(@"%@", name);
};

3. 解决方案

方案一:__weak + __strong(weak-strong dance,推荐)

这是 ARC 下最标准、最推荐的解决方案:

__weak typeof(self) weakSelf = self; // 弱引用,不增加引用计数
self.block = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf; // 进入 block 后临时强引用,防止执行中 self 被释放
    if (strongSelf) { // 判空,weakSelf 可能为 nil
        [strongSelf doSomething];
        NSLog(@"%@", strongSelf.name);
    }
};

为什么需要 __strong?

  • __weak 引用的对象可能在 Block 执行过程中被释放(变成 nil)。
  • 如果 Block 内有多个操作,第一个操作后 self 被释放,后面的操作就会访问 nil(虽然 OC 给 nil 发消息不崩溃,但逻辑可能出错)。
  • __strong 在 Block 开始时临时持有 self,保证 Block 执行期间 self 不被释放,Block 执行完后 strongSelf 自动释放,不影响 self 的正常销毁。

简单场景可以只用 __weak

// 如果 Block 内只有一个简单操作,可以不用 __strong
__weak typeof(self) weakSelf = self;
self.block = ^{
    [weakSelf doSomething]; // 即使 weakSelf 为 nil,给 nil 发消息也安全
};

方案二:使用 ReactiveObjC 的 @weakify/@strongify 宏

如果项目用了 ReactiveObjC(RAC),可以用更简洁的宏:

@weakify(self);
self.block = ^{
    @strongify(self);
    if (self) {
        [self doSomething];
    }
};

本质和 weak-strong dance 一样,只是宏帮你写了样板代码。

方案三:手动断开引用

在合适的时机把 Block 置为 nil,打破循环:

// 页面消失时断开
- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    self.block = nil; // 打破循环引用
}

// 或 Block 执行完后自己置 nil
self.block = ^{
    [self doSomething];
    self.block = nil; // 执行完后断开(注意:block 内引用 self 仍会循环,但执行完后置 nil 就打破了)
};

这种方式不推荐作为主要方案,因为容易忘记断开。weak-strong dance 是更安全的默认选择。

方案四:NSTimer 的解决方案

NSTimer 的循环引用比较特殊,因为 timer 会被 RunLoop 持有:

// 方案一:用 __weak(iOS 10+ block API)
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer *timer) {
    [weakSelf doSomething];
}];

// 方案二:在 dealloc 前 invalidate(必须在 viewWillDisappear 等时机,不能在 dealloc,因为循环引用导致 dealloc 不会调用)
- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [self.timer invalidate];
    self.timer = nil;
}

// 方案三:用 NSProxy 中间对象打破引用(YYWeakProxy 等)

4. 循环引用检测工具

工具 说明
Instruments → Leaks 苹果官方,可检测循环引用和内存泄漏
Instruments → Allocations 查看对象引用计数变化
FBRetainCycleDetector Facebook 开源,代码级循环引用检测
MLeaksFinder 腾讯开源,自动检测 UIViewController 泄漏
Xcode → Debug Memory Graph Xcode 8+ 内置,可视化查看对象引用关系图

Xcode Memory Graph 使用方法

  1. 运行 App,进入可疑页面。
  2. 点击 Xcode 底部调试栏的"Debug Memory Graph"按钮(三个圆圈的图标)。
  3. Xcode 会显示当前内存中的对象及引用关系图。
  4. 如果有循环引用,Xcode 会用紫色图标提示。
  5. 点击对象可以查看引用链,定位循环引用的原因。

八、Block 的常见使用场景

1. 回调(代替 delegate)

Block 最常用的场景就是回调,比 delegate 更简洁,代码聚合在一起:

// 网络请求回调
- (void)requestDataWithCompletion:(void (^)(NSArray *data, NSError *error))completion {
    // 异步请求...
    if (completion) {
        completion(data, nil);
    }
}

// 调用
[self requestDataWithCompletion:^(NSArray *data, NSError *error) {
    if (error) {
        NSLog(@"失败: %@", error);
    } else {
        NSLog(@"成功: %@", data);
    }
}];

2. 枚举遍历

NSArray *array = @[@"张三", @"李四", @"王五"];

// 遍历数组
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    NSLog(@"第 %lu 个: %@", idx, obj);
    if ([obj isEqualToString:@"李四"]) {
        *stop = YES; // 停止遍历
    }
}];

// 带选项的遍历(反向、并发)
[array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    NSLog(@"反向: %@", obj);
}];

// 字典遍历
NSDictionary *dict = @{@"name": @"张三", @"age": @18};
[dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
    NSLog(@"%@ = %@", key, obj);
}];

3. 排序

NSArray *numbers = @[@3, @1, @4, @1, @5, @9, @2, @6];

// 升序排序
NSArray *sorted = [numbers sortedArrayUsingComparator:^NSComparisonResult(NSNumber *obj1, NSNumber *obj2) {
    return [obj1 compare:obj2]; // 升序
    // return [obj2 compare:obj1]; // 降序
}];

// 按对象属性排序
NSArray *people = @[person1, person2, person3];
NSArray *sortedPeople = [people sortedArrayUsingComparator:^NSComparisonResult(Person *p1, Person *p2) {
    return [p1.name compare:p2.name]; // 按姓名排序
}];

4. GCD 多线程

// 异步执行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 后台线程执行耗时操作
    NSData *data = [NSData dataWithContentsOfURL:url];
    
    dispatch_async(dispatch_get_main_queue(), ^{
        // 回主线程更新 UI
        self.imageView.image = [UIImage imageWithData:data];
    });
});

// 延迟执行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"2 秒后执行");
});

// 只执行一次(单例)
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    // 只执行一次的代码
});

5. UIView 动画

[UIView animateWithDuration:0.3 animations:^{
    self.view.alpha = 0.5;
    self.view.frame = CGRectMake(0, 0, 100, 100);
} completion:^(BOOL finished) {
    NSLog(@"动画完成");
}];

// 弹性动画
[UIView animateWithDuration:0.5 delay:0 usingSpringWithDamping:0.8 initialSpringVelocity:0.5 options:0 animations:^{
    self.view.center = CGPointMake(200, 200);
} completion:nil];

6. 通知(Block 形式)

// 注册通知(Block 形式,需要手动移除)
id observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIKeyboardWillShowNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
    NSLog(@"键盘将要显示");
}];

// 移除通知
[[NSNotificationCenter defaultCenter] removeObserver:observer];

九、Block 与 delegate / 通知的对比

1. 对比表

对比维度 Block Delegate 通知(NSNotification)
通信方式 一对一 一对一 一对多(广播)
代码聚合 高(回调代码和调用代码在一起) 中(需要实现代理方法,分散在类中) 低(注册和处理分离)
灵活性 高(可以捕获上下文变量) 中(需要定义协议) 高(任意对象都可以监听)
循环引用风险 高(需要 weak-strong dance) 低(delegate 用 weak) 低(需要移除观察者)
多回调支持 可以(多个 block 参数) 可以(协议多个方法) 可以(多个通知名)
适用场景 简单回调、异步操作、枚举遍历 复杂交互、多方法回调、需要返回值 全局事件、多对象响应、解耦

2. 选型建议

  • 简单回调(网络请求、UI 动画完成):优先用 Block,代码聚合,可读性好。
  • 复杂交互(多个回调方法、需要返回值、如 UITableViewDataSource):用 Delegate,协议定义清晰,方法分离。
  • 全局事件(登录状态变化、键盘弹出、系统事件):用通知,一对多广播,解耦。
  • 多个对象需要响应同一事件:用通知。
  • 回调需要传递大量上下文变量:用 Block(自动捕获变量)。

十、常见问题排查

问题 1:EXC_BAD_ACCESS 崩溃(Block 调用时)

原因:MRC 下,栈 Block 超出作用域后被销毁,之后调用导致野指针崩溃。

// MRC 下的经典 bug
- (void)method {
    int a = 10;
    self.block = ^{ // 如果属性是 retain 而不是 copy,block 还是栈上的
        NSLog(@"%d", a);
    };
}
// 方法结束,栈 block 被销毁
// 之后调用 self.block() → EXC_BAD_ACCESS

// 解决:属性用 copy,或手动 copy
self.block = [^{
    NSLog(@"%d", a);
} copy];

ARC 下:基本不会出现这个问题,因为 ARC 会自动 copy。但如果用 __unsafe_unretained 修饰 Block 变量,仍可能崩溃。

问题 2:循环引用导致内存泄漏

现象:页面 pop/dismiss 后,dealloc 不调用,内存不释放。

排查

  1. 检查所有 Block 属性,Block 内是否引用了 self。
  2. 检查 NSTimer 是否 invalidate。
  3. 用 Xcode Memory Graph 或 Instruments Leaks 检测。

解决:用 __weak typeof(self) weakSelf = self; + __strong typeof(weakSelf) strongSelf = weakSelf;

问题 3:Block 内修改外部变量不生效

原因:普通自动变量是值捕获,Block 内修改的是副本,外部不变。而且编译器直接禁止修改(报错)。

int a = 10;
void (^block)(void) = ^{
    // a = 20; // 编译报错
};

解决:用 __block 修饰变量:

__block int a = 10;
void (^block)(void) = ^{
    a = 20; // 可以修改
};
block();
NSLog(@"%d", a); // 20

问题 4:Block 内变量值不是最新的

原因:普通自动变量是值捕获,Block 定义时的值被拷贝,之后外部修改不影响 Block 内。

int a = 10;
void (^block)(void) = ^{
    NSLog(@"a = %d", a); // 捕获时的值 10
};
a = 20;
block(); // 输出 a = 10(不是 20)

解决

  • 如果需要最新值,用 __block 修饰变量。
  • 或用静态变量/全局变量(引用捕获)。
  • 或在 Block 调用时再传入参数。

问题 5:Block 作为属性用 strong 还是 copy?

答案:约定俗成用 copy

  • MRC 下必须用 copy。
  • ARC 下 strong 和 copy 效果一样,但用 copy 更清晰、更安全(兼容 MRC)。
  • 绝对不能用 weak/assign/retain(MRC 下 retain 栈 Block 不会移到堆)。

问题 6:Block 内引用 self 一定循环引用吗?

不一定。判断标准:self 是否长期持有这个 Block?

  • self 把 Block 存为属性/成员变量 → 长期持有 → Block 内引用 self → 循环引用。
  • Block 作为方法参数临时使用(GCD、UIView 动画、enumerateObjectsUsingBlock)→ 执行完就释放 → 不循环引用。
  • Block 内不引用 self → 不循环引用。

十一、Swift 版本对照(闭包)

Swift 中的闭包(Closure)和 OC 的 Block 本质相同,语法更简洁:

1. 基本语法

// 无参数无返回值
let block1: () -> Void = {
    print("Hello")
}

// 有参数有返回值
let add: (Int, Int) -> Int = { a, b in
    return a + b
}

// 简写(类型推断)
let add2 = { (a: Int, b: Int) -> Int in
    return a + b
}

// 调用
block1()
let result = add(10, 20) // 30

2. 变量捕获

var a = 10
let block = {
    print(a) // Swift 闭包默认引用捕获(不是值捕获!)
}
a = 20
block() // 输出 20(Swift 是引用捕获,和 OC 的值捕获不同)

注意:Swift 闭包对 var 变量是引用捕获(类似 OC 的 __block),对 let 常量是值捕获。这和 OC Block 不同,需要注意。

3. 循环引用

class MyClass {
    var block: (() -> Void)?
    
    func test() {
        // 用 weak 打破循环引用
        block = { [weak self] in
            guard let self = self else { return } // 类似 OC 的 strongSelf
            self.doSomething()
        }
    }
    
    func doSomething() {}
}

Swift 用 [weak self] 捕获列表打破循环引用,guard let self = self else { return } 类似 OC 的 weak-strong dance。

4. 闭包作为参数和尾随闭包

// 闭包作为参数
func doSomething(completion: () -> Void) {
    completion()
}

// 调用
doSomething(completion: {
    print("完成")
})

// 尾随闭包语法(最后一个参数是闭包时可以写在括号外面)
doSomething {
    print("完成")
}

// GCD
DispatchQueue.global().async {
    // 后台执行
    DispatchQueue.main.async {
        // 主线程更新 UI
    }
}

5. @escaping 和 @nonescaping

// @escaping:闭包在函数返回后才执行(异步回调),需要标记
func request(completion: @escaping (Data) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, _ in
        if let data = data {
            completion(data) // 异步执行,函数已返回
        }
    }.resume()
}

// @nonescaping(默认):闭包在函数返回前执行完
func syncProcess(handler: (Int) -> Void) {
    handler(10) // 同步执行,函数返回前完成
}

十二、总结

  • Block 是什么:封装了代码逻辑和捕获变量的对象,类似闭包/lambda,有 isa 指针,是 OC 对象。
  • 三种类型:全局 Block(不捕获外部变量,存在数据段)、栈 Block(捕获外部变量,存在栈上,作用域结束销毁)、堆 Block(对栈 Block copy 后存在堆上,引用计数管理)。
  • 底层结构Block_layout 结构体,包含 isa、flags、reserved、invoke(函数指针)、descriptor(描述符)、捕获变量。调用 Block 本质是调用 invoke 函数指针。
  • 变量捕获:自动变量(局部)是值捕获(拷贝值,不能修改);静态变量是引用捕获(拷贝地址,可修改);全局变量不捕获直接访问;self 被捕获(强引用);__block 变量通过 forwarding 指针实现内外共享。
  • __block 原理:变量被包装成 Block_byref 结构体,有 forwarding 指针。Block copy 到堆时,__block 变量也 copy 到堆,栈上的 forwarding 指向堆上副本,保证内外访问同一个值。ARC 下 __block 对象会被 retain,不能打破循环引用。
  • 内存管理:MRC 下属性必须用 copy,手动管理;ARC 下大多数情况自动 copy 到堆,属性用 copy(约定俗成)或 strong 均可。
  • 循环引用:self 持有 Block 属性且 Block 内引用 self 时发生。解决用 __weak + __strong(weak-strong dance)。GCD/UIView 动画等临时 Block 不会循环引用。NSTimer + Block 会循环引用,需要 invalidate 或用 weak。
  • 使用场景:回调、枚举遍历、排序、GCD 多线程、UIView 动画、通知。
  • 与 delegate/通知对比:Block 适合简单回调(代码聚合),delegate 适合复杂多方法交互,通知适合一对多全局事件。
  • 常见坑:栈 Block 超作用域崩溃(MRC)、循环引用泄漏、值捕获导致值不更新、__block 在 ARC 下不能打破循环引用。

posted @ 2016-03-26 14:27  Mr.陳  阅读(4970)  评论(0)    收藏  举报