iOS block源码解析

什么是block

block是什么?虽然所有的iOS开发都会使用到这个对象,但实际上要完全搞清楚block也是不容易的。

首先下个定义,实际上block是包含函数指针及上下文调用的对象。后面,我们再慢慢看下block内部究竟是怎样进行函数调用和上下文的处理。

block实质

举个?

我们先拿一个简单的block进行block内部实现的分析。


- (void)test {
    
    int temp = 1;
    
    void (^Mblock)(void) = ^() {
        NSLog(@"%d", temp);
    };
    
    Mblock();
}

以上是一个非常简单的block,无参数,无返回值。我们就拿这个进行分析。老规矩。clang -rewrite-objc xx.m,编译后变成了如下内容:

struct __MBlock__test_block_impl_0 {
  struct __block_impl impl;
  struct __MBlock__test_block_desc_0* Desc;
  int temp;
  __MBlock__test_block_impl_0(void *fp, struct __MBlock__test_block_desc_0 *desc, int _temp, int flags=0) : temp(_temp) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __MBlock__test_block_func_0(struct __MBlock__test_block_impl_0 *__cself) {
  int temp = __cself->temp; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_jb_tlqj59rx34dfrxpflntjghz80000gn_T_MBlock_a175b7_mi_0, temp);
    }

static struct __MBlock__test_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __MBlock__test_block_desc_0_DATA = { 0, sizeof(struct __MBlock__test_block_impl_0)};

static void _I_MBlock_test(MBlock * self, SEL _cmd) {

    int temp = 1;

    void (*Mblock)(void) = ((void (*)())&__MBlock__test_block_impl_0((void *)__MBlock__test_block_func_0, &__MBlock__test_block_desc_0_DATA, temp));

    ((void (*)(__block_impl *))((__block_impl *)Mblock)->FuncPtr)((__block_impl *)Mblock);
}

如上的代码,我们看出来。block的调用其实是使用函数指针进行调用的。block对象实际上在编译器处理后是__MBlock__test_block_impl_0对象。

__MBlock__test_block_impl_0的实现:

struct __MBlock__test_block_impl_0 {

  struct __block_impl impl;
  struct __MBlock__test_block_desc_0* Desc;
  
  int temp;
  
  //初始化
  __MBlock__test_block_impl_0(void *fp, struct __MBlock__test_block_desc_0 *desc, int _temp, int flags=0) : temp(_temp) {
    impl.isa = &_NSConcreteStackBlock;  //栈block
    impl.Flags = flags; //标记
    impl.FuncPtr = fp;  //实现
    Desc = desc;    //描述
  }
};

而在初始化的时候传入的参数:

void (*Mblock)(void) = ((void (*)())&__MBlock__test_block_impl_0((void *)__MBlock__test_block_func_0, &__MBlock__test_block_desc_0_DATA, temp));

  • __MBlock__test_block_func_0
  • __MBlock__test_block_desc_0_DATA
  • temp

我可以通过上面的注释看出,__MBlock__test_block_func_0就是我们在block内部的实现,是一个函数的指针,并通过初始化的时候赋值给impl。

__MBlock__test_block_desc_0_DATA__MBlock__test_block_desc_0的初始化方法,内部存储的是关于block的一些描述,size和预存字段reserved。

最后的参数则是block内部需要的参数temp。

调用


 ((void (*)(__block_impl *))((__block_impl *)Mblock)->FuncPtr)((__block_impl *)Mblock);
  

这个函数精简一下:

(Mblock->FuncPtr)(Mblock);

实际上就是调用函数指针,并把block传入。最后调用实现函数:


static void __MBlock__test_block_func_0(struct __MBlock__test_block_impl_0 *__cself) {

  int temp = __cself->temp; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_jb_tlqj59rx34dfrxpflntjghz80000gn_T_MBlock_a175b7_mi_0, temp);
    }
    

阶段小结

实质上block就是一个函数指针的包装,它同时还有截获变量的功能,并自己维护一个内存管理(关于内存管理方面,后面会介绍)。

Block截获变量

关于block截获变量的问题在笔试中经常考到,这里探讨一下block在截获变量的时候做了什么。

首先,我们搞清楚变量的类型有哪些。变量包括:

  • 全局变量
  • 静态全局变量
  • 局部变量(基本数据类型、引用数据类型)
  • 静态局部变量

关于这个问题,我们同样举个?:

下面这个代码中,我们定义了静态全局变量temp3、全局变量temp4、局部变量temp、局部变量temp2、强类型的strObj。


#import "MBlock.h"

@implementation MBlock

static int temp3 = 3;
int temp4 = 4;

- (void)test {
    
    int temp = 1;
    static int temp2 = 2;
    
    __strong NSObject *strObj;
    
    void (^Mblock)() = ^() {
        NSLog(@"%d", temp);
        NSLog(@"%d", temp2);
        NSLog(@"%d", temp3);
        NSLog(@"%d", temp4);
        NSLog(@"%@", strObj);
    };
    
    
    Mblock();
}

@end

我们通过编译的源码来看下最后怎么截获变量的吧。(保留关键代码)


static int temp3 = 3;
int temp4 = 4;

struct __MBlock__test_block_impl_0 {
  struct __block_impl impl;
  struct __MBlock__test_block_desc_0* Desc;
  
