美文网首页
iOS Block底层解析一

iOS Block底层解析一

作者: KOLee | 来源:发表于2020-04-13 17:41 被阅读0次

    一、为啥要自己跟着解析

    • 前面搞过一篇《iOS Block用法与实现原理》这个有点乱,所以想着大佬们看着心累,我就用自己的理解加上查看的资料重新瞎折腾几番(毕竟自己太菜),没办法人老了,需要记录下来。
      图片.png
    • 根据我个人的理解,画了个思维导图,有助于解析理解,还是建议自己搞一遍 然后画个图 毕竟图毕竟直观

    二、底层解析过程

    • 就按上面的思维图来,来来首先 捕获变量 是什么鬼,就是没有传参数,你又在block里面调用的变量,比如: 就是你去大宝剑,然后你被抓了,不是自愿的,嗯 就是这个意思,开搞 开搞
    void test1() {
        void(^block)(void) =  ^{
             NSLog(@"fuck world");
         };
         block();
    }
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            test1();
        }
        return 0;
    }
    

    然后命令行cd到这个文件目录,在跑xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc 文件名称.m 然后你会发现有一个.cpp的文件 然后拉到代码最下面,.cpp代码如下:

    // 因为我是main函数调用test1的 从这里开始 第一步
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;  // 自动释放池 
            test1();// 方法调用
        }
        return 0;
    }
    
    // 第二步
    void test1() {
        // 代码简化后 __test1_block_impl_0j就是block底层的结构体 括号里面的为参数
        void(*block)(void) = &__test1_block_impl_0(__test1_block_func_0, &__test1_block_desc_0_DATA);
        
         block->FuncPtr(block); // FuncPtr指向block
    }
    
    // 第四步
    struct __block_impl {
      void *isa; // 指向类对象
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    // 第三步
    struct __test1_block_impl_0 {
      struct __block_impl impl; // __block_impl类型结构体
      struct __test1_block_desc_0* Desc; // __test1_block_desc_0类型结构指针
        // 方法函数调用
      __test1_block_impl_0(void *fp, struct __test1_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    // 先不关注
    static void __test1_block_func_0(struct __test1_block_impl_0 *__cself) {
             NSLog((NSString *)&__NSConstantStringImpl__var_folders_r3_94b6gvh96kj3bgwxst32yjh80000gn_T_main_71fc4b_mi_0);
         }
    // 先不关注
    static struct __test1_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    } __test1_block_desc_0_DATA = { 0, sizeof(struct __test1_block_impl_0)};
    
    
    • 上面的代码 有点多啊 有点麻烦,不过还是注释清楚了,不要觉得麻烦,自己去搞搞。先把block底层的基本结构弄清楚,那么差不多就懂一半了,万事开头难嘛。还是要好好说道 说道
    1. 自动释放池 这鬼东西不理先 我们今天只搞block,main函数 调用test1()
      2.__test1_block_impl_0一个指针调用后面跟的参数,注释也写清楚了就走个流程
      3.就是block的主要功能 封装了函数和调用环境的地方
      4.__block_impl结构体里面包含了一个isa指针,那么block本质就是一个封装了函数和调用环境的对象,就是这样了 后面都只是比较性质的了
    • 带参数的block,按上面的方法clang一下
    void test2() {
        void(^block)(int,int) =  ^(int a, int b){
            NSLog(@"a = %d, b = %d",a,b);
        };
        block(20,20);
    }
    clang结构与没带参数的区别 只是多了两个参数 其他一样
    void test2() {
        void(*block)(int,int) = &__test2_block_impl_0((void *)__test2_block_func_0, &__test2_block_desc_0_DATA));
       block->FuncPtr(block, 20, 20);
    }
    
    • 未捕获变量的block 只是多了两个参数 其他一样,解析完了,是不是很简单,有同学会说简单毛线,那么多代码,其实只要把最基础的理解好 下面的block还不是分分钟,是吧。

    三、捕获变量的Block解析过程

    • 局部变量捕获过程,代码如下:
    //捕获auto变量
    void test3() {
         // 局部变量默认 auto 修饰
        int age = 10;  // 相当于 auto int age = 10;
        void(^block)(void) =  ^{
            NSLog(@"age is %d",age);
        };
        age = 20;
        block();
    }
    
    • clang出来的.cpp是这样,我只是把不一样的地方贴出来,一样的我就不贴了
    // 第三步
    void test3() {
        int age = 10;
        // 传age的值进去
        void(*block)(void) = ((void (*)())&__test3_block_impl_0((void *)__test3_block_func_0, &__test3_block_desc_0_DATA, age));
        // 这个是block后面的赋值 没有赋值进block里面 所以修改不了block里面age的值
        age = 20;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    // 第四步
    struct __test3_block_impl_0 {
      struct __block_impl impl;
      struct __test3_block_desc_0* Desc;
      int age; // 多出来的 age 变量
         // int _age, int flags=0) : age(_agez)这里赋值
      __test3_block_impl_0(void *fp, struct __test3_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    
    • 上面的代码看出来了吧, 捕获局部变量为啥要搞这么麻烦

    1.局部变量的作用域出了函数的大括号就释放了,而block什么时候调用呢,不知道 ,万一局部变量释放了,还调用block那不GG了,所以苹果直接把他传进去放在block的结构里面

    • 然后到 静态局部变量 block捕获
      //捕获静态变量
    void test4() {
        static int age = 10;
         void(^block)(void) =  ^{
             NSLog(@"age is %d",age);
         };
         age = 20;
         block();
    }
    打印结果:
    2020-04-13 16:23:28.410241+0800 blockTest[3000:112691] age is 20
    

    -还是clang一下看看代码 我们还是只看代码不同的地方 如下:

    // 第二步
    void test4() {
        static int age = 10;
        // &age指针引用
         void(*block)(void) = &__test4_block_impl_0((void *)__test4_block_func_0, &__test4_block_desc_0_DATA, &age));
         age = 20;
         ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    // 第三步
    struct __test4_block_impl_0 {
      struct __block_impl impl;
      struct __test4_block_desc_0* Desc;
      int *age; // 指针类型属于地址传递
      __test4_block_impl_0(void *fp, struct __test4_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    • 看到了吧 就是指针引用,age的内存地址传到block里面了, 保存的也是int *类型的指针,所以打印的机构不是10,而是20

    1.为啥是20 因为传进去的是static int age = 10;的地址,跟这个指向的是同一块内存地址,所以外面的age变了,block里面的age也跟着变
    2.为啥不是直接把值传递进去呢? 是因为static 是静态变量,static修饰的意思就是有且只有一份的意思,所以直接传指针,反正我是这样理解 苹果就是这样

    • 那么接下来就是 block 捕获 全局变量和静态全局变量的过程,
    //全局变量
    
    static int age = 10;
    int height = 30;
    void test5() {
        void(^block)(void) =  ^{
            NSLog(@"age is %d,height is %d",age,height);
        };
        age = 20;
        height = 40;
        block();
    }
    打印结果:
    2020-04-13 16:39:05.945902+0800 blockTest[3070:119722] age is 20,height is 40
    

    1.clang的代码不用看了,为啥?根据苹果的尿性 ,想想 全局变量 作用域是全局的,类里面随时可以访问,还引用个屁啊,直接拿来用就是了
    2.同学们可以自己看看,跟平常函数调用全局变量一个鬼样,底层代码不会有任何的不同

    • 捕获变量到最后的 对象类型 捕获了 代码如下:
     // 对象类型
    void test6() {
        Person *p = [Person new];
        p.name = @"hello block!";
        void(^block)(void) = ^{
            NSLog(@"--- %@",p.name);
        };
        block();
    }
    打印结果:
    2020-04-13 16:42:14.056053+0800 blockTest[3093:121623] --- hello block!
    
    • 按照惯例clang搞他:
    // 第二步
    void test6() {
    
        // person初始化 objc_msgSend
        Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("new"));
        //  p.name = @"hello block!"; setName 方法
        ((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)p, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_r3_94b6gvh96kj3bgwxst32yjh80000gn_T_main_31b833_mi_0);
        // 开始调用block
        void(*block)(void) = &__test6_block_impl_0((void *)__test6_block_func_0, &__test6_block_desc_0_DATA, p, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    // 第三步
    struct __test6_block_impl_0 {
      struct __block_impl impl;
      struct __test6_block_desc_0* Desc;
      Person *p; // 拿到对象的指针
        // Person *_pz对象的指针传过来
      __test6_block_impl_0(void *fp, struct __test6_block_desc_0 *desc, Person *_p, int flags=0) : p(_p) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    // 第五步 copy调用 _Block_object_assign
    static void __test6_block_copy_0(struct __test6_block_impl_0*dst, struct __test6_block_impl_0*src) {_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    // 第六步 dispose 调用 _Block_object_dispose
    static void __test6_block_dispose_0(struct __test6_block_impl_0*src) {_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    // 第四步
    static struct __test6_block_desc_0 {
      size_t reserved;
      size_t Block_size;
        /*
         生成了copy和 dispose的两个函数指针
         */
      void (*copy)(struct __test6_block_impl_0*, struct __test6_block_impl_0*);
        
      void (*dispose)(struct __test6_block_impl_0*);
        
    } __test6_block_desc_0_DATA = { 0, sizeof(struct __test6_block_impl_0), __test6_block_copy_0, __test6_block_dispose_0};
    
    • 得慢慢看,狗日,这么多不同,不过都差不多

    1.第四步:copy 和dispose 分别调用低五步和第六步,__test6_block_copy_0会对block做一次retain操作,__test6_block_dispose_0会对block做一次release操作,引用计数管理嘛,
    2.为啥要这么做?Person对象是局部变量,然后block拿去用了,那Person对象的内存管理就得你自己管理咯,比如:你大宝剑被抓了,人家叔叔肯定管吃管住是一样的

    • 捕获变量终于搞完了 贼累,但是要为下一篇__block 修饰这些捕获的变量 做个铺垫,了解下block的内存管理

    四、Block内存管理

    • 直接照搬了,内存管理这种东西必须要记的吖,理论性的,Block 类型:

    在前面的 Block 结构体中都存在一个 isa 指针,且在构造函数的时候赋值 &_NSConcreteStackBlock。所以可以猜测认为 block 其实也是对象的一种, 尝试对 block 调用 class 方法来看看会有什么输出:


    图片.png

    可以看出来 block 确实是对象且主要的 block 类型(都是继承自NSBlock)有以下三种:

    • NSGlobalBlock( _NSConcreteGlobalBlock )存放在 数据段 中
    • NSStackBlock( _NSConcreteStackBlock ) 存放在 栈 中
    • NSMallocBlock( _NSConcreteMallocBlock )存放在 堆 中

    block 是属于哪一种类型总结下来可以用下面的图片表示:

    图片.png
    图片.png
    • 上面就是这样 MRC的代码不写了,搬一下别人的,
    补充: ARC 环境下下列操作会自动将 block 进行 copy 操作:
    • block 作为方法的返回值
    • 将 block 赋值给 __strong 指针时
    • block 作为Cocoa API中方法名含有usingBlock的方法参数时
    • block 作为GCD API的方法参数时
    • 最后今天就这样,内存管理这个,自己理解吧 我也没啥好办法,但是还是根据我自己的理解说道 说道
    1.代码段的block{}出了这个大括号,ARC会帮我们释放了
    2.栈上的block系统分配的,啥时候释放我们不知道了
    3.堆上的block,就是要自己管理的了,怎么说:就是你把栈上的block copy就会到堆上
    
    Block对变量的内存管理总结:
    • block 在栈上的时候,不会强引用外部的任何变量
    • block 从栈到堆上的时候,会有一次 copy 操作,在 copy 操作的时__main_block_copy_0 函数会根据捕获外部变量的 strong、weak修饰,来直接对强/弱引用外部变量。
    • 如果捕获变量是 __block 修饰的,copy 到堆上的 block 会将变量转换成的结构体 copy 到堆上同时生成强引用,变量转换成的结构体自身对外部变量的强弱引用则是根据捕获变量时变量自身的强弱修饰符决定。
    • 如果堆上的 block 被销毁了,__main_block_dispose_0 会对 block 引用的变量进行 release 操作。
    • 好了,搞得腰酸背痛,去按一下,下回分解__block

    相关文章

      网友评论

          本文标题:iOS Block底层解析一

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