<<iOS 与OS X多线程和内存管理>>

作者: 神经骚栋 | 来源:发表于2018-03-07 22:04 被阅读619次

    前言


    <<iOS 与OS X多线程和内存管理>>笔记:Blocks中我写的都是我们日常开发过程中所用到的Blocks.这里我们深层次的看一下Blocks的相关实现.

    把OC代码转换为C++结构体代码


    为了使我们更方便看清Block内部的运行,我们需要把OC代码代码转化为带有结构体的C++代码.这里我们就需要使用到clang -rewrite-objc指令.步骤有如下两步.

    • 打开终端,使用cd指令进入需要转化的文件目录下,比如我要对桌面上的Test工程下的main.m文件进行转化.终端指令类似于下图所示.
    • 然后执行如下的终端命令 clang -rewrite-objc main.m,如下所示.

    然后在当前文件夹下就会出现后缀为.cpp的C++执行文件.如下所示.

    Block的实现

    首先,我们在main函数中写一个简单block匿名函数并且进行调用,如下所示.

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            void (^blk)(void) = ^{printf("Block\n");};
            blk();
        }
        return 0;
    }
    

    然后,我们通过 clang -rewrite-objc main.m指令把mian.m转变为C++文件.里面代码较多,我们下拉到文件的最底部.

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    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;
      }
    };
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("Block\n");
    }
    
    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)};
    
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
            ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        }
        return 0;
    }
    

    我们可以看到,我们写的block已经被转化为一个C++语言的函数,如下所示.

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("Block\n");
    }
    

    概念函数的参数__cself相当于C++实例方法中指向实例自身的变量this,或是Objective-C实例方法中指向对象自身的变量self,也就是说参数____cself为指向Block值的变量.可是我们发现____cself并没有在这里使用,这里我们先不做研究,我们先看一下参数____cself的本质.

    struct __main_block_impl_0 *__cself
    
    • Block的结构体

    我们看到参数____cself是__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;
      }   
    };
    

    通过<<iOS 与OS X多线程和内存管理>>我们可以了解到两个成员变量各包含什么信息.

    • Block结构体的成员变量

    我们先看一下成员变量impl的结构体(在.cpp文件的顶部位置).如下所示.

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;//今后版本升级所需的区域
      void *FuncPtr;//函数指针
    };
    

    第二个成员变量Desc主要是存储今后版本升级所需的区域和Block大小.具体如下所示.

    static struct __main_block_desc_0 {
      size_t reserved; //今后版本升级所需的区域
      size_t Block_size; //Block大小
    }
    
    • Block的构造

    接下来我们就看一下__main_block_impl_0的构造函数是如何构造的.在main函数中调用的源码如图所示.

    书中为了方便大家理解这句代码调用,进行了如下的转换.也就是说blk其实上是指向类型为__main_block_impl_0的tmp结构体指针.

            struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA);
    
            struct __main_block_impl_0 *blk = &tmp;
    

    接下来我们看一下结构体的构造函数的参数.首先是__main_block_desc_0_DATA这个参数.我们在代码中找到了它的赋值过程.如下所示.

    static struct __main_block_desc_0  __main_block_desc_0_DATA = { 
                                 0, 
                                 sizeof(struct __main_block_impl_0)
    };
    

    通过上面的构造函数,__main_block_impl_0的值就会如下所示.

        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = 0;
        impl.Reserved = 0;
        impl.FuncPtr = ___main_block_func_0;
        Desc = &__main_block_desc_0_DATA;
    
    • Block的调用过程

    接下来我们看一下使用block的代码是如何实现的.

         blk();
    

    找到.cpp文件对应的代码如下所示.

        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    

    我们去掉转化部分.简化代码之后如下所示.这句代码是什么意思呢?这就是使用函数的指针调用函数.正如我们刚刚所示的一样.正如上一个模块所说的那样,___main_block_func_0的函数指针被赋值到了结构体的FuncPtr中了.另外___main_block_func_0的所需参数是__main_block_impl_0的类型,也就是blk.所以有以下的函数调用.

        (*blk->FuncPtr)(blk);
    
    • Block的实质

    这时候我们需要回过头来说明__main_block_impl_0结构体成员变量 impl中的isa指针.

    我们知道isa指针在构造函数中被赋值为&_NSConcreteStackBlock.如下图所示.

    其实Block就是Objective-C对象.为什么这么说呢?首先我们看一下什么叫做Objective-C对象.

    在Objective-C中,任何类的定义都是对象。类和类的实例(对象)没有任何本质上的区别。任何对象都有isa指针。

    假定我们创建一个如下的对象.

    @interface MyObject : NSObject
    {
        int val0;
        int val1;
    }
    @end
    

    那么基于Objective-C对象的结构体就应该如下所示.

    struct MyObject
    {
        Class isa;
        int val0;
        int val1;
    }
    

    其中的isa指针指向如下所示.具体可查看书中的98页.


    通过比较我们知道Block的结构体中有isa指针._NSConcreteStackBlock就相当于上图的class_t结构体实例.也就是说Block即为Objective-C的对象.

    Block截获自动变量值的实现


    对于Block截获自动变量值,在<<iOS 与OS X多线程和内存管理>>笔记:Blocks中我们已经说过了,现在我们列举一下例子.来看一下是如何实现截获自动变量值这一过程的.

            int number = 1;
            
            void (^blk)(void) = ^{
                printf("value:%d\n",number);
            };
            number = 3;
            blk();
    

    运行程序.打印结果如下所示.


    通过clang -rewrite-objc main.m指令编译成C++文件.其中核心代码如下所示.

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int number;//新增成员变量
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _number, int flags=0) : number(_number) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int number = __cself->number; // bound by copy
    
                printf("value:%d\n",number);
    }
    
    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)};
    
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    
            int number = 1;
    
            void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, number));
            number = 3;
            ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    
        }
        return 0;
    }
    

    这时候我们把Block的结构体拿出来看一下.我们发现新增了一个成员变量number以及构造方法发生新增了对number的赋值.

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int number;//新增成员变量
    
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _number, int flags=0) : number(_number) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    然后看一下main函数中__main_block_impl_0构造函数的构造过程.

            int number = 1;
            void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, number));
    

    这一步我们就知道在__main_block_impl_0结构体构造的时候已经把number的值存储到了自身成员变量number中了,所以后面number如何改变,那么Block在构造完成之后打印的number值就不会发生改变了.

    通过上面的表述,我们可以就了解为什么在不能Block中直接修改变量的值?(面试题).例如下图所示.

    这是为什么呢?我们看一下__main_block_func_0函数的实现,如下所示.我们可以知道传递的是__main_block_impl_0结构体的成员变量的值.而不是指针(其实就算是指针也没有任何的关系),跟原来的number变量无任何关系.所以我们不能在函数中直接修改number变量变量.

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
                int number = __cself->number; // bound by copy
    
                printf("value:%d\n",number);
    }
    

    __block说明符的实现


    上面一个模块最后我们说到如果直接在block中给变量赋值会报错,我们发现根本原因就是Block结构体中传递的是变量值,而不是指针,那么如何解决这一问题呢?这时候__block说明符就出现了.我们看一下C语言代码,如下所示.

            __block int number = 1;
            
            void (^blk)(void) = ^{
                printf("value:%d\n",number);
                number = 6;
            };
            blk();
    

    但是通过clang -rewrite-objc main.m指令转变的C++代码去发生了很大的变化.核心代码如下所示.

    //numbr变量已经通过__block的修饰变成了结构体
    struct __Block_byref_number_0 {
      void *__isa;
    __Block_byref_number_0 *__forwarding;
     int __flags;
     int __size;
     int number;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_number_0 *number; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_number_0 *_number, int flags=0) : number(_number->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_number_0 *number = __cself->number; // bound by ref
    
                printf("value:%d\n",(number->__forwarding->number));
                (number->__forwarding->number) = 6;
            }
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->number, (void*)src->number, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->number, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
      void (*dispose)(struct __main_block_impl_0*);
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    
            __attribute__((__blocks__(byref))) __Block_byref_number_0 number = {(void*)0,(__Block_byref_number_0 *)&number, 0, sizeof(__Block_byref_number_0), 1};
    
            void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_number_0 *)&number, 570425344));
            ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        }
        return 0;
    }
    

    我们看一下主要改变的部分.int number = 1;变成__block int number = 1;之后,C++代码如下所示.代码量提升了不是一倍两倍呀~

    struct __Block_byref_number_0 {
      void *__isa;
    __Block_byref_number_0 *__forwarding;//指向自身的指针
     int __flags;
     int __size;
     int number;
    };
    

    然后我们看一下在main函数中的构造代码.如下所示.

    __attribute__((__blocks__(byref)))  __Block_byref_number_0 number = {(void*)0,(__Block_byref_number_0 *)&number, 0, sizeof(__Block_byref_number_0), 1};
    

    简化代码之后,如下所示.

    __Block_byref_number_0 number = {
    0,
    &number,
    0, 
    sizeof(__Block_byref_number_0), 
    1
    };
    

    这时候Block结构体的构造函数和新增成员变量也发生了改变.成员变量变成了指向__Block_byref_number_0类型的结构体.

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_number_0 *number; //新增成员变量
    
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_number_0 *_number, int flags=0) : number(_number->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    那么在block中进行赋值的时候是如何操作的呢?这主要是通过__Block_byref_number_0的成员变量__forwarding来完成的.__forwarding是指向本身的指针.我们可以通过__forwarding来找到成员变量number的值.所以在__main_block_func_0函数实现中有如下的代码.

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
                __Block_byref_number_0 *number = __cself->number; // bound by ref
                printf("value:%d\n",(number->__forwarding->number));
                (number->__forwarding->number) = 6;
    }
    

    对于__Block_byref_number_0结构体中的__forwarding指针,我们可以看下面的示意图.

    Block存储域


    通过下面一张表我们了解到Block和__block变量时存储在栈区的结构体类型自动变量(一般情况下).

    名称 实质
    Block 栈上Block的结构体实例
    __block 栈上__block变量的结构体实例

    接下来我们还是来研究Block结构体的isa指针,在前面的例子中,isa指针是指向_NSConcreteStackBlock的.其实还有很多类似的类.我们先用一张表格来说明每个类的不同点

    设置对象的存储域 副本源的配置存储域 复制效果
    _NSConcreteStackBlock 从栈区复制到堆区
    _NSConcreteMallocBlock 引用计数增加
    _NSConcreteGlobaBlock 全局区 全局区 什么也不做

    通过上面的表格,我们就可以知道两个面试题的答案,

    问: Block的类一共有几种?
    答: 三种,分别是 _NSConcreteStackBlock 、_NSConcreteMallocBlock、_NSConcreteGlobaBlock

    问: Block为什么用copy修饰?
    答: block在定义成属性的时候应该使用copy修饰,平常我们使用的block主要是存放在栈区的(有的也会存放在全局区).栈区的block出了作用域之后就会被释放掉,如果我们在block释放掉之后还继续调用,那么就会出现crash.理论上,在全局区的block我们是不需要进行copy的.但是大部分的block是存储在栈区的,为了统一规范管理,所以我们都使用copy对block属性进行修饰.

    __block变量存储域


    上一个模块是对Block进行了说明,那么对于使用__block变量的Block从栈上复制到堆上是,__block变量会有什么影响呢?

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

    上面这张表是表达了什么意思呢? 也就是说:

    1. 如果有一个Block使用某个__block变量,那么__block变量会从栈复制到堆并被Block持有.
    2. 如果有多个Block使用某个__block变量,那么在第一个Block中__block变量会从栈复制到堆并被第一个Block持有.从第二个Block时是持有__block变量,也就是只会增加__block变量的引用计数.

    对于__forwarding指针(指向自身的指针),我们曾经说过,"不管__block变量配置在栈上还是堆上,都能正确访问该变量."我们可以通过下面的例子来说明一下情况.

    __block int val = 0;
    
    void (^blk)(void) = [^{ ++val; } copy];
    
    ++val;
    
    blk();
    
    NSLog(@"%d",val);
    
    

    通过blk这个Block的copy操作, 被__block修饰的val变量成功的从栈上复制到了堆上了.

    所以^{ ++val; }++val;都可以被转化为以下的形式.

    ++(val.__forwarding->val);
    

    我们可以通过下面的示意图来表示上面的转变过程.

    截获对象的实现


    我们曾经说过截获变量值,现在我们说一下截获对象的实现.演示源码如下所示.

            void (^blk)(id obj);
    
            {//array的作用域
            id array = [[NSMutableArray alloc] init];
            blk = [^(id obj){
                
                [array addObject:obj];
                NSLog(@"array count = %ld",[array count]);
            } copy];
            }//array的作用域已经结束
    
            blk([NSObject new]);
            blk([NSObject new]);
            blk([NSObject new]);
    

    我们知道array的作用域已经结束了(到达注释位置时候),可以我们调用block仍然可以访问到array.如下所示,这是为什么呢?

    实际上在blk的实现过程中.已经持有了array对象.<<iOS 与OS X多线程和内存管理>>是有以下代码的.

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      id  __strong array; //强引用的array成员变量
    };
    

    在Objective-C中,C语言结构体并不能含有__strong修饰符的变量.因为编译器不知道应该何时进行C语言结构体的初始化和废弃操作.不能很好的管理内存.Objective-C的运行时库可以很好的把握Block从栈上复制到堆以及堆上的Block被废弃的时机.从而有效管理成员变量的持有和释放.为此,在__main_block_desc_0就增建了两个成员变量copy和dispose,已经对应的函数.用于成员变量的持有和释放.如下图所示.

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
      void (*dispose)(struct __main_block_impl_0*);
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
    

    可是我在实际过程中并没有__strong修饰词.个人猜想是已经进行了缺省操作了.省略了__strong的修饰符.源码截图如下所示.大家可以自行试验操作.

    循环引用的本质


    上一个模块我们说了.Block可以持有对象.如果一个对象中含有某个Block的成员属性(strong修饰).在Block中直接使用self,会造成循环引用,原因就出现__main_block_impl_0结构体中的obj.__main_block_impl_0对obj是强引用,self对Block变量是强引用,两者相互引用,最终造成循环引用.

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      id  __strong obj; //强引用的obj成员变量
    };
    

    示意图如下所示.


    结束


    这一篇Block的实现总共写了三天,加上自己验证,收获良多,希望这一篇博客对大家有所帮助.还是希望大家来看一下<<iOS 与OS X多线程和内存管理>>原书,自己敲一遍实现源码,这样帮助很大,会加深印象.最后感谢各位看官查看本篇文章.如果有任何问题,欢迎联系骚栋.欢迎指导批斗.

    <<iOS 与OS X多线程和内存管理>>的PDF版传送门🚪



    相关文章

      网友评论

      • bc3d3e66fba3:就喜欢你这样分析夹带资源的
      • swjtu_wj:每次你都是<二> <一>呢? <三>呢?
        a5c2ca6e1953:MRC下,block需要自己手动管理,才有了copy,ARC下,不非得使用copy的。
        神经骚栋:@swjtu_wj 不知道一二一,一二一,这样喊整齐?

      本文标题:<<iOS 与OS X多线程和内存管理>>

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