美文网首页
Objective-C Block初探

Objective-C Block初探

作者: 收纳箱 | 来源:发表于2020-03-18 21:45 被阅读0次

    1. 概述

    一个闭包包含:

    • 一个函数(或指向函数的指针)
    • 该函数执行时需要的上下文变量

    在Objective-C中,Block就是闭包。

    Block有以下特点:

    • 可以嵌套定义,定义Block方法和定义函数方法类似(所以也常称Block为匿名函数)
    • Block可以定义在方法的内部或者外部
    • 和函数很像,Block()调用时才会执行内部的方法
    • Block的本质是对象,使代码高聚合

    2. Block的定义

    很多人觉得Block的定义很怪异,很难记住。但其实和C语言的函数指针的定义对比一下,你很容易就可以记住。

    // Block
    returnType (^blockName)(parameterTypes)
    
    // 函数指针
    returnType (*c_func)(parameterTypes)
    

    例如输入和返回参数都是字符串:

    (char *) (*c_func)(const char *);
    (NSString *) (^block)(NSString *);
    
    • 本地变量

      returnType (^blockName)(parameterTypes) = ^returnType(parameters) {
        ...
      };
      
    • 属性

      @property (nonatomic, copy, nullability) returnType (^blockName)(parameterTypes);
      
    • 方法参数

      - (void)someMethodThatTakesABlock:(returnType (^nullability)(parameterTypes))blockName;
      
    • 方法调用的参数

      [someObject someMethodThatTakesABlock:^returnType (parameters) {
        ...
      }];
      
    • C函数的参数

      void SomeFunctionThatTakesABlock(returnType (^blockName)(parameterTypes));
      
    • typedef

      typedef returnType (^TypeName)(parameterTypes);
      TypeName blockName = ^returnType(parameters) {...};
      

    3. Block与外部变量

    3.1 捕获自动变量(局部变量)

    typedef void (^MyBlock)(void);
    
    默认情况

    对于Block外部的变量,Block默认是将其复制到数据结构中来实现访问的:

    • Block内未使用外部变量,不发生捕获
    • Block内部使用了外部变量,发生捕获,Block结构体重会存储相应的数据,导致其体积增大。

    默认情况下Block只访问局部变量的值,而不需要修改它。

    int age = 10;
    MyBlock block = ^{
        NSLog(@"age = %d", age);
    };
    age = 18;
    block();
    
    //输出
    10
    
    __block修饰

    如果需要修改外部变量的值,则需要对变量使用__block进行修饰。此时,Block会复制变量的引用地址来实现访问,所以可以修改外部的变量值。

    __block int age = 10;
    MyBlock block = ^{
        NSLog(@"age = %d", age);
    };
    age = 18;
    block();
    
    //输出
    18
    

    一句话就是:没有__block修饰是值引用,有__block修饰是指针引用

    3.2 __block修饰原理

    为什么使__block修饰就可以修改变量值呢?我们创建一个main.m文件。

    #import <Foundation/Foundation.h>
    
    typedef void (^MyBlock)(void);
    
    int main(int argc, const char * argv[])
    {
      @autoreleasepool
      {
         __block int age = 10;
         MyBlock block = ^{
             NSLog(@"age = %d", age);
         };
         age = 18;
         block();
      }
      return 0;
    }
    

    然后在终端中输入:

    clang -rewrite-objc main.m
    

    会生成一个main.cpp文件。

    __block int age = 10;
    ...
    age = 18;
    
    //转换为
    __Block_byref_age_0 age = {
       0,
       &age,
       0,
       sizeof(__Block_byref_age_0),
       10
    };
    ...
    age.__forwarding->age = 18;
    

    我们发现__block修饰之后,局部变量age变成了__Block_byref_age_0结构体类型变量实例。

    现在我们要访问age变量,则需要通过__forwarding成员变量来间接访问。

    4. Block的copy操作

    4.1 Block的存储域

    Block是存储在栈上还是堆上呢?

    其实,Block有三种不同的类型:

    • 全局块(_NSConcreteGlobalBlock),存在于全局内存中,相当于单例。
    • 栈块(_NSConcreteStackBlock),存在于栈内存中,超出作用域后会被销毁。
    • 堆块(_NSConcreteMallocBlock),存在于堆内存中,是一个带引用计数的对象,需要自己管理内存。
    Block存储区域

    看到一个Block,我们如何确认Block的存储区域呢?

    Block不访问外界变量(包括栈和堆中的变量)

    MRC和ARC中都一样,Block在代码段中,即全区块。

    Block访问外界变量

    MRC:默认存储在栈区

    ARC:默认存储在栈区,然后ARC情况下,按需自动拷贝到了堆区,自动释放。

    4.2 Block的copy操作

    那么问题来了:为什么ARC下,访问外界变量的Block会自动拷贝到堆区呢?

    栈块存在于栈内存中,超出作用域后会被销毁。使用__block的变量也会被回收。为了解决这个问题,需要把栈块复制到堆上,延长其生命周期。开启ARC时,大多数情况下编译器会恰当地进行判断,是否需要将Block从栈复制到堆。如果需要,自动生成复制代码。Block的复制操作执行的是copy实例方法。Block调用了copy方法就会从栈块变成堆块。

    例如下面这个返回类型为blk_tBlock的函数:

    typedef int (^blk_t)(int);
    
    blk_t func(int rate) {
        return ^(int count) { return rate * count; };
    }
    

    返回之前的Block是在栈上的,返回之后作用域结束,会被释放。ARC开启时,编译器就会自动完成复制。而MRC时,需要我们执行copy方法手动执行。

    将Block从栈上拷贝到堆上相当消耗CPU,所以Block在栈上够用,就不要执行复制了。不然就会浪费CPU资源。

    Block的复制实际是调用copy实例方法,不同类型的Block执行copy效果也稍微有些不同。

    Block类 副本源的存储域 复制效果
    _NSConcreteGlobalBlock 程序的数据区 什么也不做
    _NSConcreteStackBlock 从栈复制到堆
    _NSConcreteMallocBlock 引用计数增加

    虽然栈块也是对象,但它没有引用计数。因为栈区的内存由编译器自动分配释放,不需要引用计数。

    4.3 Block的__forwarding

    copy之后,Block变量也会被复制到堆上。那么访问变量是访问栈上的还是堆上的呢?之前看到的__block变量的__forwarding就是解决这个问题的。

    __forwarding

    通过__forwarding,无论是在Block中还是Block外访问__block变量,也不管该变量是在栈上或者堆上,都可以顺利访问同一个__block变量。

    5. 防止Block循环引用

    某个类将Block作为自己的变量,同时又在Block内部使用了类实例对象本身,就会出现循环引用:

    self.someBlock = ^{
            [self doSomething];
    }
    

    即自己持有Block,Block又捕获了自己,造成循环引用。

    1. MRC,使用__block

      __block typeof(self) blockSelf = self;
      self.someBlock = ^{
           [blockSelf doSomething];
      }
      
    2. ARC,使用__weak

      __weak typeof(self) weakSelf = self;
      self.someBlock = ^{
           [weakSelf doSomething];
      }
      

    注意:在ARC中使用__block也可能出现循环应用。

    typedef void(^Block)(void);
    @interface SomeObj: NSObject
    @property (nonatomic, copy) Block block;
    @end
      
    @implementation SomeObj
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            __block typeof(self) blockSelf = self;
            self.block = ^{
                NSLog(@"Self: %@", blockSelf);
                blockSelf = nil;
            };
        }
        return self;
    }
    
    - (void)execBlock
    {
        self.block();
    }
    @end
      
    //使用类
    SomeObj *obj = [[SomeObj alloc] init];
    [obj execBlock]; //如果不执行execBlock,blockSelf永远不会置空,循环引用一直存在。
    

    相关文章

      网友评论

          本文标题:Objective-C Block初探

          本文链接:https://www.haomeiwen.com/subject/peocyhtx.html