美文网首页
Block底层

Block底层

作者: 深圳_你要的昵称 | 来源:发表于2020-11-18 23:31 被阅读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

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

    • Cocoa框架的方法且方法名中含有usingBlock等时
    • GCD的API

    下图是变量作用域 在被 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 持有情况

    如下图所示👇

    • 一个Block中使用了__block变量
    • 多个Block中使用了__block变量
    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操作呢?

    • 调用Block的copy实例方法时
    • Block作为函数返回值返回时
    • 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量
    • 在方法名中含有usingBlock的Cocoa框架方法GCD的API中传递Block时
    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修饰符下的
    • 基础类型的👇(将int a = 10;改为__block int a = 10;)
      --> a的类型是引用类型结构体__Block_byref_a_0,使用a是__forwarding->a,同时包含copydispose函数,__flag是8(BLOCK_FIELD_IS_BYREF)
    • 对象类型的👇 (将int a = 10;改为__block NSNumber *a = @(10);)
      --> Block中包含成员变量void (*__Block_byref_id_object_copy)(void*, void*);void (*__Block_byref_id_object_dispose)(void*);, a的类型也是__Block_byref_a_0,使用a也是__forwarding->a__flag也是8(BLOCK_FIELD_IS_BYREF)

    综上,得出下表👇

    外界变量类型 被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对象的👇


    部分注解如下👇

    • 第1 位,释放标记,一般常用 BLOCK_NEEDS_FREE 做 位与 操作,一同传入 Flags , 告知该 block 可释放。
    • 第16位,存储引用计数的值,是一个可选用参数;
    • 第24位,程序根据它来决定是否增加或是减少引用计数位的值;
    • 第25位,是否拥有拷贝辅助函数(a copy helper function);
    • 第26位,是否拥有 block 析构函数;
    • 第27位,标志是否有垃圾回收;
    • 第28位,标志是否是全局block;
    • 第30位,与 BLOCK_USE_STRET 相对,判断是否当前 block 拥有一个签名。用于 runtime 时动态调用。

    同时,注意到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

    • 第一层Copy
      之前我们分析过,Block在引用外部变量的情况下是栈block,但是通过变量作用域的改变就变成堆block,通过汇编我们也得出,是经过了objc_retainBlock,它又调用了_Block_copy👇
    • 第二层Copy
      _Block_copy源码的最后,执行了_Block_call_copy_helper,源码如下👇

    继续看看_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为什么可以修改持有的外部变量的原因。

    • 第三层Copy
      _Block_byref_copy中我们看到src2->byref_keep,其实就是调用外部的__Block_byref_id_object_copy_131,为什么?

    __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底层源码工程

    相关文章

      网友评论

          本文标题:Block底层

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