• iOS高级理论: Block介绍和使用


    一、Block概要

    1. Block的定义

    iOS 开发中,Block 是一种闭包(Closure)的概念,可以将一段代码块作为一个对象进行传递和存储,类似于函数指针。Block 可以捕获其定义时所在范围内的变量,并在需要的时候执行这段代码块。Block 的使用可以方便地实现回调、异步操作、事件处理等功能。

    2. 为什么要引入Block

    Block其本质也是对象,是一段代码块。iOS之所以引入Block,是因为使用Block可以精简代码,减少耦合。

    二、Block的用法

    2.1 作为变量使用
    int (^multiply)(int, int) = ^(int a, int b) {
        return a * b;
    };
    int result = multiply(3, 5); // 调用 Block
    
    • 1
    • 2
    • 3
    • 4
    2.2 作为方法参数
    - (void)performOperationWithBlock:(void (^)(void))block {
        block();
    }
    [self performOperationWithBlock:^{
        NSLog(@"执行 Block");
    }];
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    2.3 作为属性或实例变量
    @property (nonatomic, copy) void (^completionBlock)(void);
    self.completionBlock = ^{
        NSLog(@"完成操作");
    };
    self.completionBlock();
    
    • 1
    • 2
    • 3
    • 4
    • 5

    三、Block与外界变量

    在 iOS 开发中,Block 可以捕获其定义时所在范围内的外部变量,这种特性称为变量捕获(Variable Capture)。当 Block 内部引用外部变量时,Block 会自动捕获这些变量的值或引用,以便在 Block 执行时使用。在 Block 中使用外部变量可以实现数据共享和传递,但需要注意一些细节和注意事项。

    变量类型类型捕获到block内部访问方式
    局部变量auto可以值传递
    局部变量static可以指针传递
    全局变量不可以直接访问
    3.1 局部变量 auto

    局部变量 auto (自动变量) ,我们平时写的局部变量,默认就有 auto (自动变量,离开作用域就销毁)。比如:

    int age = 20; // 等价于 auto int age = 20;
    
    • 1
    3.2 局部变量 static

    static 修饰的局部变量,不会被销毁。由下面代码可以看得出来,Block 外部修改static 修饰的局部变量,依然能影响 Block 内部的值:

    static int height  = 30;
    int age = 20;
    void (^block)(void) =  ^{
         NSLog(@"age is %d height = %d",age,height);
    };
    age = 25;
    height = 35;
    block();
    
    // 执行结果
    age is 20 height = 35
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这是为什么会改变呢?
    因为 age 是直接值传递,height 传递的是 *height,也就是说直接把内存地址传进去进行修改了。大家可以根据上面讲解内容尝试编译成 cpp 文件,去了解详情。缺点是会永久存储,内存开销大。

    3.3 全局变量

    全局变量不能也无需捕获到 Block 内部,因为全局变量是存放在全局静态区的,直接访问就完事了。缺点也是内存开销大。

    3.4 在Block内如何修改Block外部变量

    在 Block 内部修改 Block 外部变量通常需要使用 __block 关键字来声明外部变量,以便在 Block 内部对外部变量进行修改。使用 __block 关键字可以使外部变量在 Block 内部变为可变,允许对其进行赋值操作。

    下面是一个示例代码,演示如何在 Block 内部修改外部变量:

    __block int externalVariable = 10;
    void (^block)(void) = ^{
        externalVariable = 20;
        NSLog(@"内部修改后的外部变量值:%d", externalVariable);
    };
    block(); // 输出:内部修改后的外部变量值:20
    NSLog(@"外部变量值:%d", externalVariable); // 输出:外部变量值:20
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在上面的示例中,通过使用 __block 关键字声明外部变量 externalVariable,使得在 Block 内部可以修改外部变量的值。当调用 Block 后,外部变量的值被成功修改,并且在 Block 外部也可以访问到修改后的值。

    需要注意的是,在使用 __block 关键字声明外部变量时,需要注意内存管理的问题,特别是在 ARC(Automatic Reference Counting)环墨下,避免出现循环引用或内存泄漏的情况。确保在适当的时候解除对 Block 的强引用,以避免出现循环引用问题。

    总的来说,使用 __block 关键字可以在 Block 内部修改外部变量,实现对外部变量的可变性,但需要注意内存管理和避免循环引用等问题,以确保代码的正确性和稳定性。

    四、Block的分类

    根据存储位置分类:

    • 栈上的 Block:默认情况下,Block 存储在栈上,当 Block 离开作用域时会被销毁。
    • 堆上的 Block:通过调用 copy 方法,可以将栈上的 Block 复制到堆上,以延长 Block 的生命周期。

    在 iOS 开发中,Block 可以存储在栈上或堆上,具有不同的生命周期和内存管理方式。下面详细介绍栈上的 Block 和堆上的 Block 的特点和区别:

    4.1 栈上的 Block
    1. 特点

      • 默认情况下,Block 存储在栈上。
      • 栈上的 Block 在定义它的作用域内有效,当作用域结束时,Block 会被销毁。
      • 栈上的 Block 不能跨作用域使用,一旦超出作用域范围,Block 就会失效。
    2. 内存管理

      • 栈上的 Block 不需要手动管理内存,由系统自动管理。
      • 当 Block 被复制到堆上时,系统会自动将其从栈上复制到堆上,以延长 Block 的生命周期。
    3. 注意事项

      • 将栈上的 Block 传递给异步执行的方法时,需要注意避免在 Block 执行过程中访问已经释放的栈上变量,可以通过将 Block 复制到堆上来避免此问题。
    4.2 堆上的 Block
    1. 特点

      • 通过调用 copy 方法,可以将栈上的 Block 复制到堆上。
      • 堆上的 Block 的生命周期可以延长,不会受到作用域的限制。
      • 堆上的 Block 可以在不同的作用域中传递和使用,不会因为作用域结束而失效。
    2. 内存管理

      • 堆上的 Block 需要手动管理内存,需要在不再需要使用 Block 时手动释放。
      • 在 ARC 环境下,系统会自动管理 Block 的内存,无需手动调用 copyrelease
    3. 注意事项

      • 当 Block 捕获外部变量时,如果 Block 存储在堆上,需要注意避免循环引用问题,可以使用 __weak 修饰符来避免循环引用。

    总的来说,栈上的 Block 在作用域内有效,适合用于临时的、局部的逻辑处理;而堆上的 Block 可以延长生命周期,适合在不同作用域中传递和复用。开发者在使用 Block 时,需要根据实际情况选择合适的存储位置,以确保内存管理和逻辑执行的正确性。

    4.3 区分栈上的 Block与堆上的 Block

    在 iOS 开发中,可以通过一些方法来区分 Block 是存储在栈上还是堆上的。下面列举了几种方法来区分栈上的 Block 和堆上的 Block:

    4.3.1. 使用 NSLog 打印 Block 的地址
    • 栈上的 Block:栈上的 Block 在作用域结束后会被销毁,所以可以通过在 Block 定义后立即打印 Block 的地址来查看 Block 的地址是否相同。
    • 堆上的 Block:堆上的 Block 在复制到堆上后会有不同的地址,可以通过在 Block 复制到堆上后打印 Block 的地址来查看是否有变化。
    4.3.2. 使用 __block 变量
    • 栈上的 Block:栈上的 Block 捕获 __block 变量时,变量的值会被 Block 捕获,但是在作用域结束后,__block 变量的值会被更新为 Block 执行时的值。
    • 堆上的 Block:堆上的 Block 捕获 __block 变量时,变量的值会被复制到堆上的 Block 中,作用域结束后,__block 变量的值不会被更新。
    4.3.3. 使用 Block_copy 函数
    • 栈上的 Block:对栈上的 Block 使用 Block_copy 函数会将其复制到堆上,返回一个新的堆上 Block。
    • 堆上的 Block:对堆上的 Block 使用 Block_copy 函数仍会返回一个新的堆上 Block,但是地址不同于原始的堆上 Block。
    4.4.4. 使用 __weak 修饰符
    • 栈上的 Block:栈上的 Block 捕获 __weak 变量时,变量会被自动置为 nil,因为栈上的 Block不会强引用 __weak 变量。
    • 堆上的 Block:堆上的 Block 捕获 __weak 变量时,变量会正常工作,因为堆上的 Block会强引用 __weak 变量。

    通过以上方法,可以辨别出 Block 是存储在栈上还是堆上的。在实际开发中,了解 Block 的存储位置有助于正确管理内存和避免潜在的问题。

    五、Block的循环引用

    循环引用是指两个或多个对象之间互相持有对方的强引用,导致它们无法被释放,从而造成内存泄漏。在使用 Block 时,循环引用是一个常见的问题,特别是在 Block 中捕获了外部对象并且外部对象又持有了 Block。

    下面是一个典型的循环引用场景:

    @interface ViewController : UIViewController
    @property (nonatomic, copy) void (^myBlock)(void);
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.myBlock = ^{
            // Block 中捕获了 self,导致循环引用
            NSLog(@"Block is executed");
            [self doSomething];
        };
    }
    
    - (void)doSomething {
        // Block 中捕获了 self,导致循环引用
        NSLog(@"Doing something");
    }
    
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在上面的代码中,ViewController 的实例持有一个 Block,并且在 Block 中捕获了 self,导致了循环引用。当 ViewController 的实例被释放时,由于 Block 中持有了 selfViewController 的实例无法被释放,从而造成内存泄漏。

    为了避免循环引用,可以采取以下方法:

    1. 使用 __weak 修饰符来避免循环引用,例如在 Block 中捕获 self 时使用 __weak 修饰符:

      __weak typeof(self) weakSelf = self;
      self.myBlock = ^{
          // 使用 weakSelf 避免循环引用
          NSLog(@"Block is executed");
          [weakSelf doSomething];
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
    2. 使用 __block 修饰符来避免循环引用,可以让 Block 持有一个弱引用的指针,而不是强引用:

      __block typeof(self) blockSelf = self;
      self.myBlock = ^{
          // 使用 blockSelf 避免循环引用
          NSLog(@"Block is executed");
          [blockSelf doSomething];
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6

    通过以上方法,可以有效地避免 Block 的循环引用问题,确保内存管理的正确性。在实际开发中,特别是在使用 Block 时,需要注意避免循环引用问题,以避免内存泄漏和其他潜在的问题。

  • 相关阅读:
    (02)Cartographer源码无死角解析-(17) SensorBridge→里程计数据处理与TfBridge分析
    图像识别与处理学习笔记(三)形态学和图像分割
    北漂七年拿过阿里、腾讯、华为offer的资深架构师,分享经验总结
    使用Scrapy框架集成Selenium实现高效爬虫
    以sqlilabs靶场为例,讲解SQL注入攻击原理【18-24关】
    linux内核获取本机网卡的统计信息
    利用免费的敏捷研发管理工具管理端到端敏捷研发流程
    RDS恢复其中某个数据库备份的流程
    多进程编程(四):共享内存
    让你不再害怕指针
  • 原文地址:https://blog.csdn.net/u010545480/article/details/136284954