ios - block原理解读(一)

作者: tigerAndBull | 来源:发表于2019-02-17 20:03 被阅读104次

    前言

    block在网络上的文章也比较多,
    本文将开发中block使用细节和block实现原理结合起来,
    加上个人的理解,
    帮助大家更好地理解block和使用block。

    问题

    • block的意义
    • block为什么不能修改外部变量?这里的外部变量又指的是什么?
    • block为什么要用copy
    • 被block引用的对象,引用计数为何+=2?
    • __block 又是什么原理?
    • 循环引用究竟是为何引起的?

    等等

    block产生的意义

    程序始终都要遵循逐行执行的原则,
    而block 可以理解为 逻辑触发执行
    定时器 可以理解为 时间触发执行

    举个有点意思的例子:

    背景:我现在身处异世界,我有很多酷炫的技能
    我现在有这样一个技能,发动这个技能,我可以在当前这个地方留一个分身并安排好任务,然后我可以继续做我自己的事情了,等我再次使用这个技能时,我的分身将开始处理这项任务,处理完成后,我的分身将会消失。

    隐含的问题:给分身安排具体任务的时候,这个任务在未来是否能够完成是未知的,因为我们不知道未来会发生什么,
    代码亦是如此

    进一步理解:程序是严密而又真实的,所以并不存在什么高科技呀,黑魔法呀~
    block其实就是用程序实现了代码缓存和对象缓存,相应的对象缓存在block对象中,
    执行block,就是把缓存的代码执行一遍,而相应的对象的状态,可能会因为执行完缓存下来的代码而发生变化。

    到此,希望你对block会产生了那么一点点的兴趣~

    依旧还是要从源码说起


    强调:以下讲的变量a 默认指的是 基础类型的变量,不是对象类型


    #import <UIKit/UIKit.h>
    
    int main(int argc, char * argv[]) {
        @autoreleasepool {
            
            int a = 0;
            void (^block)(void) = ^{
                NSLog(@"%d",a);
            };
            
           block();
    
            return 0;
        }
    }
    

    将其翻译成底层c++文件, 一点一点看
    main函数里的代码

    int a = 10;
    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    

    这是一大串什么?
    不要着急,我们从上到下,从左往右进行说明。
    首先,block初始化这一行

    // 原代码
    void (^block)(void) = ^{
           NSLog(@"%d",a);
     };
    // 翻译成c++代码
    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
    // 简化后
    block =  &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA,a);
    
    1. void (*block)(void) 函数指针,参数void,返回值void

    2. ((void (*)()) 上面函数指针的类型,作用是强转

    3. &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a)) 分解来看

    分解第一步:__main_block_impl_0 组成

    __main_block_impl_0是一个结构体,包含一个构造函数和三个变量
    一个普通的结构体类型,一个结构体指针类型,
    还有一个和外部的变量a,名称一样,类型一样。

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int a;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    分解第二步:__main_block_impl_0 的 构造函数

    第一个参数void *fp,传入的是 &__main_block_func_0

    __main_block_func_0是一个静态函数,
    它的参数又是__main_block_impl_0这个结构体指针

    即 FuncPtr 存储的是 __main_block_func_0 静态函数地址

    impl.FuncPtr = fp;
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int a = __cself->a; // bound by copy
      NSLog((NSString *)&__NSConstantStringImpl__var_folders_lb_tby1gwds2fnb89dzkf4cq3xh0000gn_T_main_9bc6d9_mi_0,a);
    }
    

    是不是快要绕晕了呢?

    在静态函数__main_block_func_0内,
    先是获取__main_block_impl_0结构体内的变量a,然后打印出来。

    从这一点可以看出 静态函数的作用就是写在block内部的代码的容器和入口。

    而__main_block_impl_0结构体内的变量a的值来自外部,是在结构体的构造函数内进行了赋值操作。

    得到如下结论:

    获取到的基础变量的值在block初始化的时候已经确定了,
    block外部的变量a在后续无论做什么操作,都不会影响block内部保存的变量a
    这就是在block内部直接修改基本变量会报错的原因,
    如果这里直接允许修改了,在目前条件下,也仅仅能做到block内部的变量a进行重新赋值操作,和外部变量a没有关系,产生歧义。

    示例图.png
    当然,使用__block修饰的基本变量可以进行修改,那么又是什么原理呢?我们先继续解读当前的源码

    第二个参数 &__main_block_desc_0
    __main_block_desc_0静态结构体存储的是block的基础信息

    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)};
    

    第三个参数为我们用到的外部的变量a,赋值给结构体内的变量a

    第四个参数没有传,默认为0 (辅助变量,可以忽略,不会影响block的理解)

    分解第三步:__main_block_impl_0 中的 __block_impl

    存储的block的信息,相应的参数在构造函数内进行了赋值操作

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

    最后 block初始化代码和执行代码放在一起看

    void (^block)(void) = ^{
           NSLog(@"%d",a);
     };
    // c++代码
    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
    // 简化
    block =  &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a)
    
    block();
    // c++代码
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    // 简化
    block->FuncPtr(block);
    

    block->FuncPtr 指的就是 __main_block_func_0这个函数的地址,参数为block本身

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int a = __cself->a; // bound by copy
      NSLog((NSString *)&__NSConstantStringImpl__var_folders_lb_tby1gwds2fnb89dzkf4cq3xh0000gn_T_main_19f603_mi_0,a);
     }
    

    串起来再看block构造

    block初始化:block 通过 __main_block_impl_0结构体构造函数进行初始化,同时生成__main_block_func_0静态函数,并将其地址以及其他相关信息储存在__block_impl这个结构体成员变量中。
    其中,__block_impl这个结构体成员变量是__main_block_impl_0的首地址。

    block调用:block指针指向的是__main_block_impl_0 的首地址,即__block_impl的地址,所以可以强转为(__block_impl *)类型,并访问其成员FuncPtr,指向的是静态函数地址,并传入参数__main_block_impl_0,也就是block自己。

    名称 类型 是否随block内容改变 生成顺序 说明
    __block_impl 结构体 NO 1 底层结构体,属于__main_block_impl_0成员
    __main_block_impl_0 结构体 YES 2 缓存变量/对象,主结构体
    __main_block_func_0 静态函数 YES 2 缓存代码,地址存放在__block_impl中

    该缓存代码指的是:

    缓存代码一词,代码的意思.png

    总结:
    将外部变量/对象的信息缓存在__main_block_impl_0中,
    将代码缓存在静态函数中,
    静态函数在缓存代码的时候需要用到外部变量/对象的信息
    执行block就是执行了该静态函数


    如果到此有不理解的地方可能c++基础较薄弱,百度一下辅助查看


    最后

    本文说明了block的用意,揭开了黑魔法的初级面纱,细讲了block基础源码,解释了基础类型的变量为何不能在block内部直接修改

    后续会借此基础之上,继续解读目录中的问题

    相关文章

      网友评论

        本文标题:ios - block原理解读(一)

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