【iOS】Block基础知识和底层探索

发布于:2025-08-13 ⋅ 阅读:(14) ⋅ 点赞:(0)

前言

  最近在复习OC知识,发现自己对于block的了解很少,很多东西当时都没有搞明白或者甚至不知道,然后就对block进行了重新学习。

Block的声明和创建

Block 的语法格式为:

返回值类型 (^Block名称)(参数列表) = ^返回值类型(参数列表) { 代码块 };

示例:

// 声明并创建一个无参数、无返回值的 Block
void (^myBlock)(void) = ^void(void) {
    NSLog(@"Block 执行");
};

// 调用 Block
myBlock(); // 输出:"Block 执行"

问题引入

由此,我们有以下代码,后面所有的实例分析都是在此基础上:

请添加图片描述

运行结果:

请添加图片描述

我们可以看到,上述代码进行了两次block捕获,第一次捕获A和B的初始值,A为3,B为7;然后在myblock函数外对A、B变量进行自增修改处理,再次调用myblock函数进行捕获,变量A输出不变为3,变量B输出为自增后的值8,控制变量可以发现,造成这种结果,唯一不同的是我们在定义变量B时,在前面用了==__block==进行修饰。

由此我们产生了 一系列问题:block内部是什么结构?为什么在block外部修改变量不会影响到其在block内的捕获?为什么声明时用__block进行修饰后,外部修改变量block捕获也会变?可不可以直接在block内部进行变量修改?

下面,我们来对这些问题进行一一探讨。

Block的底层结构

我们用clang -rewrite-objc main.m -o main.cpp将上述代码文件转换成C++文件:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int A = 3;
        __attribute__((__blocks__(byref))) __Block_byref_B_0 B = {(void*)0,(__Block_byref_B_0 *)&B, 0, sizeof(__Block_byref_B_0), 7};
        void(*myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, A, (__Block_byref_B_0 *)&B, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);
        A++;
        (B.__forwarding->B)++;z
        ((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);
    }
    return 0;
}

我们可以看到,我们定义myblock函数的代码在C++中是这样的:

请添加图片描述

上述代码Block底层实现的编译器生成代码,代码的核心是将一个 Block 的底层结构体指针转换为函数指针,并赋值给 myblock变量,具体形式:

void(*myblock)(void) = ((void (*)())&__main_block_impl_0(...));

等号左边等名义个函数指针myblock,类型为void(*)(void)(无参数、无返回值);

等号右边通过调用__main_block_impl_0函数生成 Block 的底层结构体,并将其转换为函数指针后赋值给 myblock。

__main_block_impl_0是编译器自动生成的 Block 初始化函数,负责创建 Block 的底层结构体(如 struct __block_impl)并初始化其核心字段。其参数通常包括:

参数 类型/含义
(void *)__main_block_func_0 指向 Block 实际执行逻辑的函数指针(即 FuncPtr字段的内容)
&__main_block_desc_0_DATA 指向 Block 描述符(struct __block_descriptor)的指针,包含 Block 的元数据(如大小、复制/销毁函数)
A 被 Block 捕获的变量(如基本类型、不可变对象)
(__Block_byref_B_0 *)&B 指向 __Block_byref_B_0结构体的指针,用于捕获可变对象或需要引用捕获的变量
570425344 标志位(可能表示 Block 的行为选项,如是否复制捕获变量、是否启用优化等)

从刚刚我们在main.m的cpp文件里看到的定义函数,我们继续探究找寻__main_block_impl_0函数的源码定义:

请添加图片描述

struct __main_block_impl_0包含以下核心成员:

成员 类型/含义
impl struct __block_impl类型,Block 的核心实现结构体(封装函数指针、类型标识、标志位等)。
Desc struct __main_block_desc_0*类型,指向 Block 描述符(存储元数据,如复制/销毁函数)。
A int类型,捕获的整型变量(值捕获)。
B __Block_byref_B_0*类型,指向引用捕获的可变对象的辅助结构体(引用捕获)。
构造函数 初始化各成员,绑定 Block 的执行逻辑、捕获变量和元数据。

然后我们顺藤摸瓜找到了Block 的核心实现结构体struct __block_impl函数的源码:请添加图片描述

由上述过程,我们可以知道,Block的底层本质是一个结构体,内部封装了一些关键信息:

函数指针:指向Block对应的可执行代码

捕获的变量:Block执行时需要访问的外部变量()

执行上下文:包括 Block 的引用计数、所属的类(用于调试)等元数据

有一张图非常好地说明了block的底层调用和逻辑:

