iOS 底层原理 block

block的使用

基本使用

objectivec 复制代码
    ^{
        NSLog(@"This is a block");
    }();// 使用括号表示调用Block的方法
    
    void (^block1)(void) = ^ {
        NSLog(@"This is another block");
    };
    block1();
    
    int age = 100;
    void (^block2)(void) = ^ {
        NSLog(@"This is block with age %d", age);
    };
    block2();
    
    struct __block_impl {
        void *isa;          // isa 指针,表明 Block 是一个 OC 对象
        int Flags;          // 标志位
        int Reserved;       // 保留字段
        void (*FuncPtr)(void *);      // 函数指针,指向 Block 内部的具体执行代码
    };
    // 1. 将 block2 强转为底层基础结构体指针
    struct __block_impl *implPtr = (__bridge struct __block_impl *)block2;

    // 2. 通过函数指针调用,并将 block2 自身作为参数传入
    // 底层等价于:block2->FuncPtr(block2);
    (implPtr->FuncPtr)(implPtr);
    
    
    NSLog(@"%@", [block1 class]);
    NSLog(@"%@", [block2 class]);

Block 可以捕获其所在作用域的外部变量。默认情况下,局部变量以值拷贝(只读) 的方式被捕获;如果需要修改外部局部变量,必须使用 __block 修饰符,此时 Block 捕获的是变量的指针(地址)

Block 会对其捕获的 OC 对象进行强引用(Strong Reference) 。如果对象本身也强持有了这个 Block(例如作为属性),就会形成 self -> block -> self 的闭环,导致内存泄漏。

标准解法(Weak-Strong Dance)

ini 复制代码
__weak typeof(self) weakSelf = self;
self.completionBlock = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) {
        [strongSelf doSomething];
    }
};

底层原理

Block 并非单纯的函数指针,在底层,它是一个继承了 NSObject 的 OC 对象。编译器(Clang)会将 Block 转换为一个特殊的 C 语言结构体。

arduino 复制代码
struct __block_impl {
    void *isa;          // isa 指针,表明 Block 是一个 OC 对象
    int Flags;          // 标志位
    int Reserved;       // 保留字段
    void *FuncPtr;      // 函数指针,指向 Block 内部的具体执行代码
};

// 编译器自动生成的自定义 Block 结构体
struct __main_block_impl_0 {
    struct __block_impl impl;            // 内嵌基础结构体
    struct __main_block_desc_0* Desc;    // 描述信息(包含 copy/destroy 函数指针)
    int capturedVar;                     // 捕获的外部变量
};
Block 的三种类型

根据存储位置和 isa 指针的指向,Block 分为三种类型:

  • __NSGlobalBlock__(全局 Block) :没有捕获任何外部变量。它存储在程序的数据段,生命周期与程序一致,无需内存管理。
  • __NSStackBlock__(栈 Block) :捕获了外部变量,且仅在当前作用域内使用。它存储在栈区,作用域结束后会被销毁。
  • __NSMallocBlock__(堆 Block) :捕获了外部变量,并且被赋值给了强引用属性、作为返回值传出,或被传入 GCD 等异步队列。此时,Runtime 会将其从栈区 Copy 到堆区,交由 ARC 进行引用计数管理。
scss 复制代码
    ^{
        NSLog(@"This is a block");
    }();// 使用括号表示调用Block的方法
    
    void (^block1)(void) = ^ {
        NSLog(@"This is another block");
    };
    block1();
    
    int age = 100;
    void (^block2)(void) = ^ {
        NSLog(@"This is block with age %d", age);
    };
    block2();
    
    __weak void (^block3)(void) = ^ {
        NSLog(@"This is weak block with age %d", age);
    };
    block3();
    
    
    
    NSLog(@"%@", [block1 class]);//__NSGlobalBlock__
    NSLog(@"%@", [block2 class]);//__NSMallocBlock__
    NSLog(@"%@", [block3 class]);//__NSStackBlock__
变量捕获机制
  • 普通局部变量:Block 在创建时,会将变量的值复制一份到结构体内部。因此,Block 内部修改不会影响外部,外部修改也不会影响 Block 内部。
  • __block 修饰的变量 :编译器会引入一个名为 __Block_byref_xxx 的新结构体,将变量包装在其中。Block 结构和外部作用域都持有这个结构体的指针,从而实现了跨作用域的共享与修改513。
  • OC 对象 :当栈 Block 被 Copy 到堆区时,底层会自动对捕获的 OC 对象执行 retain 操作(强引用);当堆 Block 被销毁时,会执行 release 操作。这就是产生循环引用的根本原因。

特别强调:在clang3.9之前,捕获局部的static会引用指针,新版本会将局部的static强行提升到全局的static

objectivec 复制代码
int global_a = 1000;
static int global_b = 2000;

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    struct Block_descriptor {
        unsigned long reserved;
        unsigned long size;
        // 还有可选的 copy/dispose 函数指针,ARC下会有
    };

    struct Block_impl {
        void *isa;
        int flags;
        int reserved;
        void *invoke;
        struct Block_descriptor *descriptor; // ✅ descriptor 在结构体内部!
    };
    
    // 捕获局部变量
    int age = 100;
    void (^block1)(void) = ^ {
        NSLog(@"This is block with age %d", age);
    };
    block1();
    
    struct Block1_impl {
        struct Block_impl impl;     // 函数指针,指向 Block 内部的具体执行代码
        
        //这里捕获了值
        int age;
    };
    struct Block1_impl *block1_pointer =(__bridge  struct Block1_impl *) block1;
    NSLog(@"block1的值是%d",block1_pointer->age);
    
    // 捕获局部static变量
    static int age1 = 100;
    void (^block2)(void) = ^ {
        NSLog(@"This is block with age1 %d", age1);
    };
    age1 = 200;
    
    struct Block2_impl {
        struct Block_impl impl;     // 函数指针,指向 Block 内部的具体执行代码
        int *age1;
    };
    struct Block2_impl *block2_pointer =(__bridge  struct Block2_impl *) block2;
    NSLog(@"block1的值是%ld",block1_pointer->impl.descriptor->size); // 36
    
    // 因为block1捕获了age1多占了4字节,这里少了4个字节,说明没有捕获任何变量
    NSLog(@"block2的值是%ld",block2_pointer->impl.descriptor->size); // 32
    
    
    // 全局
    
    void (^block3)(void) = ^ {
        NSLog(@"This is block with global data a %d   b %d", global_a, global_b);
    };
    struct Block2_impl *block3_pointer =(__bridge  struct Block2_impl *) block3;
    NSLog(@"block3的值是%ld",block3_pointer->impl.descriptor->size); // 32
   
    
}

