美文网首页
iOS — block 底层原理

iOS — block 底层原理

作者: iOS丶lant | 来源:发表于2022-01-19 13:44 被阅读0次

众所周知,block可以封装一个匿名函数为对象,并捕获上下文所需的数据,并传给目标对象在适当的时候回调。
本文将从源码出发,分析block的本质。

一、Block定义

1. Block 定义及使用

返回值类型 (^block变量名)(形参列表) = ^(形参列表) {
};

// 调用Block保存的代码
block变量名(实参);

2. 项目中使用格式
在项目中,通常会重新定义block的类型的别名,然后用别名来定义block的类型

// 定义block类型
typedef void (^Block)(int);

// 定义block
Block block = ^(int a){};

// 调用block
block(3);

二、Block底层实现

block的底层实现是结构体,和类的底层实现类似,都有isa指针,可以把block当成是一个对象。下面通过创建一个控制台程序,来窥探block的底层实现
block 的内存结构图:

Block_layout结构体成员含义如下:

  • isa: 指向所属类的指针,也就是block的类型
  • flags: 标志变量,在实现block的内部操作时会用到
  • Reserved: 保留变量
  • invoke: block执行时调用的函数指针,block内部的执行代码都在这个函数中
  • descriptor: block的详细描述,包含 copy/dispose 函数,处理block引用外部变量时使用
  • variables: block范围外的变量,如果block没有调用任何外部变量,该变量就不存在

Block_descriptor结构体成员含义如下:

  • reserved: 保留变量
  • size: block的内存大小
  • copy: 拷贝block中被 __block 修饰的外部变量
  • dispose: 和 copy 方法配置应用,用来释放资源
    具体实现代码如下(代码来自Block_private.h):
enum {
    BLOCK_REFCOUNT_MASK =     (0xffff),
    BLOCK_NEEDS_FREE =        (1 << 24),
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25),
    BLOCK_HAS_CTOR =          (1 << 26), /* Helpers have C++ code. */
    BLOCK_IS_GC =             (1 << 27),
    BLOCK_IS_GLOBAL =         (1 << 28),
    BLOCK_HAS_DESCRIPTOR =    (1 << 29)
};

/* Revised new layout. */
struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};

struct Block_layout {
    void *isa;
    int flags;
    int reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

二、Block转成c++代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 最简block
        ^{ };
    }
    return 0;
}

利用 clang.m 的文件转换为 .cpp 文件,就可以看到 block 的底层实现了

$ clang -rewrite-objc main.m 

转换后的代码:

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
    }
    return 0;
}

从代码中可以看出,__main_block_impl_0就是block的C++实现(最后面的_0代表是main中的第几个block),__main_block_func_0是block的代码块,__main_block_desc_0是block的描述,__block_impl是block的定义。

__block_impl结构体为

struct __block_impl {
  void *isa;//指向所属类的指针,也就是block的类型
  int Flags;//标志变量,在实现block的内部操作时会用到
  int Reserved;//保留变量
  void *FuncPtr;//block执行时调用的函数指针
};

__main_block_impl_0解释如下:

  • impl: block对象
  • Desc: block对象的描述

其中,__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) 这是显式构造函数,flags的默认值为0,函数体如下:

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }

可以看出:

  • __main_block_impl_0的isa指针指向了_NSConcreteStackBlock,所有的局部block都是在栈上门创建
  • 从main函数中看, __main_block_impl_0的FuncPtr指向了函数__main_block_func_0
  • __main_block_impl_0的Desc也指向了定义__main_block_desc_0时就创建的__main_block_desc_0_DATA,其中Block_size记录了block结构体大小等信息

__main_block_desc_0成员含义如下:

  • reserved: 保留变量
  • Block_size: block内存大小,sizeof(struct __main_block_impl_0)

三、Block变量截获

局部变量截获 是值截获。 比如:

    NSInteger num = 3;

    NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){

        return n*num;
    };

    num = 1;

    NSLog(@"%zd",block(2));

这里的输出是6而不是2,原因就是对局部变量num的截获是值截获。
同样,在block里如果修改变量num,也是无效的,甚至编译器会报错。


对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的。也就是说block的自动变量截获只针对block内部使用的自动变量, 不使用则不截获, 因为截获的自动变量会存储于block的结构体内部, 会导致block体积变大。特别要注意的是默认情况下block只能访问不能修改局部变量的值。



   NSMutableArray * arr = [NSMutableArray arrayWithObjects:@"1",@"2", nil];

    void(^block)(void) = ^{

        NSLog(@"%@",arr);//局部变量

        [arr addObject:@"4"];
    };

    [arr addObject:@"3"];

    arr = nil;

    block();

打印为1,2,3
局部对象变量也是一样,截获的是值,而不是指针,在外部将其置为nil,对block没有影响,而该对象调用方法会影响

局部静态变量截获 是指针截获。

   static  NSInteger num = 3;

    NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){

        return n*num;
    };

    num = 1;

    NSLog(@"%zd",block(2));