请添加图片描述

​ ——图片来自博客【iOS】Block底层分析@zhngxvy

Block的执行流程

Block 的执行分为​​创建​​、​​存储​​、​​传递​​和​​调用​​四个阶段,核心是​​函数指针的调用​​和​​捕获变量的管理​​。

Block的创建与存储

创建:通过 ^语法定义 Block 时,编译器会生成一个 struct Block_layout结构体实例,并将代码块编译为对应的机器指令(存储在invoke函数指针中)。

存储位置:Block 初始存储在栈上(栈 Block),但以下场景会触发 Block 被复制到堆上(堆 Block):

  • Block 被赋值给一个强引用的变量(如 __strong修饰的属性或局部变量)。
  • Block 被作为参数传递给一个异步函数(如 dispatch_async)。
  • Block 被显式复制(调用 Block_copy())。

Block的传递与调用

Block 可以像对象一样被传递(如作为方法参数、存储在集合中),其调用的本质是执行 invoke函数指针,并传递参数。

示例(Block 作为方法参数):

// 定义一个接受 Block 的方法
- (void)executeBlock:(void (^)(void))block {
    NSLog(@"准备执行 Block...");
    block(); // 调用 Block(执行 invoke 函数)
    NSLog(@"Block 执行完成");
}

// 使用
[self executeBlock:^{
    NSLog(@"自定义 Block 逻辑");
}];

执行流程

  1. 定义 Block 时,编译器生成栈上的 struct Block_layout
  2. 将 Block 作为参数传递给 executeBlock:方法时,Block 被复制到堆上(因方法参数需要强引用)。
  3. 方法内部调用 block()时,触发invoke函数指针,执行 Block 的代码逻辑。

Block的捕获机制

Block 可以捕获外部作用域的变量(如局部变量、实例变量),捕获行为分为两种:值捕获和指针捕获

捕获局部变量

auto:自动变量,离开作用域就自动销毁,只存在于局部变量(没有特别关键字修饰,一般默认为auto)
static:静态局部变量

我们先来看以下代码:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int A = 18;//局部变量(自动变量)
        static int B = 52;//静态局部变量
        void(^jubuBlock)(void) = ^{
            NSLog(@"jubuBlock - A:%d - B:%d", A, B);
        };
      
        jubuBlock();
        NSLog(@"输出1 - A:%d B:%d", A, B);
      
        A = 20;
        B = 100;
      
        jubuBlock();
        NSLog(@"输出2 - A:%d B:%d", A, B);
    }
    return 0;
}

输出结果如下:

请添加图片描述

由此,我们可以发现,当我们在block函数外部修改局部变量时,block函数内部的自动变量不会受影响,而静态局部变量会跟着被修改。这是为什么呢?

为了探寻这一原因,我们将上述代码文件转化为cpp文件,我们可以发现__main_block_impl_0结构体定义如下:

请添加图片描述

我们可以得到以下结论:

  • A:捕获的自动变量(按值复制)。
  • B:捕获的静态局部变量(按指针复制,存储其内存地址)。

这就可以解释刚刚的问题:为什么在block函数外部修改自动变量,内部的变量值不会产生改变,而修改静态变量就会变?到这里,block捕获局部变量的本质就很明显了:

自动变量:按值捕获(复制其当前值到block内部)。即使外部变量后续被修改,block内部使用的仍是捕获时的副本。

静态局部变量:按指针捕获(存储其内存地址)。外部变量修改时,block通过指针访问的是最新值。

捕获全局变量

首先,有以下代码:

#import <Foundation/Foundation.h>

int A = 18;//全局变量
static int B = 52;//静态全局变量

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^quanjuBlock)(void) = ^{
            NSLog(@"quanjuBlock - A:%d - B:%d", A, B);
        };
        quanjuBlock();
        NSLog(@"输出1 - A:%d B:%d", A, B);
        A = 20;
        B = 100;
        quanjuBlock();
        NSLog(@"输出2 - A:%d B:%d", A, B);
    }
    return 0;
}

输出结果:请添加图片描述

我们再来看这部分源码:

请添加图片描述

我们发现,这部分的源码跟上面局部变量有点出入,在全局变量一直在__main_block_impl_0结构体定义中并没有我们声明的全局变量A和B,即全局、全局静态变量并没有出现在我们的Block实现结构体中,说明二者无法被捕获。而全局变量存储在内存中,打印的一直是最新的值。

小结

