白话文之Block

作者: 旺仔Milk | 来源:发表于2019-03-20 14:32 被阅读4次

    相信无论是学习 还是面试的时候 block 都是一个绕不开的问题
    如果你连block用还都不会用 那么,请多用block

    目录

    1. Block是什么?

    2. Block的截获变量



    Block是什么?(怎样理解block?)

    百度一下搜索到的很多人给出的答案都是 block 是代码块或者说是 匿名函数, 原则上都没有错,我以前面试时候 也是 这么对面试官说的;
    但是当你在深入理解一下, 你会发现 iOS里的 Block 其实就是一个对象
    扩展一点来说 block是封装了函数调用以及函数调用环境的OC对象
    (我用了加粗,如果你也能对别人说 block 是一个对象, 想好怎么给人说明 block 为什么是一个对象)

    block是封装了函数调用已经函数调用环境的OC对象

    针对环境这一词用在block上 我是在MJ的底层原理课程上听到 杰哥提到的,而也是因为这一个词语解释出了Block的重要内容之一截获变量的概念(后面相关小结会给出具体解释)

    一切从你写的一行block开始

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            void (^block)(void) = ^{
                NSLog(@"Hello, World!");
            };
    
            block();
          }
      return 0;
    }
    

    最简单的一个 block 声明 与调用, 相信大部分同学都从网上的视频 看到过.m文件转成.cpp文件
    然后 查看Block的 内部实现啊 balabala........
    如果你以为我也要 这么做, 恭喜你 答对了 我确实要这么做 不过我会换另外一个角度来用点通俗易懂的语言来解释 block 底层做了点什么
    去掉了一些个强制转换后的上面Block的cpp版本(原文对照看注释)

    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ 
    { __AtAutoreleasePool __autoreleasepool;
            /* 
            void (^block)(void) = ^{
                NSLog(@"Hello, World!");
            };
            */
            void (*block)(void) = &__main_block_impl_0(
                                                       __main_block_func_0,
                                                       &__main_block_desc_0_DATA
                                                       );
    
            // block();
            block->FuncPtr(block);
        }
        return 0;
    }
    

    ok, 已经有开发经验的同学可以很轻松的看出来cpp 版本里这是创建了一个block(废话!谁看不出来)

    往__main_block_impl_0函数中传入了两个参数:__main_block_func_0 和 __main_block_desc_0_DATA
    先来翻一下这俩参数是啥.
    __main_block_desc_0_DATA

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

    这个东西 暂且可以理解为对block的一个说明, 里面有block的大小 和一个保留字段, 这个参数没啥好解释的 知道是啥就行
    下面我们开看看 重头戏
    __main_block_func_0

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_c60393_mi_0);
            }
    

    这个方法 就是block的具体实现了

    image.png
    这里你要注意了哦,在转换为 c++后 这段代码 被独立成了 一个函数

    接着我们来看 __main_block_impl_0都做了些什么

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

    如果只学过OC的同学 可能会纠结

     __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)
    

    这是啥 为啥在结构里里有个这么和玩应 还和结构体名字一样?
    在第一次看别人的文章时候 我也蒙 因为我也不懂 这是干啥用的?
    其实这只是c++的语法而已,如果你不了解c++那么 可以 理解为

    UIView
    //   __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)
    // 就是个初始化方法, 在初始化方法里面给参数进行了赋值
    - (instancetype)initWithFrame:(CGRect)frame {
     /* impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc; */
    }
    

    大意上就是 初始化一个block 然后将 上面生成的 __main_block_func_0 和 __main_block_desc_0_DATA 传给相应的参数
    这里可能会绕一下 结构体里面的 struct __block_impl impl; 是啥东西?
    你可以理解为其实就是一个嵌套结构而已,创建block 时将参数 传入impl里保存

    // __block_impl的结构是这样的
    struct __block_impl {
    // isa 看到了没?  这也证明了 为什么说 block 是一个对象
        void *isa;   
        int Flags;
        int Reserved;
    // FuncPtr : 创建blcok 时候的 __main_block_func_0 参数 赋值给了 这个参数
        void *FuncPtr;
    };
    

    tips: 题外话 想当初你是不是也背过这个方法名?然后磨磨唧唧背不下来? __main_block_impl_0 里的 impl 是啥? 后面为啥还有个 0 ?
    这个保证你搜都不一定有答案 会的人觉得这个问题太2b, 不会的人 搜了半天也无解
    ---其实 impl 就是 implements 单词的缩写(英语不好的 包括我 自行解决吧)
    ---那么0又是啥呢? 打比方 你一个.m 中 可能有多个block吧 那你怎么保证 第 50行写下的 block 不会调用你 138行 写的第二个block 呢?
    这就是 0的作用了 编译器 会将你的所有block 进行编号 从而保证一一对应的关系
    第一个block 编号是0 第二个 编号是1 第三个编号是2 以此类推
    对照一下
    __main_block_impl_0
    __main_block_func_0
    __main_block_desc_0
    大致就是样

    这样一看 是不是脑子嗡的一样? 卧槽原来是这样...

    Block的截获变量

    先来讲个故事
    block是封装了函数调用已经函数调用环境的OC对象
    文章开端就提到了 "环境"这个词, 这也让我理解了截获变量到底是干啥用的
    在很久很久以前电脑游戏 还需要CD/DVD来安装的年代
    在安装游戏的时候,你是不是遇到过 要运行游戏,你需要先安装 DirectX 这个东西才能打开游戏痛快的玩耍?
    再或者在网页上看个视频 提示你 要下载 FlashPlayer 才能播放?
    而这里的 DirectX / FlashPlayer 就可以 理解为所需要的环境
    而我 就将 截获外部的变量 理解为 函数表用 所需要的环境

    这里不在重复解释局部变量,静态局部变量, 全局变量,被截获(捕获)后的处理方式
    (局部变量值传递, 静态局部变量引用传递, 全局变量直接使用 balabala的 这些都是苹果规定的 背就完了)
    我们来说说为什么会出现截获变量, 它是怎么来的?

    来一份.m中的原代码

    // 全局变量
    int age_ = 10;
    // 静态全局变量
    static int height_ = 10;
    
    void (^block)(void);
    
    void test()
    {
        // 局部变量
        auto int a = 10;
        // 静态局部变量
        static int b = 10;
        block = ^{
            NSLog(@"age is %d, height is %d, a is %d, b is %d", age_, height_, a, b);
        };
    }
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            test();
            block();
        }
        return 0;
    }
    

    这份代码在编译成cpp文件之后 会长这样

    iint age_ = 10;
    static int height_ = 10;
    
    void (*block)(void);
    
    struct __test_block_impl_0 {
      struct __block_impl impl;
      struct __test_block_desc_0* Desc;
      int a;
      int *b;
      __test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
      int a = __cself->a; // bound by copy
      int *b = __cself->b; // bound by copy
    
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_k3_vyx2rf814vzfz9gzs0vz541h0000gn_T_main_bd3fa6_mi_0, age_, height_, a, (*b));
        }
    
    static struct __test_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    } __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0)};
    
    void test()
    {
        auto int a = 10;
        static int b = 10;
    // 关于静态变量 可以看到 b 被传入的 是 &b (引用)
        block = &__test_block_impl_0(__test_block_func_0, &__test_block_desc_0_DATA, a, &b);
    }
    
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            test();
            block->FuncPtr(block);
        }
        return 0;
    }
    

    大概就是这样了 在 main函数 外面生成了 block的相关对外不可见的函数(上文提到的俩参数)

    ok,换个思维 我们来讨论一个问题


    image.png

    如图所示 我们想在 main方法中拿到 tempA, tempB 两个参数 如果按照上述代码是肯定不可能的
    我们需要创建 外部的变量去赋值 然后在 main方法中使用外部变量才可以实现

    image.png

    局部变量的截获
    对照一下上面生成的 cpp 文件 你发现了什么呢?
    是的 block 的截获变量 其实就是作用域的原因从而产生的处理办法
    虽然.m代码中block的实现是在main函数之内, 但是经过编译器转码 其实block的实现就是一个新的函数__test_block_func_0, 而这个生成的实现函数__test_block_func_0想要调用局部变量 那就需要block自身持有这些变量 (此处存在描述上的歧义, 主旨是为了看的通俗易懂,不要过分纠结此处的字面意思)

    __test_block_impl_0(需要持有 a和 b 呀)然后才能让__test_block_func_0 方法去调用a, b 两个参数;

    全局变量的截获
    网上基本上所有block底层原理的讲述 都会涉及到 局部变量与全局变量
    上面说完了 为啥截获 局部变量, 那么就再来说说全局变量
    那为啥全局变量 不用截获 直接就可以用呢?
    相信看了关于 局部变量的截图就已经明白了 因为全局变量 本身就是在哪都可以用的,所以压根就不需要浪费资源再去持有全局变量, 拿过来直接用就好了呀😝

    相关文章

      网友评论

        本文标题:白话文之Block

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