首页 >iOS开发

《iOS 与OS X多线程和内存管理》笔记:Blocks实现(二)

2018-03-13 15:19 编辑: 阿彤木 分类:iOS开发 来源:神经骚栋

前言

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

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

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

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

1396375-3268b46c870de936.png

1396375-e8622c415cab897c.png

  • 然后执行如下的终端命令 clang -rewrite-objc main.m,如下所示.

1396375-36dbb3127f2d4a28.png

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

1396375-0e956688b243fe87.png

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多线程和内存管理》我们可以了解到两个成员变量各包含什么信息.

1396375-da86d3e25b26b51a.png

  • 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函数中调用的源码如图所示.

1396375-4f1ef05d7559f74d.png

书中为了方便大家理解这句代码调用,进行了如下的转换.也就是说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指针.

1396375-901bf0eb361daa76.png

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

1396375-526e1548e1330b44.png

其实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页.

1396375-c230587db691bee7.png

通过比较我们知道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();

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

1396375-955ce704b4390cc7.png

通过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中直接修改变量的值?(面试题).例如下图所示.

1396375-b42c553daf1caf6d.png

这是为什么呢?我们看一下__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指针,我们可以看下面的示意图.

1396375-3ce0a6132a04f725.png

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

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

1396375-d41a2c4a24281030.png

截获对象的实现

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

       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.如下所示,这是为什么呢?

1396375-e2723838b20a1d6c.png

实际上在blk的实现过程中.已经持有了array对象.<>是有以下代码的.

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,已经对应的函数.用于成员变量的持有和释放.如下图所示.

1396375-f5f558a9d378b41f.png

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的修饰符.源码截图如下所示.大家可以自行试验操作.

2.png

循环引用的本质

上一个模块我们说了.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成员变量
};

示意图如下所示.

1396375-71018c880bd890cc.png

结束

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

作者:神经骚栋
链接:https://www.jianshu.com/p/aac7a5ee5359

搜索CocoaChina微信公众号:CocoaChina
微信扫一扫
订阅每日移动开发及APP推广热点资讯
公众号:
CocoaChina
我要投稿   收藏文章
上一篇:Swift多线程:GCD进阶,单例、信号量、任务组
下一篇:iOS 中关于列表滚动流畅方案的一些探讨
我来说两句
发表评论
您还没有登录!请登录注册
所有评论(0

综合评论

相关帖子

sina weixin mail 回到顶部