自动变量(局部变量) 静态局部变量 全局变量
存储区域 栈(Stack) 全局数据区(Data Segment) 全局数据区(Data Segment)
生命周期 随作用域结束(如函数返回)销毁 程序启动时创建,程序结束时销毁 程序启动时创建,程序结束时销毁
默认捕获机制 按值拷贝(复制当前值到block内部) 按指针拷贝(存储变量内存地址) 按指针拷贝(存储变量内存地址)
block内访问形式 直接使用拷贝后的副本(独立于原变量) 通过指针解引用访问原变量 通过指针解引用访问原变量
外部修改的影响 不影响block内部的副本(block捕获的是历史值) 影响block内部(指针指向的原变量被修改) 影响block内部(指针指向的原变量被修改)
是否支持修改捕获值 默认不可直接修改(需__block修饰) 可直接修改(通过指针操作原变量) 可直接修改(通过指针操作原变量)

产生这种差异的原因

auto和static:因为作用域的问题,自动变量的内存随时可能被销毁,所以要捕获就赶紧把它的值拿进来,防止调用的时候访问不到。

静态局部变量:存储在全局数据区,生命周期长于block,因此block捕获其指针后,即使原变量所在作用域销毁(如函数返回),仍可通过指针访问最新值。

注意:尽管静态局部变量的生命周期很长,但其**作用域(可访问范围)**仅限于声明它的函数内部:

  • 函数外部无法直接访问该变量(即使通过指针或引用)。
  • 不同函数中声明的同名静态局部变量相互独立(因为各自存储在全局数据区的不同位置)。

全局变量:在Block中访问局部变量相当于是跨函数访问,要先将变量存储在Block里(捕获),使用的时候再从Block中取出,而全局变量是直接访问。

tips:

自动变量按值捕获是安全的(避免悬垂指针),但可能增加内存拷贝开销(大对象需注意)。

静态/全局变量按指针捕获更高效(无拷贝),但需注意多线程并发修改时的线程安全问题。

Block的类型

上述说到Block含有isa指针,而OC对象的isa指针指向它的类型,那么Block的类型都有哪些呢?

首先有以下代码:

void (^block)(void) = ^{
    NSLog(@"hello world!");
};
NSLog(@"%@ %@", block, [block class]);
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block  class] superclass] superclass]);

运行后结果如下:
请添加图片描述
我们可以发现这个block的类型是NSGlobalBlock,其父类是NSBlock,根父类是NSObject,这也能说明Block是一个OC对象。

除了NSGlobalBlock,Block还有哪些类型,我们先来看如下代码:

        void (^block1)(void) = ^{
            NSLog(@"hello world!");
        };
        NSLog(@"%@ %@", block1, [block1 class]);
        
        int A = 3;
        void (^block2)(void) = ^{
            NSLog(@"%d", A);
        };
        NSLog(@"%@ %@", block2, [block2 class]);
        
        NSString *string = @"hello world!";
        void (^block3)(void) = ^{
            NSString *stringCopy = [string copy];
        };
        NSLog(@"%@ %@", block3, [block3 class]);

当我们在ARC环境下运行代码后,有如下结果:
请添加图片描述

随后,我们在项目的Bulid Settings中关闭ARC:

请添加图片描述

随即得到如下结果:

请添加图片描述

我们可以发现,在MRC环境下,若block不捕获任何外部变量,且未被复制,则其类型为__NSGlobalBlock__;若block捕获外部变量,但未被复制,则其类型为__NSStackBlock__;若block被复制,则其类型为__NSMallocBlock__

但如果是在ARC环境下,即使不显式调用copy也会是__NSMallocBlock__类型,这是因为在ARC下,编译器会自动插入内存管理代码(如retainreleasecopy),核心目的是确保block在需要跨作用域存活时保持有效,确保对象(包括block)在其生命周期内被正确管理

所以,我们可以做出如下总结:

存储位置 触发条件 生命周期
栈上 block未被复制(仅在局部作用域使用,未被赋值给id类型变量或作为参数传递) 离开作用域时自动销毁(无法跨作用域使用)
堆上 block被复制(显式调用copy,或隐式被id类型变量持有、作为方法参数传递等)。 由引用计数管理,直到release计数归零后销毁。
全局区 block不捕获任何外部变量,且未被复制。 程序启动到结束时一直存在(全局唯一)。

__block修饰符

在我们前面的学习中,我们知道在block函数外部修改自动变量,函数内部捕获的值不会受到影响,那如果我们想修改在block中捕获的自动了变量的值怎么办?

首先,我们肯定想到说,想修改?那我直接在函数内部进行修改行不行!我们试一下:

