美文网首页workiOSblock相关
《OC高级编程》笔记2——block的使用和实质探究

《OC高级编程》笔记2——block的使用和实质探究

作者: Wang66 | 来源:发表于2016-04-01 18:42 被阅读1048次

    block的使用


    block是什么

    ** block就是可以截获局部变量的匿名函数。**

    解释一下:** block可以获取被定义时词法范围内的状态(比如局部变量等),并且在一定条件下(比如使用__block变量)可以修改这些状态。 **
    比如,在某方法中的一个block,是可以获取到该方法内的变量的。

    block的语法
    block语法.jpg

    比如下面定义了一个:名为addBlock,参数列表是两个int型的数据,返回值也为int型的block。

        int (^addBlock)(int, int) = ^(int x, int y){
            return x + y;
        };
        int result = addBlock(2,4); // 传入实参,执行该block,返回了int型的结果。
    

    block和其他变量一样都可以局部变量,全局变量,静态变量,甚至方法参数等。
    block既然是种变量,那它也就有自己所属的类型。决定一个block是什么类型的因素是返回值和参数。int (^addBlock)(int, int)代表返回值为int型,两个int型参数,名为addBlock。但这样表示block有点不太好。其一是阅读起来不太顺畅,其二是若我们要重构或者修改原来定义的block,则要在每个使用该block的地方进行手工修改。所以我们可以统一在一个地方对其进行类型再定义

    // 把返回值为void型,俩int型参数的block统一再定义为MyBlock类型。
    typedef void (^MyBlock)(int);
    
      ...
        MyBlock myBlock = ^(int x){
            NSLog(@"myBlock:rereult = %d", x);
        };
    
    // 或者block作为方法参数时
    - (void)doSomething:(MyBlock)myBlock param:(int)count
    {
        // 调用myBlock
        myBlock(count);
    }
    

    注意:block的语法本身就比较怪异,再加上:定义block时(^blockName)括号里面的是block名字,但是通过typedef进行类型再定义时(^blockClass)括号里表示代表该block的类型名。总之,block的语法比较别扭,别记错了。

    截获局部变量

    开头我们说了block是可以截获局部变量的匿名函数。也就是说在某方法内的block是可以获取该方法定义的局部变量的。** 而且是只读的,不可以对其进行修改操作。若非要进行修改,则得在局部变量前加上__block修饰符。**下面用三小段代码分别来验证:

    // block内可以读取局部变量
    
        int count = 10;
    
        void (^countBlock1)(void) = ^(void){
            NSLog(@"count----%d",count);
        };
        
        countBlock1();
    
    // BlockWang[1534:689473] count----10
    
    // 试图在block内修改局部变量
    
        int count = 10;
        
        void (^countBlock1)(void) = ^(void){
            count++;
        };
        
        countBlock1();
    

    上面这段代码编译时会报错:


    试图在block内修改局部变量编译时报错.png
    // 在局部变量前加上__block修饰符,后就可以在block内部修改此局部变量了
    
        __block int count = 10;
        
        void (^countBlock1)(void) = ^(void){
            NSLog(@"count----%d",++count);
        };
        
        countBlock1();
    
    // BlockWang[1534:689473] count----11
    

    需要小心下面这段代码:我们在定义一个block后再修改了count值为2,然后再执行该block。执行的打印结果是count----10,这就说明block“截获局部变量”的处理是在定义这个block时,而且似乎所谓“截获局部变量”就是在block中有了个和count相应的独立的数据,不然我们当修改count值时,为什么打印出的block内的该变量没变化呢?这个疑问在后面block的实现中我们慢慢分析。

        int count = 10;
        
        void (^countBlock)(void) = ^(void){
            NSLog(@"count----%d",count);
        };
        
        count = 2;
        countBlock(); // 执行block
    
    // BlockWang[1534:689473] count----10
    

    block的实质


    接下来我们会把代码通过Clang命令转换为中间代码来观察block的实现,探索它的本质。

    block的实现结构:

    首先我们研究只打印字符串的,最简单的block:

    #include "BlockClang.h"
    
    int main()
    {
        void (^myBlock)(void) = ^(void){
            printf("this is a block");
        };
        
        myBlock();
        
        return 0;
    }
    

    打开终端,进入项目路径,然后敲入Clang的命令clang -rewrite-objc BlockClang.c。此时,Finder里多了个文件BlockClang.cpp,它正是转换后的中间代码。
    小小的一段代码转换为BlockClang.cpp后竟然有超500多行,我们只提取出对我们有意义的部分:

    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("this is a block");
     }
    
    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()
    {
     void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    
     ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    
     return 0;
    }
    

    我们可以看到,block的结构实现是结构体,其中__main_block_impl_0结构体代表block的结构。它有一个__block_impl类型的impl成员和__main_block_desc_0 *类型的成员Desc(顾名思义,它俩分别代表block的实现和描述信息)。以及一个构造函数__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)通过该构造函数,分别给block的成员赋值。那block的两个成员变量的结构又是怎么样的,它们里面都有哪些成员呢?

    // __block_impld结构体的结构
    
    struct __block_impl {
      void *isa; // block的类型
      int Flags; // 标志位
      int Reserved; // 保留位
      void *FuncPtr; // block的实现,函数指针,指向__main_block_func_0
    };
    
    // __main_block_desc_0结构体的结构
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size; // block的大小
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
    

    然后就是__main_block_desc_0函数,即block的实现体。该函数接受一个__cself参数,即对应的block自身。(** 思考:为什么要传一个自身作为参数? **)

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
      printf("this is a block");
     }
    

    最后看main函数里block的实现和调用:

     void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    
     ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    

    可以看出执行block就是调用一个以block自身作为参数的函数,这个函数对应着block的执行体。

    block为什么能截获局部变量?

    我们来看个截获局部变量的block,并转换为中间代码,观察代码,以尝试解答这个问题。

    int main()
    {
        int count = 10;
        void (^myBlock)(void) = ^(void){
            printf("count = %d", count);
        };
        
        myBlock();
        
        return 0;
    }
    

    转换后的代码。只列出发生了变化的代码:
    可以看到__main_block_impl_0结构体中多了count这个成员变量。并且构造函数的参数中也多了count这一项。

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int count;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _count, int flags=0) : count(_count) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    可以看到block的实现体中__main_block_func_0多了int count = __cself->count;这一句。
    ** block之所以可以截获局部变量就是因为__cself访问了该block里面的count成员变量,而block的count成员的值是在实现该block时赋得的。** 此时,前面我们的疑问:这个函数“为什么要传一个自身作为参数?的问题也迎刃而解,不言而喻了。”之所以该方法要传代表block结构的__main_block_impl_0结构体为参数,就是为了读取该block捕获的局部变量。

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int count = __cself->count; // bound by copy
    
      printf("count = %d", count);
     }
    
    int main()
    {
     int count = 10;
     void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count));
    
     ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    
     return 0;
    }
    
    block为什么只能读取局部变量,而不能修改局部变量呢?

    因为main函数中的局部变量count和函数__main_block_func_0不在同一个作用域中,调用过程中只是进行了值传递。当然,在上面代码中,我们可以通过指针来实现局部变量的修改。不过这是由于在调用__main_block_func_0时,main函数栈还没展开完成,变量count还在栈中。但是在很多情况下,block是作为参数传递以供后续回调执行的。通常在这些情况下,block被执行时,定义时所在的函数栈已经被展开,局部变量已经不在栈中了。(不过既然如此,我们可以推断出静态局部变量之所以可以在block修改就是通过——指针。因为静态局部变量存在于内存数据段,不存在栈展开后非法访存的风险。见下一段。)
    所以,对于auto类型的局部变量,不允许block进行修改是合理的。

    block为什么可以又可以修改静态变量和全局变量呢?

    因为它们不存在栈展开后非法访存的风险。所以可以通过** 指针 ** 来传递静态变量的。
    可以看出静态变量在main内实现block时,捕获的是count的地址&count。以及在__main_block_impl_0结构体中成员变量变成了指针类型int *count;。即通过指针修改(它们是址传递)。

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int *count;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_count, int flags=0) : count(_count) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int *count = __cself->count; // bound by copy
    
      printf("count = %d", ++(*count));
     }
    
    int main()
    {
     static int count = 10;
     void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &count));
    
     ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    
     return 0;
    }
    
    为什么被__block修饰的局部变量在block中却又是可以修改的?

    我们来一段局部变量前加了__block的代码例子:

    #include "BlockClang.h"
    
    int main()
    {
        __block int count = 10;
        void (^myBlock)(void) = ^(void){
            printf("count = %d",++count);
        };
        
        myBlock();
    }
    

    转换中间代码后,看到比以前多了很多东西。

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_count_0 *count; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_count_0 *_count, int flags=0) : count(_count->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    
    
    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    
    
    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};
    
    
    
    struct __Block_byref_count_0 {
      void *__isa;
    __Block_byref_count_0 *__forwarding;
     int __flags;
     int __size;
     int count;
    };
    
    
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_count_0 *count = __cself->count; // bound by ref
    
      printf("count = %d",++(count->__forwarding->count));
     }
    
    
    
    
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->count, (void*)src->count, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->count, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    
    
    int main()
    {
     __attribute__((__blocks__(byref))) __Block_byref_count_0 count = {(void*)0,(__Block_byref_count_0 *)&count, 0, sizeof(__Block_byref_count_0), 10};
     void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_count_0 *)&count, 570425344));
    
     ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    }
    

    可以看到__main_block_impl_0结构体成员变量count变为了__Block_byref_count_0 *类型。而相应的__main_block_func_0函数中count也变为了__Block_byref_count_0 *类型。

    __Block_byref_count_0也是一个结构体。它的构成是:

    struct __Block_byref_count_0 {
      void *__isa;
    __Block_byref_count_0 *__forwarding; // 指向另外一个变量,这儿的具体实现思路不太懂
     int __flags;
     int __size;
     int count;
    };
    
    

    ** 但是问题照样存在,我们修改的变量count它是位于栈上的。若当block被回调执行时,栈早已被展开,早没count了。这该如何是好?**

    上面的代码中我们可以注意到:__main_block_desc_0函数中多了两个成员函数,分别指向__main_block_copy_0__main_block_dispose_0函数。

    当block从栈上被copy到堆上时,会调用__main_block_copy_0__block类型的成员变量count从栈上复制到堆上;而当block被释放时,相应地会调用__main_block_dispose_0来释放__block类型的成员变量i。
    一会在栈上,一会在堆上,那如果栈上和堆上同时对该变量进行操作,怎么办?
    这时候,__forwarding的作用就体现出来了:当一个__block变量从栈上被复制到堆上时,栈上的那个__Block_byref_i_0结构体中的__forwarding指针也会指向堆上的结构。


    资料参考:

    iOS中block实现的探究
    C语言中闭包的探究及比较
    C语言中闭包的探究及比较
    对Objective-C中Block的追探
    谈Objective-C block的实现

    相关文章

      网友评论

      本文标题:《OC高级编程》笔记2——block的使用和实质探究

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