block与__forwarding

__block修饰的变量,在内部会有一个forwarding。当我们在代码中使用 __block 修饰一个变量时,编译器会将其包装成一个结构体对象(例如 __Block_byref_age_0)。

  • 初始状态 :这个结构体默认是创建在栈区 的。此时,结构体内部的 __forwarding 指针指向它自己
  • 拷贝状态 :当 Block 被 copy 到堆区 时,这个 __block 结构体也会被 copy 一份到堆上。此时,

block和循环引用

1. 循环引用的本质

循环引用的本质是 "相互强引用" 。当一个对象(如 ViewController)强引用了一个 Block 属性,同时该 Block 内部又直接使用了 self(Block 会强捕获 self),就会形成 self → Block → self 的闭环。

由于 ARC(自动引用计数)机制下,只要引用计数大于 0 对象就不会被释放,这种闭环会导致两者的引用计数永远无法归零,最终造成内存泄漏(即 dealloc 方法永远不会被调用)。

2. 常见的循环引用场景

  • Block 作为属性 :在 ViewController 中定义一个 strongcopy 修饰的 Block 属性,且在 Block 内部直接访问 self 的属性或方法。
  • NSTimer 与 GCD 回调 :使用 NSTimerdispatch_after 等 API 时,如果 Block 捕获了 self,且定时器未被正确销毁(如未调用 invalidate),也会导致循环引用14。
  • Delegate 与父子对象 :虽然不全是 Block 的问题,但在对象层次结构中(如父对象强引用子对象,子对象又强引用父对象),或者 delegate 属性错误使用了 strong 修饰,同样会引发循环引用3。

3. 核心解决方案:打破强引用闭环

要解决 Block 引起的循环引用,最常用的手段是使用弱引用(__weak)来打破闭环。以下是三种常见的实战方案:

方案一:使用 __weak 修饰 self(最常用)

在 Block 外部定义一个 __weak 修饰的 weakSelf,Block 内部使用它。弱引用不会增加对象的引用计数,当对象被释放时,weakSelf 会自动置为 nil

如果 Block 内部包含异步操作(如网络请求、延迟执行),为了防止 weakSelf 在异步执行期间被意外释放,还需要在 Block 内部使用 __strong 将其临时提升为强引用(即经典的 weak-strong dance

ini 复制代码
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
    // 异步操作前,强引用 weakSelf,避免 self 在 block 执行中被释放
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (!strongSelf) return; // 防止 self 已释放
    [strongSelf doSomething]; 
};
方案二:使用 __block 修饰并手动置 nil

在 ARC 环境下,如果用 __block 修饰 self,Block 捕获该变量时会变为强引用。此时必须在 Block 执行完毕后,手动将该变量置为 nil 来打破循环。

ini 复制代码
__block typeof(self) blockSelf = self;
self.myBlock = ^{
    [blockSelf doSomething];
    blockSelf = nil; // 手动置 nil,打破循环引用
};
self.myBlock(); // 必须确保 Block 被执行,否则依然会内存泄漏
方案三:将 self 作为 Block 的参数传递

self 作为参数传入 Block,Block 内部通过参数访问对象,从而避免捕获外部的 self。这种方式虽然能避免循环引用,但会破坏 Block 调用的简洁性,通常仅在简单场景下使用。

4. 最佳实践与排查工具

  • 规范 Delegate 声明 :所有的 delegate 属性必须声明为 weak,避免父子对象相互强引用3。
  • 善用调试工具 :在 Xcode 中可以使用 Memory Graph Debugger 可视化检查对象之间的引用关系,或者运行 Instruments 的 Leaks 工具进行内存泄漏测试。
  • ARC 的局限性:虽然 ARC 极大简化了内存管理,但它无法自动处理循环引用,开发者仍需主动管理引用关系。
相关推荐
2501_915106323 小时前
Swift 4开发环境设置教程:xCode安装与Playground入门
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
9765033356 小时前
iOS 上架/审核 4.3a全面解读-最新
flutter·ios·objective-c·uniapp
用户21816970493021 小时前
iOS 底层原理 Category load initialize 关联对象
objective-c
秋雨梧桐叶落莳4 天前
【iOS】从源码深入理解RunLoop机制
macos·ios·objective-c·cocoa·cocoapods·uikit
2501_915106324 天前
第一次开发 iPhone App,可能碰到的问题,解决办法
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
sakiko_7 天前
OC基础语法(与Swift对比)-1
开发语言·ios·objective-c·swift
2501_916008897 天前
Flutter 开发 iOS,项目打包成 IPA,命令与免 Xcode两种方法
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
无定义_10 天前
萌新联赛(六)
macos·objective-c·cocoa
FairGuard手游加固11 天前
iOS 游戏加固技术解析:代码混淆、资源加密、内存保护与重签名检测
安全·游戏·macos·unity·ios·objective-c