请添加图片描述

我们会发现,在block函数内部直接修改自动变量会发生报错:Variable is not assignable (missing __block type specifier),翻译过来就是:变量未分配(丢失 __block 类型限定符)。

这是因为自动变量的默认捕获机制:在OC中,block对自动变量的默认捕获方式是按值复制Copy by Value):

  • 当block定义时,会复制自动变量的当前值到block内部的副本中。
  • block内部访问该变量时,实际访问的是副本,而非原变量
  • 因此,若在block内部尝试修改该变量(如赋值操作),编译器会报错:因为修改的是副本,原变量无法被block直接修改,这是不允许的。

那么我们真的没有办法修改自动变量了吗😭有的兄弟有的!

如果我们需要在block内部修改自动变量,需用__block修饰符声明该变量。好的,我们现在进行实际操作试试😋:

请添加图片描述

OK!对自由变量A加了__block修饰后就可以在block函数内部进行修改了✅

嘶~这个__block到底有什么魔力?下面我们来探索一下。

先说结论:__block的作用是将自动变量转换为块级变量(Block Variable),使其被block和原作用域共享同一实例。

__block变量的包装结构体

现在我们来看看__block这部分的cpp源码:

请添加图片描述
我们发现,被__block修饰的自动变量A在源码中,编译器生成了一个包装结构体__Block_byref_A_0,用于存储被修饰变量的值及其元数据。

其成员含义:

成员 类型 说明
__isa void* 指向类对象的指针(通常为_NSConcreteStackBlock或堆上的类,初始可能为NULL)。
__forwarding __Block_byref_A_0* 转发指针,指向变量的实际存储位置(栈或堆)。当block被拷贝到堆时,此指针会指向堆中的副本。
__flags int 标志位(如是否被拷贝、是否需要释放等)。
__size int 结构体的大小(用于内存管理)。
A int 被__block修饰的原始变量

通过此结构体,block和原作用域的变量共享同一内存空间,实现“跨作用域修改变量”的能力。

block的实例结构体

block的实例结构体( __main_block_impl_0)源码如下:

请添加图片描述

block的实例通过此结构体表示,包含block的执行逻辑、描述信息及对__block变量的引用:

结构体成员 类型 说明
impl struct __block_impl block的基类结构体,包含isa(类型标识)、Flags(标志位)、FuncPtr(执行函数指针)
Desc struct __main_block_desc_0* 指向block描述符的指针,记录block的内存布局和回调函数(如拷贝、销毁)
A __Block_byref_A_0* 指向__block变量包装结构体的指针(通过引用捕获,而非值拷贝)
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_A_0 *_A, int flags=0) 
  : A(_A->__forwarding) {  // 初始化时,A指向包装结构体的转发指针
    impl.isa = &_NSConcreteStackBlock;  // 标记为栈上block
    impl.Flags = flags;                 // 设置标志位(如是否需要拷贝)
    impl.FuncPtr = fp;                  // 绑定执行函数(如__main_block_func_0)
    Desc = desc;                        // 绑定描述符
}

A(_A->__forwarding)表示block实例的A成员直接指向__Block_byref_A_0结构体的__forwarding指针。这确保了无论block是否被拷贝到堆,A始终指向变量的实际存储位置(栈或堆)。

block的执行逻辑

block的实际执行体(__main_block_func_0:),其源码如下:

通过__cself(block实例自身)访问捕获的变量。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_A_0 *A = __cself->A;  // 获取包装结构体指针

  (A->__forwarding->A) = 19;  // 修改被包装的变量A的值(通过转发指针)
  NSLog((NSString *)&__NSConstantStringImpl__... , (A->__forwarding->A));  // 打印修改后的值
}

通过A->__forwarding->A访问被__block修饰的变量A。__forwarding指针确保即使block被拷贝到堆,仍能正确访问变量的实际存储位置(栈或堆副本)。

Block循环引用

Block的本质是一个对象(继承自NSObject),其内存管理遵循ARC规则。当Block捕获外部对象(如self)时:

  • 默认情况下,Block会强引用捕获的对象(对对象类型变量按引用捕获,基本类型按值捕获)。

  • 如果外部对象(如self)本身强引用该Block(例如将Block作为self的属性),会形成强引用闭环:

    self → Block(强引用) → self(强引用)

    此时两者的引用计数均无法归零,导致内存泄漏。

造成的原因

Block作为对象的属性,且内部捕获self

