Block底层

2020-11-18  本文已影响0人  深圳_你要的昵称

前言

大家在日常的开发工作中经常会用到Block,都知道它是一个匿名函数,那具体是一个怎样的结构呢,相信知道的人不多,今天我们重点查看下Block的底层源码实现原理。

一、Clang分析Block

示例👇

- (void)viewDidLoad {
    [super viewDidLoad];

    int a = 10;
    void (^block)(void) = ^{
        NSLog(@"Cooci - %d",a);
    };
    NSLog(@"%@",block);
}

我们先clang看看,block所对应的c++代码是什么样的👇

我们先看看函数名称_block_impl_0

接着我们看看匿名函数的第1个入参__ViewController__viewDidLoad_block_func_0👇

第2个入参__ViewController__viewDidLoad_block_desc_0_DATA👇

至此,我们通过Clang分析Block对应的底层C++代码,Get到两点:

  1. Block对应的C++底层是结构体xxx_block_impl_0,其中xxx就是Block所在的路径,哪个文件的哪个方法里声明的Block。该结构体包含2个重要的入参:block_func_0_block_desc_0。(block_desc_0_DATA只是_block_desc_0的一个别名)
  2. Block会自动捕获外部变量,并将其保存到了Block底层结构体中。

至于block_func_0_block_desc_0的底层源码,我们在后面会有分析。

现在我们稍微改一下示例代码, 将int a 改成__block int a👇

- (void)viewDidLoad {
    [super viewDidLoad];

    __block int a = 10;
    void (^block)(void) = ^{
        NSLog(@"Cooci - %d",a);
    };
    
    NSLog(@"%@",block);
}

再看看clang之后的结果👇

明显发现,由a变成了(__Block_byref_a_0 *)&a
接着看看__Block_byref_a_0的定义👇

然后查看a的赋值,在__ViewController__viewDidLoad_block_func_0里面👇

由之前的int a = __cself->a变成__Block_byref_a_0 *a = __cself->a。之前是值拷贝,现在是引用的拷贝,a是一个地址指针,指向了外部变量a=10,使用的时候取的是a->__forwarding->a,就是引用

最后看看Block的底层xxx_block_impl_0👇

一样,它也有isa指针,指向_NSConcreteStackBlock,说明Block也分类别,具体分为哪几类呢?

1.1 Block的类型

Block大致分为3种类型:_NSConcreteStackBlock _NSConcreteGlobalBlock_NSConcreteMallocBlock,具体区别如下👇

类别 存储域 详情
_NSConcreteStackBlock 栈区 自动截获变量并且在该变量作用域内
_NSConcreteGlobalBlock 静态全局区域(.data区) 定义全局变量的地方定义Block;Block语法的表达式中不截获任何变量时,或只截获了全局变量、静态变量
_NSConcreteMallocBlock 堆区 _NSConcreteStackBlock超出变量作用域,ARC大多数情况下,编译器进行适当判断后调用_Block_copy拷贝到

示例👇

  1. _NSConcreteStackBlock 栈区Block,注意:现在必须使用__weak修饰,
    int a = 10;
    void ( __weak ^block)(void) = ^{
        NSLog(@"----%d",a);
    };

    // block_copy
    NSLog(@"%@",block);
打印结果是
<__NSStackBlock__: 0x7ffeed6d53f8>
  1. _NSConcreteGlobalBlock全局Block
    void (^block)(void) = ^{
        NSLog(@"------");
    };
    NSLog(@"%@",block);
打印结果是
<__NSGlobalBlock__: 0x10bb2d030>
  1. _NSConcreteMallocBlock堆区Block
    int a = 10;
    void (^block)(void) = ^{
        NSLog(@"----%d",a);
    };
    NSLog(@"%@",block);
打印结果是
<__NSMallocBlock__: 0x60000179d950>

全局区的Block很好理解,不包含任务外部变量,要包含也只能是全局变量或静态变量,说白了就是静态全局区的变量。但是栈区 和 堆区的,就很难区分了,在ARC下编译器大多数情况会适当地进行判断然后,自动将Block从栈复制到堆,那编译器在什么情况下不能判断需要手动复制呢?

向方法或函数的参数中传递Block

但是如果在方法或函数中适当地复制了传递过来的参数,那么就不必在调用该方法或函数前手动复制了,以下方法或函数不用手动复制:

下图是变量作用域 在被 Block__block修饰符作用后生命周期发生的变化👇


示例👇

typedef void (^blk_t)(void);
NSArray *getBlockArray() {
    int val = 10;
    //ARC不会自动复制,需手动复制
    return [[NSArray alloc] initWithObjects:^{NSLog(@"blk0: %d", val);}, ^{NSLog(@"blk1: %d", val);}, nil];
//    return [[NSArray alloc] initWithObjects:[^{NSLog(@"blk0: %d", val);} copy], [^{NSLog(@"blk1: %d", val);} copy],nil];
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        void (^globalBlock)(void) = ^{ };
        //__NSGlobalBlock__
        NSLog(@"GlobalBlock is %@", globalBlock);
        
        __block int a = 10;
        void (^stackBlock)(void) = ^void { a++; };
        //MRC    __NSStackBlock__
        NSLog(@"StackBlock is %@", stackBlock);
        //ARC   __NSMallocBlock__
        NSLog(@"MallocBlock is %@", stackBlock);
        
        NSArray *array = getBlockArray();
        blk_t blk = (blk_t)[array objectAtIndex:0];
![2020111314240475.png](https://img.haomeiwen.com/i3444487/a06867d70cb7ad2d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
        blk(); //如果没有手动copy复制,崩溃。因getBlockArray()执行完后,栈上的Block被废弃。
    }
    return 0;
}

