刨根问底,Block竟然是?

作者: 郑明明 | 来源:发表于2017-09-17 10:42 被阅读525次

    对于iOS开发者来说,Block就像一件哆啦A梦口袋中的宝贝,帮助我们简化代码,实现功能。但是哆啦A梦这部动画片中并没有人好奇这些宝贝是如何实现的,但是作为程序猿,应该要学会刨根问底,了解本质,本文,就深入浅出地讲解Block的实现。

    初步了解Block

    block是基于C语言的扩展功能。block有一个比较常见的说法,叫做:带有自动变量的匿名函数,第一眼看上去有些陌生,我们抓住关键字来理解一下,首先匿名函数,即为没有名字的函数,我们尝试用函数指针来实现一下

    int function(int a) {
      cout << a << endl;
    }
    int (* functionPointer) (int) = &function;
    int result = (*function)(19);
    

    注意看最后一行,我们通过函数指针去调用了函数,并不知道函数名。接着还有关键字是带有自动变量,这里的自动变量也就是局部变量,到这里,我们来看看block语法:
    ^ 返回值类型 参数列表 表达式
    举个例子看看

    ^ int (int a) { return a * a; };
    

    同样我们像之前函数指针的赋值一样,将Block语法赋值给声明为Block类型的变量中

    int (^blk) (int) = ^ int (int a) { return a * a; };
    

    调用则为:

    blk(19);
    

    通过对比赋值匿名函数和赋值Block类型变量可以发现,两者的写法即为相似,区别在于block中为^符号,而函数为*符号
    这里可能大家有个疑惑,那截获自动变量是什么意思呢,我们带着这个问题继续往下。

    截获自动变量

    我们先从下面一个场景入手

    int value = 0;
    void (^blk) (void) = ^{ printf("%d", value); };
    value = 19;
    blk();
    

    对block有了解的人都知道,以上输出应该是0,而不是19,在表面上大家就可以理解为block截获了变量的瞬间值

    一、通过源码来究其本质
    int main(int argc, const char * argv[]) {
    
        void(^blk)(void) = ^ {
            printf("%d", 19);
        };
        blk();
        return 0;
    
    }
    

    我们使用clang -rewrite-objc指令来将以上代码解析为C++源代码:具体转化的代码如下(这段源码看上去很复杂,但却是我们理解block本地的必经之路

    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("%d", 19);
        }
    
    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[]) {
    
        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;
    }
    

    我们从main函数入手,看看到底发生了什么变化,我们将main函数中类型转换的操作去掉,简化后的代码如下:

    int main(int argc, const char * argv[]) {
        void(*blk)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
    }
    

    进一步分解为:

    int main(int argc, const char * argv[]) {
        struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0, &_main_block_desc_0_DATA);
        struct __main_block_impl_0 *blk = temp;
    }
    

    从上面可以理解为,编译之后的block是结构体类型的,声明的blk是一个指向结构体类型block的指针。
    了解了基本的之后,再来看其中所涉及到的结构体类型是如何定义的,我们从__main_block_impl_0开始(这个结构体名的命名规则是:Block所属的函数+Blcok语法在该函数出现的顺序):

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

    该结构体中定义了两个成员,implDesc,同时拥有一个结构体初始化方法(main函数中调用的方法),其中对impl的几个成员变量赋值,同时赋值了Desc
    再来看看成员之一的__block_impl结构体是如何定义的:

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    

    我们主要关注这个几个成员:

    • *isa:指向类的指针
    • *FuncPtr:在main函数中初始化时我们可以注意到是将__main_block_func_0(一个静态函数)赋值给了它

    接着再来看看另外一个成员__main_block_desc_0是如何定义的:

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

    从main函数中初始化可以看出将__main_block_des_0类型的变量__main_block_desc_0_DATA传入赋值

    基本了解了各个部分之后,我们先进行一下总结:

    • 声明block:创建了一个__main_block_imp_0类型的结构体,并用一个该类型的指针指向这个结构体
    • 使用block:调用了结构体中的成员__block_implFuncPtr方法
    二、截获自动变量的源代码分析

    通过刚才的理解,大致知道了编译之后的代码构成,但是上述代码并没有截获自动变量,我们又重新写了一份截获自动变量的代码:

    int main(int argc, const char * argv[]) {
    
        int value = 3;
        void(^blk)(void) = ^ {
            printf("%d", value);
        };
        blk();
            
        return 0;
    }
    

    并且继续使用clang将其编译成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 value;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _value, int flags=0) : value(_value) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int value = __cself->value; // bound by copy
    
            printf("%d", value);
        }
    
    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[]) {
    
        int value = 3;
        void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, value));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    
        return 0;
    }
    

    此时,我们只要看一下捕获自动变量和没有捕获自动变量时的区别就能够理解它的实现原理。
    我们仍然从main函数入手,可以发现唯一发生变化的是:__main_block_impl_0结构体初始化参数多了一个value,这个value也正是我们block截获的自动变量。
    那么我们就再来看看__main_block_impl_0结构体定义中出现了什么变化

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int value; // 1
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _value, int flags=0) : value(_value) { // 2
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    从上面的标记可以看出,有两个地方存在不同

    1. 结构体中存在一个int类型的成员(和截获值相同类型和名字)
    2. 初始化方法中将传入值赋值给该成员变量

    我们返回到main函数中初始化方法中看传入值是被截获的值。

    接着我们来看静态方法__main_block_func_0的区别:

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

    我们可以发现,参数中的*_cself指针终于派上用场了,这个参数是在main函数中调用该静态方法的时候传入的

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

    参数为blk,前面也提到了blk是一个指向结构体的指针。
    可以发现,该静态方法中操作的实际上是结构体本身的成员,而不是block之外的value变量。

    到了这里,我们又该进行一次总结了:
    截获自动变量,从本质上是在结构体内部增加了一个类型和名字一样的成员,并且赋值,从而block操作的都是成员。

    三、使用__block修饰符的源码分析

    以上我们分析了没有捕获自动变量的情况以及捕获自动变量的情况,在捕获自动变量时,如果我们修改了捕获的值,就会报错

    Variable is not assignable(missing,__block type specifier)
    

    因为在底层实现上,不允许改变结构体自己的内部成员值,这时,我们就需要加上__block修饰符。
    我们写以下代码,来实现对捕获的自动变量修改:

    int main(int argc, const char * argv[]) {
    
        __block int value = 3;
        void(^blk)(void) = ^ {
            value = 19;
        };
        blk();
            
        return 0;
    }
    

    同样地,我们通过clang来编程成C++代码:

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    struct __Block_byref_value_0 {
      void *__isa;
    __Block_byref_value_0 *__forwarding;
     int __flags;
     int __size;
     int value;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_value_0 *value; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_value_0 *_value, int flags=0) : value(_value->__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_value_0 *value = __cself->value; // bound by ref
    
            (value->__forwarding->value) = 19;
        }
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->value, (void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->value, 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[]) {
    
        __attribute__((__blocks__(byref))) __Block_byref_value_0 value = {(void*)0,(__Block_byref_value_0 *)&value, 0, sizeof(__Block_byref_value_0), 3};
        void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_value_0 *)&value, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    
        return 0;
    }
    

    可以发现,仅仅是多了一个__block修饰符,就多了这么多代码,那么让我们来研究一下,苹果是如何通过__block来实现修改截获的自动变量的呢。
    这一次我们不从main函数入手,我们从上往下看发现了一个新的结构体类型__Block_byref_value_0,我们来看看这是什么:

    struct __Block_byref_value_0 {
      void *__isa; // 1
    __Block_byref_value_0 *__forwarding; // 2
     int __flags;
     int __size;
     int value; // 3
    };
    

    从上面的三处标记来看:

    1. 是一个指向类的指针,之前__block_imp中也存在这个指针
    2. __forwarding是一个指向自己的指针
    3. 被截获的自动变量作为了这个结构体的成员

    看到这里好像有点明白,苹果将用__block修饰的变量转变成了一个结构体,并在其中持有截获的自动变量。

    我们继续往下看,发现__main_block_impl_0中也存在不同,具体如下:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_value_0 *value; // by ref // 1
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_value_0 *_value, int flags=0) : value(_value->__forwarding) { // 2
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    可以发现两有个地方不一样:

    1. 成员变量变成了刚刚的__Block_byref_value_0类型的指针,名字仍然和截获的自动变量名一致
    2. 初始化的时候将传入对象的__forwarding指针赋值给了成员变量的指针

    我们继续看main函数中是如何初始化的:

    int main(int argc, const char * argv[]) {
    
        __attribute__((__blocks__(byref))) __Block_byref_value_0 value = {(void*)0,(__Block_byref_value_0 *)&value, 0, sizeof(__Block_byref_value_0), 3};
        void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_value_0 *)&value, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    
        return 0;
    }
    

    我们仍然采用简化的方式来看看逻辑:

    int main(int argc, const char * argv[]) {
        // 1
        __Block_byref_value_0 value = {
            0,
            &value,
            0,
            sizeof(__Block_byref_value_0), 
            3
        };
        struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0, &_main_block_desc_0_DATA, &value, 570425344); // 2
        struct __main_block_impl_0 *blk = temp;
        blk->FuncPtr(blk);
        return 0;
    }
    

    通过标记中的两点:

    1. __block修饰的变量被编译成了一个结构体变量,并进行了初始化,对照结构体参数可以发现,第二个参数赋值了自身的引用,最后一个参数赋值了截获的自动变量的值。
    2. __main_block_impl_0的初始化参数中,传入了value的引用

    那值是如何改变的呢,我们去看一下调用的这个静态方法的实现:

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_value_0 *value = __cself->value; // bound by ref
    
            (value->__forwarding->value) = 19;
        }
    

    可以发现改变的操作是,利用成员变量value,调用指向自己的指针__forwarding,然后访问成员value的值,从而进行改变。
    这里可能大家都有个疑问,为什么不直接访问value就行了,还要先调用指向自己的指针,这岂不是多此一举。
    其实不然,苹果这样设计是有它自己的目的,本文打算不做扩展,我们还是围绕这主题继续进行下去。

    除了以上不同之外,还可以发现多了两个方法:

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->value, (void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);}
    

    外层的方法名叫做

    1. __main_block_copy_0
    2. __main_block_dispose_0

    内层分别调用

    1. _Block_object_assign
    2. _Block_object_dispose

    本文中,我们仅仅知道这两个方法是帮助我们自动进行__Block_byref_value_0结构体变量内存管理的。

    总结

    本文主要以block源码分析为主,重点理解了block是如何捕获自动变量的,又是如何修改自动变量的。但是关于block,还有很多精妙的设计和知识点,需要我们继续探究。

    参考资料
    • 《Objective-C高级编程(iOS与OS X多线程和内存管理)》
    • 官方文档

    相关文章

      网友评论

        本文标题:刨根问底,Block竟然是?

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