  //内部参数只有局部变量
  int temp;
  int *temp2;
  NSObject *__strong strObj;
  
  __MBlock__test_block_impl_0(void *fp, struct __MBlock__test_block_desc_0 *desc, int _temp, int *_temp2, NSObject *__strong _strObj, int flags=0) : temp(_temp), temp2(_temp2), strObj(_strObj) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void _I_MBlock_test(MBlock * self, SEL _cmd) {

    int temp = 1;
    static int temp2 = 2;

    __attribute__((objc_ownership(strong))) NSObject *strObj;

    //
    void (*Mblock)() = ((void (*)())&__MBlock__test_block_impl_0((void *)__MBlock__test_block_func_0, &__MBlock__test_block_desc_0_DATA, temp, &temp2, strObj, 570425344));


    ((void (*)(__block_impl *))((__block_impl *)Mblock)->FuncPtr)((__block_impl *)Mblock);
}

从上面的代码中,我们可以看出实际上在block实质上的对象Impl中只有局部变量的参数存在,全局变量不存在变量的截获。而且,静态局部变量保存的是一个指针,引用类型变量则保留了所有权修饰符。

__block

我们常常会在block内部修改变量的值,这个时候就需要添加__block修饰符。

举个?


@implementation MBlock

static int temp3 = 3;
int temp4 = 4;

- (void)test {
    
    __block int temp = 1;
    static int temp2 = 2;
    
    void (^Mblock)() = ^() {
        temp = 3;
        temp2 = 3;
        temp3 = 3;
        temp4 = 3;
    };
    
    Mblock();
}

@end

可以看出只有局部变量在block内部修改的时候需要添加__block修饰。编译后结果:

struct __Block_byref_temp_0 {
    void *__isa;    //isa指针
    __Block_byref_temp_0 *__forwarding; //内存相关指针
    int __flags;    
    int __size; //占据空间大小
    int temp;   //实际上的值
};

static void _I_MBlock_test(MBlock * self, SEL _cmd) {

    __attribute__((__blocks__(byref))) __Block_byref_temp_0 temp = {(void*)0,(__Block_byref_temp_0 *)&temp, 0, sizeof(__Block_byref_temp_0), 1};
    static int temp2 = 2;

    void (*Mblock)() = ((void (*)())&__MBlock__test_block_impl_0((void *)__MBlock__test_block_func_0, &__MBlock__test_block_desc_0_DATA, &temp2, (__Block_byref_temp_0 *)&temp, 570425344));

    ((void (*)(__block_impl *))((__block_impl *)Mblock)->FuncPtr)((__block_impl *)Mblock);
}

可以看出编译后,局部变量temp变成了结构体对象__Block_byref_temp_0。初始化的时候,把temp的地址传入。

static void __MBlock__test_block_func_0(struct __MBlock__test_block_impl_0 *__cself) {
  __Block_byref_temp_0 *temp = __cself->temp; // bound by ref
  int *temp2 = __cself->temp2; // bound by copy

        (temp->__forwarding->temp) = 3;
        (*temp2) = 3;
        temp3 = 3;
        temp4 = 3;
    }

而最后调用的时候,是通过temp对象的forwarding指针获取对象,再去获取temp的值。至于为啥要通过forwarding指针去绕一圈的原因,下面说block内存相关内容的时候会进行介绍。

内存管理

block内存相关的问题,在前面的解说中遇到了一部分。现在我们详细解释一下。

前面我们介绍block的时候,Impl内部有一个参数isa,它指向的是_NSConcreteStackBlock。很明显,这是一个栈block的意思,之前的?也是在栈中创建的block。那么还存在其他的类型么?

block的分类

  • NSGlobalBlock
  • NSStackBlock
  • NSMallocBlock

NSGlobalBlock

全局block位于text段,并且在block内部没有引用任何外部变量。


void (^globalBlock) () = ^ () {
      NSLog(@"global block");
};
NSLog(@"%@", globalBlock);

//<__NSGlobalBlock__: 0x1096e203a>

NSStackBlock

NSStackBlock位于栈内存,函数返回后block将无效,在block内部可以引用外部变量。在MRC下,NSStackBlock在函数的空间被收回后再调用会crash,所以使用的时候需要手动copy。而ARC下,当其赋值给strong对象时,系统会自动copy,所以在ARC下还是很难看到NSStackBlock的。

int temp=0;
NSLog(@"%@", ^{
    NSLog(@"stack block here, temp =%d", temp);
});

//<__NSStackBlock__: 0x7fff592ebad3>

void (^block)()=^{
    NSLog(@"stack block here, temp =%d", temp);
};
NSLog(@"%@",block);
    
//<__NSMallocBlock__: 0x7fae49e03262>

NSMallocBlock

堆内存的block,可以对其引用计数+1或者-1,copy的话不会产生新的对象。

copy

对上面的三个block进行copy操作的话,都会发生什么呢?经过尝试发现了如下的规则:

操作
NSGlobalBlockcopyNSGlobalBlock
NSStackBlockcopyNSMallocBlock
NSMallocBlockcopy计数+1

并且实际上之前__block对象的forwarding指针指向随着copy操作而改变,copy前指向栈区的指针,在copy后会指向堆区。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值