<>笔记: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:

简书

责编内容by:简书阅读原文】。感谢您的支持!

您可能感兴趣的

iOS-仿膜拜贴纸滚动(物理仿真) 导读 简单用OC写了一个小球滚动效果; 类似平衡球. GitHub地址: https://github.com/wangliujiayou/WLBallView 欢迎Star. 膜拜滚...
The simulator and the device give different result... Have any of you ever ran into issues where the changes you've made are only being reflected on the simulator and not on ...
IOS 6 Read-Only Property Error Assignment I have a Singleton class called Constants and I need to set some app wide constants in there. Id like these constants to...
iOS 解耦、组件化最佳实践 iOS 解藕、组件化最常用的是使用统跳路由的方式,目前比较常用的 iOS 开源路由框架主要是JLRoutes、MGJRouter、HHRouter等,这些路由框架各有优点和缺点,基本可以满足大部分需求。目前最常用来作路由跳转,以实现基本的组...
New Updates to BluePic! We are excited to announce that our team has just made several updates to the BluePic application. For those who are no...