当前位置: 首页 > news >正文

「OC」小白书读书笔记——Block的相关知识(下)

「OC」小白书读书笔记——Block的相关知识(下)

引入

前面我们说到block当中捕获变量的底层原理,以及block对应的各种不同的类,现在开始讲一下block在大括号之中生成,却能够超出变量存在域可存在原因,我们看一下下面的例子

#import <Foundation/Foundation.h>

typedef int (^MultiplierBlock)(int);

// 定义一个函数,用于创建一个 block,并捕获局部变量 factor
MultiplierBlock createMultiplierBlock(int factor) {
    // 这里的 factor 是局部变量
    // block 捕获了 factor 的值,并在闭包中保存了一份拷贝
    MultiplierBlock block = ^(int number) {
        return factor * number;
    };
    
    // 返回前,将 block 从栈复制到堆上,确保它在函数返回后依然有效
    return [block copy];
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 调用 createMultiplierBlock 创建一个 block,捕获的 factor 值为 5
        MultiplierBlock multiplier = createMultiplierBlock(5);
        
        // 在 main 函数中调用 multiplier,即使 createMultiplierBlock 中的 factor 早已不复存在
        int result = multiplier(4);
        NSLog(@"Result: %d", result); // 输出结果为 20 (5 * 4)
    }
    return 0;
}

Block 的存储转换

  • 默认情况下,block 在函数中创建时位于栈上,生命周期与局部变量类似。但在本例中,我们调用了 [block copy],将其复制到堆上。
  • 这样做保证了 block 能够在函数返回后继续存在,因为堆内存的生命周期不依赖于当前函数的作用域。

image-20250221105835990

image-20250221105900036

如同上图所示这个被copy到堆上的block就是_NSConcreteMallocBlock类,一般来说,在需要持有 block 时,只需要执行一次 copy 操作就足够了,多次调用 copy 不会导致重复复制,只是增加引用计数。

__block修饰符的作用

我们知道常规的局部变量,只能被block捕获其初始值,在修改之后并没办法对其捕获的值进行更新。我们故然可以使用静态局部变量来避免这个问题,不过因为静态局部变量在程序运行过程中是不会被释放的,所以还是要尽量少用。那还有什么别的方法来实现这个需求呢?这就是我们要讲的__block关键字。

若在1个Block中使用__block变量,则当该Block从栈复制到堆时,使用的所有__block变量也必定配置在栈上。这些__block变量也全部被从栈复制到堆。此时,Block持有__block变量。即使在该Block已复制到堆的情形下,复制Block也对所使用的__block变量没有任何影响。

image-20250221224757538

在多个Block中使用__block变量时,由于最初所有的Block都会被配置在栈上,所以__block变量也会配置在栈上。当任何一个Block从栈复制到堆时,__block变量会一并从栈复制到堆,并且被该Block持有。当其余的Block从栈复制到堆时,被复制的Block会持有__block变量,并增加__block变量的引用计数,如图所示。

image-20250222102013512

若是在堆上废弃那么对应的block也需要被释放,内容如下:

image-20250222110425108

__block不管是修饰基础数据类型还是修饰对象数据类型,底层都是将它包装成一个对象(我这里取个名字叫__blockObj),然后block结构体中有个指针指向__blockObj。既然是一个对象,那block内部如何对它进行内存管理呢?

当block在栈上时,block内部并不会对__blockObj产生强引用。

当block调用copy函数从栈拷贝到堆中时,它同时会将__blockObj也拷贝到堆上,并对__blockObj产生强引用。

在 Objective-C 中,_Block_object_assign_Block_object_dispose 是与 block 内存管理相关的内部函数。它们用于处理 block 在运行时的内存管理,尤其是在涉及 block 捕获和块内对象引用时。

_Block_object_assign

作用
_Block_object_assign 是用于将一个对象赋值给一个 block 中的局部变量(例如捕获的 __block 变量)。当 block 捕获一个对象时,它需要确保对这个对象的引用在 block 执行期间是有效的。通过 _Block_object_assign,block 会对捕获的对象进行管理,确保对象的生命周期与 block 的生命周期保持一致。

