Blocks

前言:git

  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的循环引用等一些使用状况。github

正文:编程

1、什么是Blocks?数组

  Blocks是C语言的扩充功能。能够用一句话来表示Blocks的扩充功能:带有自动变量(局部变量)的匿名函数。Block的使用其实至关于代理,通常是跨越两个类来使用的。好比做为property属性或者做为方法的参数,这样就能跨越两个类了.数据结构

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

2、Blocks模式框架

2.1  Block语法jsp

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

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

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

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"; ”。

3、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\n");}; // 声明并定义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\n");
}

//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\n";
    void(^blk)(void) = ^{printf(fmt,val);};
    val = 2;
    fmt = "These value were changed. val = %d\n";
    blk();
    return 0;
}

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

int main(){
    int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    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\n";
    ((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变量。

相关文章
相关标签/搜索