1.2 __block变量存储域

捕获了__block修饰符的外部变量的Block__block变量存储域也会受到影响

__block变量的配置存储域 Block从栈复制到堆时的影响
从栈复制到堆并被Block持有
被Block持有
1.2.1 持有情况

如下图所示👇

1.2.2 释放情况

堆上Block被废弃,它所使用的__block变量也就被释放

1.3 __forwarding

之前我们看到了如果Block持有了外部被__block修饰的变量,那么C++底层会将外部变量做一个引用处理,使用到了__forwarding👇

可以看出,__forwarding实际是指向了结构体__Block_byref_a_0的首地址,如下图所示👇

那么,__block变量被Block从栈区copy到堆区后,Block的成员变量__forwarding的指向也发生了变化,如下图所示👇

1.4 Copy 和 Dispose

还是回到我们之前clang生成的C++源码,有两个函数👇

一个copy,一个dispose,为什么会有这两个函数的存在呢?因为C语言结构体不能含有附有__strong修饰符的变量,因为编译器不知道应何时运行C语言结构体初始化废弃操作,不能很好地管理内存。但是OC的运行时库能准确把握从栈复制到堆以及堆上的Block被废弃时机,因此Block结构体中即使含有附有__strong修饰符__weak修饰符的变量,也可以恰当地进行初始化和废弃。为此需要使用在xxx_block_desc_0结构体中增加的成员变量copydispose,以及作为指针赋值给该成员变量的__main_block_copy_0函数__main_block_dispose_0函数

1.4.1 栈copy到堆的时机

既然copy操作是将栈上的Block复制到堆时,那什么时候会触发这个copy操作呢?

1.4.2 基础类型 与 对象类型 被Block捕获的区别

分两种情况:不包含__block修饰符__block修饰符下的

  1. 不包含__block修饰符
    • 基础类型的👇 --> 结构很简单,直接使用int a,并没有copydispose函数

    • 对象类型的👇 (将int a = 10;改为NSNumber *a = @(10);) -->使用的是对象 cself->a,有copydispose函数,__flag是3(BLOCK_FIELD_IS_OBJECT

  1. __block修饰符下的

综上,得出下表👇

外界变量类型 被Block捕获后变量的类型 包含copy dispose函数 __flag标识位
基础类型 原类型 默认标识
对象类型 原类型 BLOCK_FIELD_IS_OBJECT
__block基础类型 __Block_byref_a_0引用类型,不包含成员变量copy dispose BLOCK_FIELD_IS_BYREF
__block对象类型 __Block_byref_a_0引用类型,包含成员变量copy dispose BLOCK_FIELD_IS_BYREF

二、循环引用

2.1 什么是循环引用

示例代码👇

warning :Capturing 'self' strongly in this block is likely to lead to a retain cycle

2.2 解决方式

打破循环引用有3种方式:

  1. 我们大家熟知的,weak-strong-dance
- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    self.name = @"lg_cooci";
    self.block = ^(void){
        __strong typeof(self) strongSelf = weakSelf;
        NSLog(@"%@", strongSelf.name);
    };
    self.block();
}

切记,一定要__strong,防止Block捕获的对象过早的释放。

  1. __block修饰的外部变量持有
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.name = @"lg_cooci";
    __block ViewController *vc = self;
    self.block = ^(void){
        NSLog(@"%@", vc.name);
        vc = nil;
    };
    self.block();
}

注意,__block外部变量使用完成后,记得置为nil,否则引发内存泄露。

3.Block中添加入参

typedef void(^KCBlock)(ViewController *);

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.name = @"lg_cooci";
    self.block = ^(ViewController *vc){
        NSLog(@"%@", vc.name);
    };
    self.block(self);
}

这种方式代码里最少,也很好理解。

三、Block底层原理

之前我们只是从C++代码的层面,大致分析了Block的底层结构体组成,再结合__block对外部变量存储域的影响,现在我们再从汇编层入手,查看Block的底层实现流程。
首先断点进入汇编层👇

再在objc_retainBlock处打断点,step into进入查看👇

发现了Block是在库libobjc,底层是调用_Block_copy,就是我们通常所说的从栈拷贝到堆,这个会在后面的三层Copy中重点分析其内部流程处理。

Block签名

接着我们在block调用前打断点,再查看寄存器信息👇

Block本身就是匿名函数,当然有方法签名信息。@代表对象,代表是函数指针。

Block底层源码