主要用途

  • 赋值与引用计数管理:它会增加捕获对象的引用计数,以确保对象在 block 执行期间不会被提前释放。这样,block 内部对捕获对象的引用不会导致对象意外被销毁。
  • 引用传递:如果对象是 __block 变量,_Block_object_assign 确保 block 在内部维护正确的对象引用,而不会导致循环引用。

_Block_object_dispose

作用
_Block_object_dispose 是用来释放 block 内部捕获的对象引用的函数。它会在 block 被销毁时,正确地减少捕获对象的引用计数,确保不会造成内存泄漏。

主要用途

  • 释放捕获对象:当 block 执行完毕并且被销毁时,_Block_object_dispose 会减少捕获对象的引用计数,避免内存泄漏。
  • 清理操作:在 block 销毁时进行清理,确保在 block 使用的对象不再被引用时,能正确释放。

image-20250223002507169

__forwarding指针

block int val = 10;
void (^blk) (void) = ^{val = 1; }; 

在我们使用__block修饰符进行修饰变量的时候,我们将OC代码转化为C语言代码,可以看到代码量剧增,其中我们用__block修饰的变量名成功就就会变成一个结构体类型的变量,__Block_byref_val_0 结构体类型的自动变量,即栈上生成的__Block_byref_val_0结构体实例。这个结构声明如下:

struct __Block_byref_val_0 {
    void *isa;                                  // 标识符,一般不直接使用
    struct __Block_byref_val_0 *forwarding;       // 转发指针,始终指向最新的存储位置
    int flags;                                  // 标志位,用于记录状态
    int size;                                   // 结构体的大小
    int val;                                    // 被 __block 修饰的变量值
};

我们来讲一下这个__forwarding指针的作用:当 block 从栈复制到堆上后,局部 __block 变量也必须从栈复制到堆上,否则在 block 执行时会访问无效的内存。__forwarding 指针的更新正是实现这一过程的关键,它确保了 block 内所有对 __block 变量的引用都指向堆上的有效拷贝,而不会停留在即将销毁的栈内存中。

image-20250222120257273

这样,无论 block 在栈上还是堆上执行,所有对该 __block 变量的访问都通过 __forwarding 指针进行,确保访问的是最新、有效的内存区域。

循环引用

接下来将一个我认为比较重要的一个点就是关于block之中产生的循环引用问题

 self.str = @"aa";
 self.block = ^(void) {
        NSLog(@"%@",self.str);
    };
 self.block();

运行以上代码,我们可以看到编译器出现以下警告

image-20250222130228137

这条警告提示你在 block 中对 self 的捕获是强引用(strong capture),这可能会导致循环引用(retain cycle)。

以下是解决循环引用的几个方法

  • 1.weak-strong-dance(最常用的方法)
  • 2.__block修饰对象,同时置nil

强弱共舞

为解决这个问题,一个很简单也最常用的方法,就是使用weak关键词对self进行修饰

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

- (void)viewDidLoad {
    [super viewDidLoad];  
    self.name = @"man";
    __weak typeof(self) weakSelf = self;
    self.block = ^(void){
        NSLog(@"%@",weakSelf.name);
    };
    self.block();
}

但如果在block的基础上还嵌套了一个block,那很有可能在执行block中的嵌套时,weak修饰的对象已经被置为nil。这时候我们如何去解决这个问题呢?我们可以使用 __strong 在内部 block 中对弱引用进行一次临时提升,能确保在整个内部 block 执行期间,对象的引用计数保持在至少 1,从而保证对象不会被提前释放。以下例子:

// MyObject.h
#import <Foundation/Foundation.h>

@interface MyObject : NSObject
@property (nonatomic, copy) void (^block)(void);
@end

// MyObject.m
#import "MyObject.h"

@implementation MyObject

- (instancetype)init {
    self = [super init];
    if (self) {
        NSLog(@"MyObject init");
    }
    return self;
}

- (void)dealloc {
    NSLog(@"MyObject dealloc");
}

@end