输出为2,意味着num = 1这里的修改num值是有效的,即是指针截获。
同样,在block里去修改变量m,也是有效的。

__block 修饰的外部变量
对于用 block 修饰的外部变量引用,block 是复制其引用地址来实现访问的。block可以修改block 修饰的外部变量的值。

__block int age = 10;
myBlock block = ^{
    NSLog(@"age = %d", age);
};
age = 18;
block();

输出为:

age = 18

为什么使用__block 修饰的外部变量的值就可以被block修改呢?
我们使用 clang 将 OC 代码转换为 C++ 文件:

__block int val = 10;
转换成
__Block_byref_val_0 val = {
    0,
    &val,
    0,
    sizeof(__Block_byref_val_0),
    10
};

会发现一个局部变量加上block修饰符后竟然跟block一样变成了一个Block_byref_val_0结构体类型的自动变量实例!!!!

四、Block的几种形式

block有三种类型:

  • 全局块(_NSConcreteGlobalBlock)
  • 栈块(_NSConcreteStackBlock)
  • 堆块(_NSConcreteMallocBlock)

这三种block各自的存储域如下图:

  • 全局块存在于全局内存中, 相当于单例.
  • 栈块存在于栈内存中, 超出其作用域则马上被销毁
  • 堆块存在于堆内存中, 是一个带引用计数的对象, 需要自行管理其内存

a、遇到一个Block,我们怎么这个Block的存储位置呢?

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


Block 既不在栈又不在堆中,在代码段中,ARC和MRC下都是如此。此时为全局块。


2、Block访问外界变量


MRC 环境下:访问外界变量的 Block 默认存储栈中。

ARC 环境下:访问外界变量的 Block 默认存储在堆中(实际是放在栈区,然后ARC情况下自动又拷贝到堆区),自动释放。


b、ARC下,访问外界变量的 Block为什么要自动从栈区拷贝到堆区呢?

栈上的Block,如果其所属的变量作用域结束,该Block就被废弃,如同一般的自动变量。当然,Block中的__block变量也同时被废弃。如下图:


为了解决栈块在其变量作用域结束之后被废弃(释放)的问题,我们需要把Block复制到堆中,延长其生命周期。开启ARC时,大多数情况下编译器会恰当地进行判断是否有需要将Block从栈复制到堆,如果有,自动生成将Block从栈上复制到堆上的代码。Block的复制操作执行的是copy实例方法。Block只要调用了copy方法,栈块就会变成堆块。


Block的类 位置 复制效果
NSGlobalBlock 程序数据区 什么也不做
NSStackBlock 从栈复制到堆上
NSMallocBlock 引用计数增加

代码示例:

1、不使用外部变量的block是全局block

   NSLog(@"%@",[^{
        NSLog(@"globalBlock");
    } class]);

输出:NSGlobalBlock

2、使用外部变量并且未进行copy操作的block是栈block

NSInteger num = 10;
    NSLog(@"%@",[^{
        NSLog(@"stackBlock:%zd",num);
    } class]);

输出:NSStackBlock

日常开发时是将block当做参数传递给方法:

- (void)testWithBlock:(void(^)(NSString *string))callback{
    NSString *string = @"string";
    callback(string);

    NSLog(@"%@",[callback class]);
}

    NSInteger num = 10;
    [self testWithBlock:^(NSString *string) {
        NSLog(@"stackBlock:%zd",num);
    }];

输出:NSGlobalBlock

3、对栈block进行copy操作,就是堆block,而对全局block进行copy,仍是全局block

  • 比如堆1中的全局进行copy操作,即赋值:
void (^globalBlock)(void) = ^{
        NSLog(@"globalBlock");
    };

 NSLog(@"%@",[globalBlock class]);

输出:NSGlobalBlock 仍是全局block

  • 而对2中的栈block进行赋值操作:
NSInteger num = 10;

void (^mallocBlock)(void) = ^{

        NSLog(@"stackBlock:%zd",num);
    };

NSLog(@"%@",[mallocBlock class]);

输出:NSMallocBlock

对栈blockcopy之后,并不代表着栈block就消失了,左边的mallock是堆block,右边被copy的仍是栈block
比如:

[self testWithBlock:^{

    NSLog(@"%@",self);
}];

- (void)testWithBlock:(dispatch_block_t)block
{
    block();

    dispatch_block_t tempBlock = block;

    NSLog(@"%@,%@",[block class],[tempBlock class]);
}

输出:NSStackBlock,NSMallocBlock

即如果对栈Block进行copy,将会copy到堆区,对堆Block进行copy,将会增加引用计数,对全局Block进行copy,因为是已经初始化的,所以什么也不做。

c、block变量与forwarding

在copy操作之后,既然block变量也被copy到堆上去了, 那么访问该变量是访问栈上的还是堆上的呢?forwarding 终于要闪亮登场了,如下图:

通过forwarding, 无论是在block中还是 block外访问block变量, 也不管该变量在栈上或堆上, 都能顺利地访问同一个__block变量。

相关文章

网友评论

      本文标题:iOS — block 底层原理

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