• Blocks


    前言:

      Blocks表现为“带有自动变量(局部变量)的匿名函数”。Blocks的本质是Objective-C的对象。本文主要内容来自《Objective-C高级编程 iOS与OSX多线程和内存管理》学习与探索,从Blocks的表现形式出发,通过Objective-C转换成的C++源码探索Blocks的本质。我们主要探讨了以下几个结论:(1)Block的实质是栈上Block的结构体实例;__block变量的实质是栈上__block变量的结构体实例。(2)Block“截获自动变量值”是Block把所使用的自动变量值被保存到Block的结构体实例(即Block自身)中了。(3)Block变量和__block变量超出其作用域而存在的理由是编译器帮我们把栈上的变量复制到了堆上(有些情况需要我们自己复制)。除此之外,我们还探讨了Block的循环引用等一些使用情况。

    正文:

    一、什么是Blocks?

      Blocks是C语言的扩充功能。可以用一句话来表示Blocks的扩充功能:带有自动变量(局部变量)的匿名函数。Block的使用其实相当于代理,一般是跨越两个类来使用的。比如作为property属性或者作为方法的参数,这样就能跨越两个类了.

      下面的2.1讲解的"Block语法"和2.2讲解的"Block类型变量"可以说明"Blocks是匿名函数"这一特性。而"带有自动变量"在Blocks中的表现为"截获自动变量值",下面的2.3节将会讲解这个特性。

    二、Blocks模式

    2.1  Block语法

      Block语法使用,有如下几种形式:

    //返回值类型+参数列表+表达式
    ^int(int count){return count+1;}
    //参数列表+表达式 ^(int count){return count+1;}
    //表达式 ^{printf("Blocks ");}

      注意上面的举例,省略了返回值类型的block,不一定它的返回值为void。

    2.2  Block类型变量

      跟函数指针类型变量很相像,声明Block类型变量举例如下:

    int(^blk)(int) = ^(int count){return count+1;};

      Block可以作为函数的参数传递,也可以作为函数的返回值。为了使用方便,我们将blk用typedef声明如下:

    typedef int (^blk_t)(int);

      Block类型变量可完全像通常的C语言变量一样使用,因此也可以使用指向Block类型变量的指针,即Block的指针类型变量。

    typedef int (^blk_t)(int);
    blk_t blk = ^(int count){return count+1;};
    blk_t *blkptr = &blk;
    (*blkptr)(10);

    2.3 截获自动变量值

      举一个例子:

    int main(int argc, char * argv[]) {
        int val = 10;
        void(^blk)(void)=^{printf("%d",val)};
        val = 2;
        blk();  // 输出10,而不是2.
    }

      val为10的值被保存(即被截获),从而在执行块时使用。这就是自动变量的截获。

    2.4 __block关键字的使用

      若想在Block语法的表达式中将值赋给在Block语法外声明的自动变量,需要在该自动变量上附加__block说明符,否则会产生编译错误的,后面会讲为什么。

        __block int val = 10;
        void(^blk)(void)=^{val=1;};
        blk();

      如果截获的是Objective-C对象,那么向其赋值也会产生编译错误,必须附加__block说明符:

     __block id array = [[NSMutablearray alloc] init];
      void(^blk)(void)=^{array = [[NSMutablearray alloc] init];}; //如果array没有指定__block说明符,此处就会编译报错.

      但是,对截获的Objective-C对象调用变更该对象的方法是ok的:

     id array = [[NSMutablearray alloc] init];
      void(^blk)(void)=^{
        id obj = [[NSObject alloc] init];
        [array addObject: obj];//array没有指定__block说明符,没有问题.
       }; 

      另外,现在的Blocks中,截获自动变量的方法并没有实现对C语言数组的截获,比如“const char text[] = "hello"; ”,当然,你可以把它声明成指针的形式来解决这个问题“const char *text = "hello"; ”。

    三、Blocks的实现

    3.1 Block的实质(C++源码分析)

      Blocks的实质是Objective-C的对象。

      我们可以用clang(LLVM编译器)来将Objective-C的代码转换成可以理解的C++的源代码,来探索这个问题。例如,我写一个Objective-C的代码文件命名为block_obj.m,然后我就可以通过如下clang命令将其转换为block_obj.cpp文件:

    clang -rewrite-objc block_obj.m

      写一个很简单的Objective-C的Blocks的代码:

    #include <stdio.h>
    int main(){
       void(^blk)(void) = ^{printf("Block
    ");}; // 声明并定义Blocks
       blk();                                    // 执行Blocks函数
       return 0;
    }

      转换后的block_obj.cpp文件中,我们看一下其C++实现相当长,这里我们摘几个跟我们上面这3句Objective-C的代码息息相关的转换来看,恕我不把源码整个贴出来了,只捡需要的来逐步讲解一下。上面的Objective-C的代码直观的转换为下面的C++代码:

    int main(){
       void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); //声明并定义Blocks
       ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); //执行Blocks函数
       return 0; 
    }

      第二句的执行Blocks函数看起来比较复杂,其实是简单的使用函数指针调用函数,它可以简化为下面的句子:

    (*blk->impl.FuncPtr)(blk);

      blk对应的结构体是下面的C++代码:

      //blk对应的结构体
      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;
         }
    };

      对blk结构体中各个成员变量进一步查看其源码如下:

    //blk结构体中构造函数中参数fp指针指向的函数
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) { // __cself相当于C++中的this指针,这里指向Blocks的变量
      printf("Block
    ");
    }
    
    //blk结构体中成员变量impl的数据结构,即Block的结构体
    struct __block_impl {
        void *isa;
        int Flags;
        int Reserved; // 版本升级所需的区域
        void *FuncPtr; // 函数指针
    };
    
    //blk结构体中成员变量Desc的数据结构
    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)};

      Blocks函数对应的变换后源码是上面的__main_block_func_0函数,从变换后的源码来看,通过Blocks使用匿名函数实际上被作为简单的C语言函数来处理的。由Block语法转换的__main_block_func_0函数的指针被赋值成员变量FuncPtr中。__main_block_func_0函数的的参数_cself指向Block值。在调用该函数的源代码中可以看出Block正是作为参数进行了传递。

      到此,我们总算摸清了Block的实质。但还不够,我们要重点理解将Block指针赋给Block的结构体的成员变量isa这句话,才能最终理解为什么Block就是Objective-C对象:

    isa = &_NSConcreteStackBlock;

      首先要理解Objective-C类和对象的实质。从最基本的objc_class结构体谈起,“id”这一变量类型用于存储Objective-C对象,它的声明如下:

    typedef struct objc_object {
        Class *isa;  
    } *id;
    // objc_class就是Class
    typedef struct objc_class *Class;
    struct objc_class {   Class isa; };

      objc_object结构体和objc_class结构体归根结底是在各个对象和类的实现中使用的最基本的结构体。在Objective-C中,各类的结构体就是基于objc_class结构体的class_t结构体:

    struct class_t {
        struct class_t *isa;
        struct class_t *superclass;
        Cache cache;
        IMP *vtable;
        uintptr_t data_NEVER_USE;  
    };

    "Objective-C中由类生成对象"意味着,像该结构体这样"生成由该类生成的对象的结构体的实例"。该实例持有声明的成员变量、方法的名称、方法的实现(即函数指针)、属性以及父类的指针,并被Objective-C运行时库所使用。这就是Objective-C的类与对象的实质。

      再看上面的Block结构体:

    struct __main_block_impl_0 {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
      struct __main_block_desc_0* Desc; 
    };

    此__main_block_impl_0结构体相当于objc_object结构体的Objective-C类对象的结构体,对isa的初始化“isa = &_NSConcreteStackBlock;”, _NSConcreteStackBlock相当于class_t结构体实例,在将Block作为Objective-C的对象处理时,关于该类的信息放置于_NSConcreteStackBlock中。

      至此,就理解了Block的实质,知道Block即为Objective-C的对象了。

    补充isa的知识:

      isa是一个Class类型的指针,每个实例对象有个isa指针,它指向对象的类,而Class里也有个isa指针,指向metaClass(元类),元类保存了类方法的列表。元类也有isa指针,它的isa指针最终指向根元类(root metaClass),根元类的isa指针指向本身,这样形成了一个封闭的内循环。

    (1)每一个对象本质上都是一个类的实例。其中类定义了成员变量和成员方法的列表对象通过对象的isa指针指向类。

    (2)每一个类本质上都是一个对象,类其实是元类(metaClass)的实例。元类定义了类方法的列表类通过类的isa指针指向元类

    (3)所有的元类最终继承一个根元类,根元类isa指针指向本身,形成一个封闭的内循环

    3.2 截获自动变量(C++源码分析)

      Block如何截获自动变量的,我们还是从C++源码进行分析。先写一个简单的截获自动变量的Block程序,如下:

    #include <stdio.h>
    int main(){
        int dmy = 256;
        int val = 10;
        const char *fmt = "val = %d
    ";
        void(^blk)(void) = ^{printf(fmt,val);};
        val = 2;
        fmt = "These value were changed. val = %d
    ";
        blk();
        return 0;
    }

    直观的转换为下面的C++代码:

    int main(){
        int dmy = 256;
        int val = 10;
        const char *fmt = "val = %d
    ";
        void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
        val = 2;
        fmt = "These value were changed. val = %d
    ";
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        return 0;
    }

    和上面3.2节介绍的基本相同,我们直接看blk对应的结构体的C++代码:

    //blk对应的结构体
    struct __main_block_impl_0 {
         struct __block_impl impl;
         struct __main_block_desc_0* Desc;
         const char *fmt;
         int val;
        // 构造函数
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
            impl.isa = &_NSConcreteStackBlock;
            impl.Flags = flags;
            impl.FuncPtr = fp;
            Desc = desc;
      }
    };

    其中,fmt和val是blk中要用到的变量,自动变量被作为成员变量追加到__main_block_impl_0中了,Blocks的自动变量截获只针对Block中使用的自动变量。在构造函数中,我们可以看到自动变量值被截获。对blk结构体中各个成员变量进一步查看其源码如下:

    // blk结构体中构造函数中参数fp指针指向的函数
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        const char *fmt = __cself->fmt; // bound by copy
        int val = __cself->val; // bound by copy
       printf(fmt,val);
    }
    //blk结构体中成员变量impl的数据结构,即Block的结构体 struct __block_impl {   void *isa;   int Flags;   int Reserved;   void *FuncPtr; }; //blk结构体中成员变量Desc的数据结构 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)};

      总的来说,所谓“截获自动变量值”意味着在执行Block语法时,Block语法表达式所使用的自动变量值被保存到Block的结构体实例(即Block自身)中

    3.3 __block说明符(C++源码分析)

       Block仅截获自动变量的值,Block中使用自动变量后,在Block的结构体中重写该自动变量也不会改变原先截获的自动变量。要改变截获的自动变量值,我们要谈一谈“__block说明符”了。先写一个简单的例子:

     int main(){
         __block int val = 10;
         void(^blk)(void) = ^{val = 1;};
     }

      变换后如下:

    int main(){
        __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
        void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
    }

       我们看到,一个简单的__block val变量,变成了结构体实例,把上面的__block val变量整理一下,就是下面的结构体:

    __Block_byref_val_0 val = {
             (void*)0,
             (__Block_byref_val_0 *)&val,  // 指向实例自身
             0, 
             sizeof(__Block_byref_val_0), 10
     };        

      main中的blk对应的结构体和__block val变量对应的结构体如下所示:

    //blk对应的结构体
    struct __main_block_impl_0 {
       struct __block_impl impl;
       struct __main_block_desc_0* Desc;
       __Block_byref_val_0 *val; // by ref ,block变量
       __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    // __block val变量对应的结构体
      struct __Block_byref_val_0 {
           void *__isa;
           __Block_byref_val_0 *__forwarding; // 持有指向该实例自身的指针
           int __flags;
           int __size;
           int val;      // 成员变量val是相当于原自动变量的成员变量
    };

       blk结构体中构造函数中参数fp指针指向的函数,即一个简单的val=1,变成如下的代码:

    // blk结构体中构造函数中参数fp指针指向的函数
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
          __Block_byref_val_0 *val = __cself->val; // bound by ref
         (val->__forwarding->val) = 1;
    }

      认真理解上面这句代码,val是属于__Block_byref_val_0结构体的,__Block_byref_val_0结构体的成员变量__forwarding持有指向该实例自身的指针,最后通过__forwarding访问成员变量val(成员变量val是该实例自身持有的变量,它相当于原自动变量)。

      (1)__block变量即__Block_byref_val_0结构体实例;

      (2)访问__block变量,即(val->__forwarding->val)=1;

      (如下图所示:__Block_byref_val_0结构体中访问__block变量)

    图1 __forwarding的指向

           此处看起来这个__forwarding有点多余,它的存在的意义我们下一节讨论。

       另外,上面两句代码还生成了一些copy相关的代码如下,虽然我们没有进行过block的copy操作,但还是生成了copy的代码,这都是编译器帮助我们做的,下一节也会讲解为什么会需要copy block,以及编译器在哪些情况下会copy block。

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
        _Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
    }
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {
        _Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
    }
    
    //blk结构体中成员变量Desc的数据结构
    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};

      我们看到,编译器帮我们在__main_block_desc_0中增加了成员变量copy和dispose,以及作为指针赋值给成员变量__main_block_copy_0函数和__main_block_dispose_0函数。

    3.4 Block存储域

       本节主要说明:(1)Block超出变量作用域可存在的理由;(2)__block变量的结构体成员变量__forwarding存在的理由。

      首先,通过上面的分析,我们知道:Block的实质是:栈上Block的结构体实例__block变量的实质是:栈上__block变量的结构体实例。我们之前看到Block的类为_NSConcreteStackBlock。实际上Block的类有以下三种形式,它们的内存分配对应如下:

    图2 三种Block类对应的内存分配

      有两种情况,Block是_NSConcreteGlobalBlock类的,即配置在程序的数据区域:(1)Block本身定义在全局区(2)Block不截获自动变量(Block无论定义在哪里,有可能转换后的源码是_NSConcreteStackBlock类,但其实是_NSConcreteGlobalBlock类)。除此之外的Block语法生成的Block为_NSConcreteStackBlock类对象,且设置在栈上。

      而对于栈上的Block,有一个问题,就是如果其所属的变量作用域结束,该Block就会废弃,由于__block变量也配置在栈上,同样的,如果其所属的变量作用域结束,则__block变量也会被废弃。Block提供了将Block和__block变量从栈上复制到堆上来解决这个问题,各种Block的复制效果如下:

    图3 三种Block类复制的效果

       而__block变量用结构体成员变量__forwarding可以实现无论_block变量配置在栈上还是堆上时都能够正确地访问__block变量。__forwarding的终极解释:__block 变量从栈复制到堆上时,会将成员变量__forwarding的值替换为复制目标堆上的__block变量用结构体实例的地址。在栈上和复制到堆上的__forwarding的指向可以用下图表示:


    图4 __forwarding复制前和复制后的指向

      这时,我们再看一眼刚开始讲的Block截获自动变量的问题,就比较清楚为什么blk中的val自动变量不会跟随栈上的val在变动了:

      当ARC有效时,大多数情形下编译器会恰当的进行判断,自动生成将Block从栈上复制到堆上的代码。编译器有时候会自动帮我们复制,有时候不会帮我们复制,如果我们不手动调用copy方法的话,就会带来一些问题。那么,什么情况下需要手动copy Block,什么情况不需要?

      不需要我们手动复制Block的情况

      (1)Cocoa框架的方法且方法名中含有usingBlock等时。比如在使用NSArray类的enumerateObjectsUsingBlock实例方法,不用手动复制Block。

      (2)GCD的API。比如在使用dispatch_async函数时,不用手动复制Block。

      需要手动复制Block的情况:向方法或函数的参数中传递Block时。比如,在NSArray类的initWithObjects实例方法上传递Block时,需要手动复制,举例如下:

    -(id)getBlockArray {
      int val = 10;
      return [[NSArray alloc] initWithObjects:
        [^{NSLog(@"blk0:%d",val);} copy],  //如果不进行copy的话,取出来用时就会发生异常
        [^{NSLog(@"blk1:%d",val);} copy],nil];   
    }

      除了以上讲到的需要手动复制Block的情况和不需要手动复制Block的情况,我们从之前的三种Block复制效果可以知道,无论Block是在栈上、堆上,还是在程序的数据区域,用copy方法复制都不会引起任何问题。在不确定时调用copy方法即可。比如,我们常声明blk类型的属性如下:

    typedef void (^blk_t)(void);
    @property (nonatomic, copy) blk_t blk;

    3.5 __block变量存储域

      上节对Block复制的情况进行了说明,那么复制时__block变量的情况是怎么样的呢?

    图5 Block中使用__block变量的复制情况

      图5是一个Block使用__block变量时的复制情况,其实多个Block变量使用一个__block的情况也类似。其思考方式与Objective-C的引用计数式内存管理完全相同。使用__block变量的Block持有__block变量。如果Block被废弃,它所持有的__block变量也就被释放

    3.6 截获对象

      参考3.3节的代码:

     int main(){
         __block int val = 10;
         void(^blk)(void) = ^{val = 1;};
     }

      我们看看转换后的C++源码中的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->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
    }
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {
        _Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
    }
    
    //blk结构体中成员变量Desc的数据结构
    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};

      __main_block_copy_0函数会将val变量复制给Block用结构体的成员变量val中并持有该对象__main_block_dispose_0函数调用相当于release实例方法的函数,释放赋值在对象类型的结构体成员变量中的对象。copy函数和dispose函数调用时机如下:

    那么,什么时候栈上的Block会复制到堆上呢?有以下4种情况:

    (1)调用Block的copy实例方法时;  

    (2)Block作为函数返回值返回时;

    (3)将Block赋值给类的附有__strong修饰符的id类型或Block类型成员变量时;

    (4)向方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时。

    对于__block变量和__block对象,总结如下:copy函数持有截获的对象、变量,dispose函数释放截获的对象、变量

    3.7 Block循环引用

      首先看一段代码:

       @weakify(self);
        [[MTANetworkManager instance] addResMiddleware:^(id object, MTAMiddlewareNextBlock next) {
            @strongify(self);
                [self refreshTokenWithTask:task];
        }];

      如果不写上面的@weakify,@strongify关键字,会存在下面左边的循环引用,使用了关键字之后,我们打破了这个循环引用,如下所示:

      上面的代码中,self拥有block,block中又使用了self,因此需要使用@weakify(self)和@strongify(self)来避免循环引用。原理:

      After @strongify is called, self will have a different pointer address inside the block than it will outside the block. That's because @strongify declares a new local variable called self each time. (This is why it suppresses the -Wshadow warning, which will “warn whenever a local variable shadows another local variable.”) It's worth reading and understanding the implementation of these functions. So even though the names are the same, treat them as separate strong references. However, remember that after your first use of @strongifyself will refer to local, stack variables

     结论:

      通过上面的分析,我们主要探讨了以下结论:

    (1)Block的实质是栈上Block的结构体实例;__block变量的实质是栈上__block变量的结构体实例。

    (2)Block“截获自动变量值”是Block把所使用的自动变量值被保存到Block的结构体实例(即Block自身)中了。

    (3)Block变量和__block变量超出其作用域而存在的理由是编译器帮我们把栈上的变量复制到了堆上(有些情况需要我们自己复制)。

    (4)__block变量存在的理由是:__forwarding可以实现无论_block变量配置在栈上还是堆上时都能够正确地访问__block变量。

  • 相关阅读:
    【记录】Excel 中VLOOPUP 使用心得
    【记录】Mybatis-plus中Page插件 快速进行分页操作
    【记录】mybatis-plus 更新字段的三种策略解析
    Instant Client连接数据库
    python3安装沙盒环境
    redis配置哨兵模式
    redis主从配置
    mongodb4.2主从(副本集附仲裁节点)部署带认证模式
    主从数据不一致导出同步错误(主库删除记录,从库不存在)
    批量执行redis命令
  • 原文地址:https://www.cnblogs.com/Xylophone/p/6229909.html
Copyright © 2020-2023  润新知