当对象的属性是Block,且Block内部访问了self(如调用self的方法或属性),同时对象强引用该Block时,形成循环引用。

// ViewController.h
@interface ViewController : UIViewController
@property (nonatomic, strong) void (^myBlock)(void); // Block属性(强引用)
@end

// ViewController.m
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 1. 创建Block,内部捕获self(强引用)
    self.myBlock = ^{ 
        NSLog(@"Self: %@", self);  // 捕获self(强引用)
    };
    
    // 2. self强引用myBlock(属性是strong)
    // 此时形成循环引用:self → myBlock → self
}

@end

循环路径:

self(ViewController实例)通过strong属性强引用myBlock→ myBlock通过默认的强引用捕获self→ 两者引用计数均无法归零。

Block被嵌套对象持有,且捕获外层self

当Block被另一个对象(如manager)持有,而self又持有该manager,同时Block内部捕获self时,也可能形成循环。

// Manager.h
@interface Manager : NSObject
@property (nonatomic, copy) void (^block)(void);
@end

// ViewController.m
@implementation ViewController {
    Manager *_manager;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _manager = [[Manager alloc] init];
    
    // 1. Manager持有block(copy后为强引用)
    _manager.block = ^{ 
        NSLog(@"ViewController: %@", self);  // Block捕获self(强引用)
    };
    
    // 2. self持有_manager(强引用)
    // 循环路径:self → _manager → block → self
}

@end

解决方法

  解决循环引用的核心是打破强引用闭环,通常通过==弱引用(__weak__unsafe_unretained)==弱化Block对self的引用

使用__weak修饰self

在Block内部通过__weak修饰的weakSelf引用self,避免Block强引用self。

// ViewController.m
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 1. 定义弱引用指向self(打破循环)
    __weak typeof(self) weakSelf = self;
    
    // 2. Block捕获weakSelf(弱引用)
    self.myBlock = ^{ 
        // 使用weakSelf替代self(可能为nil)
        NSLog(@"Self: %@", weakSelf); 
    };
    
    // 3. self仍强引用myBlock,但myBlock弱引用self → 无闭环
}

@end

__weak typeof(self) weakSelf = self创建了一个弱引用weakSelf,其引用计数不增加,Block捕获weakSelf(弱引用),因此self的引用计数不会因Block的持有而增加。

循环路径被打破:self → myBlock → weakSelf(弱引用,不增加计数)。

使用__unsafe_unretained(不推荐)

__unsafe_unretained__weak类似,但不会自动将失效的指针置为nil,可能导致野指针(访问已释放的对象)。

__unsafe_unretained typeof(self) unsafeSelf = self;
self.myBlock = ^{ 
    NSLog(@"Self: %@", unsafeSelf);  // 若self已释放,unsafeSelf可能为野指针
};

__unsafe_unretained仅在以下场景使用:目标对象生命周期明确短于Block(无需置nil)或无法使用__weak(如兼容旧版本iOS)。

在Block执行完毕前确保self存活

若Block需要长时间执行(如异步任务),可通过临时强引用确保self在Block执行期间不被释放,执行完毕后自动释放。

__weak typeof(self) weakSelf = self;
self.myBlock = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;  // 临时强引用
    if (strongSelf) {
        [strongSelf doSomething];  // 在Block执行期间,strongSelf保持self存活
    }
};  // strongSelf在此处销毁,不影响self的引用计数

__strong typeof(weakSelf) strongSelf = weakSelf在Block内部创建一个临时强引用strongSelf,若weakSelf未失效(self仍存活),strongSelf会强引用self,确保Block执行期间self不被释放,Block执行完毕后,strongSelf销毁,self的引用计数恢复正常。

小结

所以,我们可以总结:Block循环引用的核心是强引用闭环,解决方案的关键是弱化其中一个环节的引用。最常用的方法是使用__weak修饰self,在Block内部通过弱引用访问self,打破循环。同时需注意:

  • 避免在Block内部直接强引用self(默认行为)。
  • 若需在Block执行期间确保self存活,可结合临时强引用(__strong)。
  • __unsafe_unretained需谨慎使用,防止野指针崩溃。

总结

  Block是Objective-C中强大的闭包工具,在iOS开发中十分重要,常用于处理异步操作、回调、集合操作等,核心能力是捕获并保存环境状态,支持灵活的异步编程和回调逻辑。理解其存储位置(栈/堆/全局)变量捕获规则(值/指针/引用)内存管理(ARC/MRC)循环引用解决方案是掌握Block的关键。


网站公告

今日签到

点亮在社区的每一天
去签到