// main.m
#import <Foundation/Foundation.h>
#import "MyObject.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 创建对象并设置 block
        MyObject *obj = [[MyObject alloc] init];
        __weak typeof(obj) weakObj = obj;
        obj.block = ^{
            NSLog(@"Inside block: %@", weakObj);
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                NSLog(@"Inside delayed block: %@", weakObj);
            });
        };
        // 执行 block,此时 obj 仍存在
        obj.block();
    }
    
    // 此时 obj 已经超出 autoreleasepool 的范围,应该被销毁
    // 为了观察延迟 block 的输出,我们让主运行循环等待一段时间
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
    return 0;
}

我们来看以上程序,在延时的block之中,我们并没有使用__strongweakObj进行再持有得到的以下结果

image-20250222172215405

现在修改一下代码:

#import <Foundation/Foundation.h>
#import "MyObject.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 创建对象并设置 block
        MyObject *obj = [[MyObject alloc] init];
        __weak typeof(obj) weakObj = obj;
        obj.block = ^{
            __strong typeof(obj) strongObj = weakObj;
            NSLog(@"Inside block: %@", strongObj);
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                NSLog(@"Inside delayed block: %@", strongObj);
            });
        };
        // 执行 block,此时 obj 仍存在
        obj.block();
    }
    
    // 此时 obj 已经超出 autoreleasepool 的范围,应该被销毁
    // 为了观察延迟 block 的输出,我们让主运行循环等待一段时间
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
    return 0;
}

image-20250222172326437

其中strongObj是一个临时变量,在block的作用域内,当block执行完就会释放strongObj,这种方式属于打破self对block的强引用,依赖于中间者模式,属于自动置为nil,也就是自动释放

__block修饰变量后置nil

#import <Foundation/Foundation.h>
#import "MyObject.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 创建对象并设置 block
        MyObject *obj = [[MyObject alloc] init];
        __block typeof(obj) blockObj = obj;
        obj.block = ^{
            NSLog(@"Inside block: %@", blockObj);
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                NSLog(@"Inside delayed block: %@", blockObj);
                blockObj = nil;
            });
        };
        // 执行 block,此时 obj 仍存在
        obj.block();
    }

    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
    return 0;
}

可以看到我们将对象赋值给一个__block修饰的变量,在block结束后对blockObj进行手动释放

参考文章

OC中block的底层实现原理

OC底层知识点之 - Block底层原理

相关文章:

  • 实现vlan间的通信
  • 解决单设备号双目摄像头调用难题:经验分享与总结
  • 融媒体中心智能语音识别系统设计与实现
  • 第2个小脚本:批量读取所有英文txt文章内容提取高频的单词
  • Matlab学习笔记五十:循环语句和条件语句的用法
  • 【微服务架构】SpringSecurity核心源码剖析+jwt+OAuth(七):SpringSecurity中的权限管理
  • 【HD-RK3576-PI】系统更新与恢复
  • Spring MVC 是如何将 @RequestMapping 注解映射到对应的 Handler 方法?
  • 【大英赛】大英赛准备笔记
  • MCP基础学习计划详细总结
  • Vue3项目中的前缀和
  • C++ ------ 智能指针
  • 2025年常见渗透测试面试题-webshell免杀思路(题目+回答)
  • 抓包神器,自研EtherCAT抓包工具
  • Next.js/Nuxt.js 服务端渲染优化
  • 1.1 初识AI
  • C语言进阶之字符函数和字符串函数
  • AcWing 5972. 科学记数法
  • 【游戏安全】强制交互类风险
  • Magnet 库的技术架构与核心机制解析
  • 严打金融黑灰产,今年来上海警方破获各类经济犯罪案件690余起
  • 马上评|安排见义勇为学生补考,善意与善意的双向奔赴
  • 白天气温超30℃的北京,晚间下起了冰雹
  • 观察|“双雄”格局下电池制造商如何生存:加码不同技术、抢滩新赛道
  • 习近平致电祝贺阿尔巴尼斯当选连任澳大利亚总理
  • 排污染黑海水后用沙土覆盖黑泥?汕尾环保部门:非欲盖弥彰