<>笔记:Blocks实现(二)

综合技术 2018-03-07

前言

在 <>笔记: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("Blockn");};
        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("Blockn");
}

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("Blockn");
}

概念函数的参数 __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;
  }   
};

通过<>我们可以了解到两个成员变量各包含什么信息.

  • 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截获自动变量值,在 <>笔记:Blocks 中我们已经说过了,现在我们列举一下例子.来看一下是如何实现截获自动变量值这一过程的.

int number = 1;
        
        void (^blk)(void) = ^{
            printf("value:%dn",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:%dn",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:%dn",number);
}

__block说明符的实现

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

__block int number = 1;
        
        void (^blk)(void) = ^{
            printf("value:%dn",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:%dn",(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:%dn",(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对象.<>是有以下代码的.

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的实现总共写了三天,加上自己验证,售货良多,希望这一篇博客对大家有所帮助.还是希望大家来看一下<>原书,自己敲一遍实现源码,这样帮助很大,会加深印象.最后感谢各位看官查看本篇文章.如果有任何问题,欢迎联系骚栋.欢迎指导批斗.

<>的PDF版传送门:door:

您可能感兴趣的

iOS通过Runtime实现友盟统计 奋斗的七月 在友盟官网可以看到相应的步骤,申请appkey,导入SDK,然后在AppDelegate里面写入相应的代码,下面就是关键的代码: 实现页面的统计需要在每个UIViewController中配对调用如下方法: - (void)viewWillAppe...
【iOS印象】漫谈 iOS App 架构与设计模式... 整理译自: aplus.rs/2017/much-a… 项目可维护的一些原则: 尽可能少写代码 不要试图和 SDK 和工具过不去 采用 iOS 框架中,尤其是 UIKit 中已有的设计模式 ...
Switch iOS Tutorial The switch control is used to control the state of a Boolean object. In this tutorial the state will be changed and this state will also be displayed ...
如何提高代码的可读性 读《编写可读代码的艺术》... 很多同行在编写代码的时候往往只关注一些宏观上的主题:架构,设计模式,数据结构等等,却忽视了一些更细节上的点:比如变量如何命名与使用,控制流的设计,以及注释的写法等等。以上这些细节上的东西可以用 代码的可读性 来概括。 不同于宏观上的架构,设计模式等需要好几个类,好几个模块才能看出来:...
Isolating your Data Layer In a truly layered architecture (MVVM, Viper, etc), the data layer should be relegated to its own layer - every other piece just wants the data. Unfor...