Objective-C 之Block(2)

287 阅读8分钟

Block的实质

Block是“带有自动变量值的匿名函数”。

通过clang -rewirte-objc 源代码文件名就能将含有Block语法的源代码变换为cpp的源代码。

int main(int argc, char * argv[]) {
    
    void (^blk)(void) = ^{printf("Block");};
    blk();
    return 0;
    
}

变换后截取其中的代码逻辑部分如下:

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("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(int argc, 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;

}

首先为:

^{printf("Block");};

变换后为:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) 
{
	printf("Block");
}

通过Block使用的匿名函数实际上被作为简单的C语言函数来处理。 另外,根据Blocl语法所属的函数名(此处为main)和该Block语法在函数出现的顺序值(此处为0)来给经clang变换的函数命名。 该函数的参数__cself相当于C++实例方法中志向实例自身的变量this,或者OC中的self,即参数__cself为志向Block值的变量。

这个方法中参数的声明为:

struct __main_block_impl_0 *__cself

参数__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;
  }
};

去除构造函数的部分为:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
};

再看__block_impl结构体的声明:

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

里面包含某些标志、今后版本升级所需的区域以及函数指针。 第二个成员变量是Desc指针,以下为__main_block_desc_0结构体的声明。

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
}

其结构为今后版本升级所需要的区域和Block的大小。

再看__main_block_impl_0结构体的构造函数:

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

以上就是初始化__main_block_impl_0结构体成员的源代码。

再看看main函数中构造函数的调用如下:

void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

去掉转换部分来看如下:

__main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0,&__main_block_imp_0_DATA);

struct __main_block_impl_0 *blk = &temp;

该源代码将__main_block_impl_0结构体的自动变量,即栈上生成的__main_block_impl_0结构体实例的指针,赋值给__main_block_impl_0结构体指针类型的变量blk。

以下这句代码代表最初的打印代码:

void (^blk)(void) = ^{printf("Block");};

将打印的Block块赋给Block类型变量blk,相当于将__main_block_impl_0结构体的指针赋值给变量blk。打印的代码块就是__main_block_impl_0结构体类型的自动变量,即栈上生成的__main_block_impl_0的结构体实例。

再来看__main_block_impl_0实例的构造方法参数:

__main_block_impl_0(__main_block_func_0,&__main_block_imp_0_DATA);

第一个参数是由Block语法转换C语言函数指针。第二个参数是作为静态全局变量初始化的__main_block_desc_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)};

由上部分代码可以,该源代码使用Block,即__main_block_impl_0结构体实例的大小,进行初始化。

再来看__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;
  }
};

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

以上,__main_block_impl_0结构体等同于,结构体构造函数会如下进行初始化:

struct __main_block_impl_0 {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
  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();

就可以变换为以下代码:

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

去掉转换部分后:

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

这是简单地使用函数指针调用函数。由打印代码块转换的__main_block_func_0函数的指针被赋值到结构体的成员变量FuncPtr中,也说明了__main_block_func_0的参数__cself指向Block值。

但是impl.isa = &_NSConcreteStackBlock;,将Block指针赋值给Block结构体成员变量isa。

注:isa为何物?

引用简书作者曲年_《Objective-C isa 指针 与 runtime 机制》_一文中解释如下

在Objective-C中,任何类的定义都是对象。类和类的实例(对象)没有任何本质上的区别。任何对象都有isa指针。

那么什么是类呢?在xcode中用快捷键Shift+Cmd+O 打开文件objc.h 能看到类的定义:

#if !OBJC_TYPES_DEFINED
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

可以看出: Class是一个objc_class结构类型的指针,id是一个objc_object结构类型的指针。

objc_class结构体的定义如下:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

各参数含义如下:

  • isa:是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass).根元类的isa指针指向本身,这样形成了一个封闭的内循环。
  • super_class:父类,如果该类已经是最顶层的根类,那么它为NULL。
  • version:类的版本信息,默认为0
  • info:供运行期使用的一些位标识。
  • instance_size:该类的实例变量大小
  • ivars:成员变量的数组

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

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

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

objc_class结构体与objc_object结构体相同。但是,objc_object结构题是各个对象在实现中使用的最基本的结构体,objc_class是类在视线中使用的最基本的结构体。

例:

@interface MyObject:NSObject
{
	int val0;
	int val1;
}

基于objc_object结构体,该类的对象的结构体如下:

struct MyObject{
	Class isa;
	int val0;
	int val1;
}

MyObject类的实例变量val0和val1被直接声明为对象的结构体成员。 OC中由各类生成对象意味着,像该结构体这样“生成由该类生成的对象的结构体实例”。生成的各个对象,即由该类生成的对象的各个结构体实例,通过成员变量isa保持该类的结构体实例指针。


截获自动变量值

int main(int argc, char * argv[]) {
    
    int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    void (^blk)(void)=^{
        printf(fmt,val);
    };
    
    val = 2;
    fmt = "These values were changed.val=%d\n";
    
    blk();
    
    return 0;
}

通过clang -rewrite-objc转换后的代码如下:

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

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, char * argv[]) {

    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 values were changed.val=%d\n";

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

    return 0;

}

有区别的部分仅仅在于以下部分:将变量作为成员变量追加到了__main_block_impl_0 结构体中。

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

__main_block_impl_0结构体内声明的成员变量了行于自动变量类型完全相同。但是Block语法表达式中没有使用的自动变量不会被追加。Blocks的自动变量截获只针对Block中使用的自动变量。

在初始化结构体实例是,根据传递给构造函数的参数对由自动变量追加的成员变量进行初始化。

__main_block_impl_0初始化的代码过程总结来说就是以下赋值过程:

impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = &__main_block_desc_0_DATA;
val = 10;
fmt = "val = %d\n";

由此可知,在__main_block_impl_0结构体实例中(即打印代码块),自动变量被截获。

再来看其中匿名函数代码块:

^{
	printf(fmt,val);
};
    

可以转换为:

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

在转换后的代码中,截获到__main_block_impl_0结构体实例的成员变量上的自动变量,这些变量在Block语法表达式之前被声明定义。因此,原来的源代码表达式无需改动遍可以使用截获的自动变量值执行。

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