基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

Objective-C 中 Block 有哪几种类型?

知识点图片

在 Objective-C 中,Block 本质上是一个 Objective-C 对象。根据 Block 在内存中的位置以及它是否捕获了外部变量,Block 主要分为三种类型:

  1. __NSGlobalBlock__ (全局 Block)
  2. __NSStackBlock__ (栈 Block)
  3. __NSMallocBlock__ (堆 Block)

下面详细解释这三种类型及其产生条件:


1. __NSGlobalBlock__ (全局 Block)

  • 内存位置:数据区(Data Segment / .data 区),类似于全局变量。
  • 产生条件
    • Block 内部没有访问任何外部的局部变量(自动变量)。
    • 或者访问的是全局变量、静态变量(static)。
  • 生命周期:与应用程序的生命周期相同,程序结束时才释放。
  • 特点:对它进行 copy 操作无效,仍然是全局 Block。

代码示例:

plaintext
void (^globalBlock)(void) = ^{
    NSLog(@"Hello World");
};
NSLog(@"%@", [globalBlock class]); // 输出: __NSGlobalBlock__

2. __NSStackBlock__ (栈 Block)

  • 内存位置:栈区(Stack)。
  • 产生条件
    • Block 内部访问了外部的局部变量(自动变量)。
    • 该 Block 没有被强引用(在 ARC 下情况比较特殊,见下文)。
  • 生命周期:由系统自动管理。作用域结束(大括号结束)时,Block 就会被销毁。
  • 特点
    • 如果 Block 在栈上,一旦作用域结束,Block 内存被回收。如果此时再调用该 Block,会导致 Crash(野指针)。
    • 为了解决这个问题,通常需要将其 copy 到堆上。

注意(ARC vs MRC):

  • MRC (Manual Reference Counting) 下,访问了外部局部变量的 Block 默认就是栈 Block。
  • ARC (Automatic Reference Counting) 下,编译器非常智能。只要你把 Block 赋值给一个强引用(Strong)变量,或者作为返回值返回,编译器会自动帮你执行 copy 操作,将其变为堆 Block。
  • 在 ARC 下要看到栈 Block,通常是在打印时直接创建 Block 而不赋值,或者使用 __weak 修饰。

代码示例 (ARC 环境):

plaintext
int age = 10;
// 直接打印,没有赋值给强引用变量
NSLog(@"%@", [^{
    NSLog(@"%d", age);
} class]); // 输出: __NSStackBlock__

3. __NSMallocBlock__ (堆 Block)

  • 内存位置:堆区(Heap)。
  • 产生条件
    • 当一个 __NSStackBlock__ 被执行了 copy 操作后,就会变成 __NSMallocBlock__
    • 在 ARC 中,当 Block 捕获了外部变量并赋值给 __strong 修饰的变量(或属性)时,系统自动将其拷贝到堆上。
  • 生命周期:由引用计数管理(Reference Counting)。当引用计数为 0 时销毁。
  • 特点:这是我们在开发中最常接触到的 Block 类型,因为我们需要 Block 在作用域结束后依然存在(例如网络回调、按钮点击事件)。

代码示例 (ARC 环境):

plaintext
int age = 10;
void (^mallocBlock)(void) = ^{
    NSLog(@"%d", age);
};
// ARC下,赋值给变量会自动 copy 到堆上
NSLog(@"%@", [mallocBlock class]); // 输出: __NSMallocBlock__

总结对比表

Block 类型 类名 存储区域 捕获局部变量 Copy 操作结果
全局 Block __NSGlobalBlock__ 数据区 (.data) 什么也不做 (仍是 Global)
栈 Block __NSStackBlock__ 栈区 (Stack) 复制到堆区 (变成 Malloc)
堆 Block __NSMallocBlock__ 堆区 (Heap) 是 (源自栈) 增加引用计数

为什么需要 copy

在 MRC 时代,Block 默认在栈上。如果 Block 是在一个函数内部定义的,并且作为返回值返回,或者被异步线程持有,当函数执行完毕,栈帧销毁,Block 的内存也就销毁了。此时再去调用 Block 就会崩溃。

执行 copy 操作可以将 Block 的内容(包括它捕获的变量值)从复制到上。堆上的内存由程序员(或 ARC)管理,从而保证 Block 在作用域结束后依然存活。

00:00
00:00