Block对应的底层是结构体Block_layout,其中flags是个枚举,用来描述Block对象的👇


部分注解如下👇

同时,注意到Block_layout还包含一个结构体Block_descriptor_1👇

除了Block_descriptor_1之外,并未包含Block_descriptor_2Block_descriptor_3,这是因为在没有引用外部变量捕获到不同类型变量时,编译器会改变结构体的结构,按需添加Block_descriptor_2Block_descriptor_3

flags包含BLOCK_HAS_COPY_DISPOSE时,会加入Block_descriptor_2;当flags包含BLOCK_HAS_SIGNATURE时,会加入Block_descriptor_3Block_descriptor_2Block_descriptor_3是通过Block_descriptor_1指针偏移来访问的。

Block 结构图如下所示👇

__block底层源码

之前我们通过clang得出的C++源码可知,被__block修饰符修饰的外部变量,在Block中是结构体Block_byref👇

除了Block_byref,还有Block_byref_2 Block_byref_3,道理与Block_descriptor_x一样,也是编译器根据__block修饰的变量的类型来确定的。其中Block_byref_2里的byref_keepbyref_destroy 函数是来处理里面持有对象的保持销毁

重点:三层Copy

继续看看_Block_descriptor_2源码👇

其实是通过flags中是否有BLOCK_HAS_COPY_DISPOSE值来判断是否需要copy、dispose,然后通过内存平移,找到对应的Block_descriptor_2返回。

如果满足Block_descriptor_2👇

里面包含一个成员copy,根据我们上面分析知道,__block修饰的对象类型,被Block捕获后,会生成成员变量copy dispose,我们示例看下底层C++代码,查找下对应的copy函数👇

- (void)viewDidLoad {
    [super viewDidLoad];

    __block NSNumber *a = @(10);
    void (^block)(void) = ^{
        NSLog(@"Cooci - %@", a);
    };
    
    block();
    
    NSLog(@"%@",block);
}

clang一下👇


最终定位到是_Block_object_assign,看其源码👇

接着看_Block_byref_copy源码👇

static struct Block_byref *_Block_byref_copy(const void *arg) {
    struct Block_byref *src = (struct Block_byref *)arg;
    
    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src指向栈
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        copy->isa = NULL;
        // byref value 4 is logical refcount of 2: one for caller, one for stack
        //这里或上4是因为栈的forwarding通过下面的代码指向了堆
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        copy->forwarding = copy; // 堆上的forwarding指向堆自身
        src->forwarding = copy;  // 栈上的forwarding也指向堆
        copy->size = src->size;

        if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
            //有copy、dispose,通过src偏移一个struct Block_byref结构体大小拿到src2, 也就是包含copy和dispose成员变量的Block_byref_2结构体
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;

            if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
                //从src2偏移一个struct Block_byref_2大小拿到src3, 也就是包含layout成员变量的Block_byref_3结构体
                struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
                struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
                copy3->layout = src3->layout;
            }
            //调用外部的__Block_byref_id_object_copy_131
            (*src2->byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    }
    // already copied to heap
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }

    return src->forwarding;
}

我们看到struct Block_byref *copy = (struct Block_byref *)malloc(src->size);这里就是第二层copy。

copy->forwarding = copy; src->forwarding = copy; 这两句代码可以看出,堆上的变量的forwarding指向了自己,而栈上的forwarding也指向了堆,这样就实现了栈和堆指向同一变量的操作,也就是__block为什么可以修改持有的外部变量的原因。

__Block_byref_id_object_copy_131入参里面,有一个内存平移40,原因👇

而131 = 128 +3,其中128表示BLOCK_BYREF_CALLER --> 代表__block变量有copy/dispose的内存管理辅助函数👇

我们示例里是对象类型NSNumber,就表示是这个BLOCK_FIELD_IS_OBJECT,即为3。然后copy函数拼起来就是__Block_byref_id_object_copy_131,而它里面调用的是_Block_object_assign,走的就是下面这个case👇

这个就是最后一层的copy操作。

Block销毁流程

最后我们看看Block的销毁流程是什么样的。上述示例中,我们去查查dispose里调用的底层方法是哪个?👇

131的原因同理,不做分析了。继续看_Block_object_dispose👇

示例的变量类型是__block NSNumber类型,所以走case BLOCK_FIELD_IS_BYREF,接着看看_Block_byref_release👇

因为Block捕获的外部变量是__block NSNumber类型,存在一个栈copy到堆的过程,所以需要释放堆的Block,通过byref->forwarding找到堆Block,最终free释放掉。

其它的情况:基础类型对象类型被__block修饰的基础类型,也是按照这个思路跟进源码去分析流程,这里就不做分析。

总结

本篇文章开头通过Clang得出Block对应的底层结构体,同时配合__block修饰符分析了基础类型对象类型被__block修饰的基础类型被__block修饰的对象类型四种情况下的变量的存储域的变化,还有所对应的底层Block的类别,及Block的成员变量和函数的区别,进而分析了3层copydispose销毁的流程。

附件

Cooci大神的Block底层源码工程

上一篇下一篇

猜你喜欢

热点阅读