objective-c高级编程(ios与osx多线程和内存管理)(Blocks-2)

前言:

以下文字都是摘自ios与osx多线程和内存管理一书,方便以后自己过来查阅的.
本文梗概:Block的实质:Block究竟是什么?本节将通过Block的实现进一步帮大家加深理解。。

1. Blocks的实现:


1.1 Block的实质:

在实际编译时无法转换成我们能够理解的代码,但clang(LLVM编译器)具有转换为我们可读代码的功能。通过”-rewrite-objc“选项就能讲含有Block语法的代码变换为C++代码,其实也仅是使用了struct结构,本质是c语言代码。

clang -rewrite-objc 源代码文件名

转换Block语法。

int main()
{
    void (^blk)(void) = ^{printf("Block\n");};

    blk();

    return 0;
}

该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("Block\n");
}

static struct __main_block_desc_0 {
    unsigned long reserved;
    unsigned long Block_size;
} __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0)
};

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

    ((void (*)(struct __block_impl *))(
        (struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
        return 0;
}

8行的代码居然增加到了43行。
下面,将源码分成几个部分逐步理解。首先看最初源码中的Block语法。

^{printf("Block\n");};

变换后的代码:

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

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

遗憾的是,由这次Block语法变换而来的__main_block_func_0函数并没有使用参数__cself.使用参数__cself的例子将在后面介绍。我们先来看看该参数的声明。

struct __main_block_impl_0 *__cself

与c++的this和oc的self相同,参数__cself__main_block_impl_0结构体的指针。
该结构体声明如下:

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

由于转换后的代码中,也一并写入了其构造函数,所以看起来复杂。如果去除构造函数,就变的简单。
第一个成员变量是impl,先看其__block_impl结构体的声明。

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

我们从其名称可以联想到某些标志,今后版本升级所需要的区域以及函数指针。(这些会在后面详细说明)。
第二个成员变量是Desc指针,以下为其__main_block_desc_0结构体的声明。

struct __main_block_desc_0 {
    unsigned long reserved;
    unsigned long Block_size;
}

这些也如同其成员名称所示,其结构为今后版本升级所需的区域和Block的大小。
那么,下面我们来看看初始化含有这些结构体的__main_block_impl_0结构体的构造函数。(初始化__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;
    }

用于初始化__block_impl结构体的isa成员的_NSConcreteStackBlock,在我们讲解它之前,先看看该构造函数的调用。

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

因为转换较多,看起来不是很清楚,所以我们去掉转换的部分。如下:

struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

struct __main_block_impl_0 *blk = &tmp;

由上可知,代码将__main_block_impl_0结构体类型的自动变量,即栈上生成的__main_block_impl_0结构体实例的指针,赋值给__main_block_impl_0结构体指针类型的变量blk。以下为这部分代码对应的最初代码:

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

将Block语法生成的Block赋给Block类型变量blk。它等同于将__main_block_impl_0结构体实例的指针赋给变量blk。它等同于将__main_block_impl_0结构体实例的指针赋给变量blk。该源码中的Block就是__main_block_impl_0结构体类型的自动变量,即栈上生成的__main_block_impl_0结构体实例。
下面就看看__main_block_impl_0结构体实例构造函数。

static struct __main_block_desc_0 __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0)
};

由此可知,该源码使用Block,即__main_block_impl_0结构体实例的大小,进行初始化。
下面看栈上的__main_block_impl_0结构体实例(即Block)是如何根据这些参数进行初始化的。
如果张开__main_block_impl_0结构体的__block_impl结构体,可记述为如下形式:

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

该结构体根据构造函数会像下面这样进行初始化。

isa = &_NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;

__main_block_func_0函数指针赋给成员变量FuncPtr。
对应的Block部分;
blk();
这部分可变换为以下源码:

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

去掉转换部分。
(*blk->impl.FuncPtr)(blk);
这就是简单地使用函数指针调用函数。正如我们刚才确认的,由Block语法转换的__main_block_func_0函数的指针被赋值成员变量FuncPtr中。另外也说明了,__main_block_func_0函数的参数__cself指向Block值。在调用该函数的代码中可以看出Block正是作为参数进行了传递。
到此总算是摸清了Block的实质。现在说说_NSConcreteStackBlock到底是什么?
isa = &_NSConcreteStackBlock;
将Block指针赋给Block的结构体成员变量isa。为了理解它,首先要了解oc类和对象的实质。其实所谓Block就是oc对象。
id“这一变量类型用于存储oc对象。在oc源码中,虽然可以像使用void *类型那样随意使用id,但此id类型也能够在c语言中声明。在/usr/include/objc/runtime.h中是这样子声明的:

typedef struct objc_object {
    Class isa;
} *id;

id为objc_object结构体的指针类型。我们再看看Class
typedef struct objc_calss *Class;
Classobjc_class结构体的指针类型。objc_class结构体在/usr/include/objc/runtime.h中声明如下:

struct objc_class {
Class isa;
};

这与objc_object结构体相同。然而,objc_object结构体和objc_class结构体归根结底是在各个对象和类的实现中使用的最基本的结构体。
下面我们通过编写简单的oc类声明来确认下。

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

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

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

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

各类的结构体就是基于objc_class结构体的class_t结构体。class_t结构体在objc4运行时库的runtime/objc-runtime-new.h中声明如下:

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

在oc中,比如NSObject的class_t结构体实例以及NSMutableArray的class_t结构体实例等,均生成并保持各个类的class_t结构体实例。该实例持有声明的成员变量,方法的名称,方法的实现(即函数指针),属性以及父类的指针,并被oc运行时库所使用。
到这里,就可以理解oc的类与对象的实质了。
那么回到刚才的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结构体的oc类对象的结构体。另外,对其中的成员变量isa进行初始化,具体如下:
isa = &_NSConcreteStackBlock;
_NSConcreteStackBlock相当于class_t结构体实例。在将Block作为oc的对象处理时,关于该类的信息放置于_NSConcreteStackBlock中。
现在大家就能理解Block的实质,知道Block即为oc的对象了。

专栏:C++的this,Objective-c的self

C++中定义类的实例方法如下:

    void MyClass::method(int arg)
    {
        printf("%p %d\n", this, arg);
    }

    C++编译器将该方法作为c语言函数来处理。
    void __ZN7MyClass6methodEi(MyClass *this, int arg);
    {
        printf("%p %d\n", this, arg);
    }

    MyClass::method方法的实质就是__ZN7MyClass6methodEi函数。“this”作为第一个参数传递进去。
    该方法的调用如下:

    MyClass cls;

    cls.method(10);
    该源码通过c++编译器转换成c语言函数调用的形式:

    struct MyClass cls;

    __ZN7MyClass6methodEi(&cls, 10);

    即this就是MyClass类(结构体)的实例。
    同样,来看下oc的实例方法:

    - (void) method:(int)arg
    {
        NSLog(@"%p %d\n", self, arg);
    }

    oc编译器同C++的方法一样,也将该方法作为C语言的函数来处理。

    void _I_MyObject_method_(struct MyObject *self, SEL _cmd, int arg)
    {
        NSLog(@"%p %d\n", self, arg);
    }
    与C++中变换结果的this相同,“self”作为第一个参数被传递过去,以下为调用方代码。

    MyObject *obj = [[MyObject alloc] init];
    [obj method:10];

    如果使用clang的-rewrite-objc选项,则上面源码会转换为:

    MyObject *obj = objc_msgSend(
        objc_getClass("MyObject"), sel_registerName("alloc"));

    obj = objc_msgSend(obj, sel_registerName("init"));

    obj_msgSend(obj, sel_registerName("method:"), 10);

    objc_msgSend函数根据指定的对象和函数名,从对象持有类的结构体中检索_|_MyObject_method_函数的指针并调用。
    此时,objc_msgSend函数的第一个参数obj作为_|_MyObject_method_函数的第一个参数self进行传递。同C++一样,
    self就是MyObject类的对象。

1.2 截获自动变量:

本节主要讲解如何截获自动变量值。通过clang进行代码转换。

    int main()
{
      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;
  }

转换后的代码:

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;
    int val = __cself->val;

    printf(fmt, val);
}

static struct __main_block_desc_0 {
    unsigned long reserved;
    unsigned long Block_size;
} __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0)
};

int main()
{
    int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    void (*blk)(void) = &__main_block_impl_0(__main_block_func_0,
     &__main_block_desc_0_DATA, fmt, val);

     return 0;
}

着与前面转换的代码稍有不同。首先Block语法表达式中使用的自动变量被作为成员变量追加到了__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结构体内声明的成员变量类型与自动变量类型完全相同。注意,_Block语法表达式中没有使用的自动变量不会被追加,如代码中的变量dmy。_Blocks的自动变量截获只针对Block中使用的自动变量。下面看看初始化该结构体实例的构造函数的差异。

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,
             const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {

在初始化结构体实例时,根据传递给构造函数的参数对自动变量追加的成员变量进行初始化。以下通过构造函数调用确认其参数。

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

使用执行Block语法时的自动变量fmtval来初始化__main_block_impl_0结构体实例。
即在该源码中,__main_block_impl_0结构体实例的初始化如下。

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

由此可知,在__main_block_impl_0结构体实例(即Block)中,自动变量值被截获。
下面再来看下使用Block的匿名函数的实现。最初源码的Block语法如下:
{printf(fmt, val);}
该源码可转换为一下函数:

static void __main_block_func_0 (struct __main_block_impl_0 *__cself)
{
    const char *fmt = __cself->fmt;
    int val = __cself->val;

    printf(fmt, val);
}

在转换后的代码中,截获到__main_block_impl_0结构体实例的成员变量上的自动变量,这些变量在Block语法表达式之前被声明定义。因此原来的源码表达式无需改动便可食用截获的自动变量值值。
总的来说,所谓“截获自动变量值”意味着在执行Block语法时,Block语法表达式所使用的自动变量值被保存到Block的结构体实例(即Block自身)中。
然而,前一篇文章中提到的,Block不能直接使用C语言数组类型的自动变量。如上所诉,截获自动变量时,将值传递给结构体的构造函数进行保存。
下面确认在Block中利用c语言数组类型的变量时有可能使用到的源码。首先来看数组传递给Block的结构体构造函数的情况。

void func(char a[10])
{
    printf("%d\n", a[0]);
}

int main()
{
    char a[10] = {2};
    func(a);
}

该源码可以顺利编译,并正常执行。在之后的构造函数中,将参数赋给成员变量中,这样在变换了Block语法的函数内可由成员变量赋值给自动变量。代码预测如下:

void func(char a[10])
{
    char b[10] = a;
    printf("%d\n",b[0]);
}

int main()
{
    char a[10] = {2};
    func(a);
}

该源码将C语言数组类型变量赋值给C语言数组类型变量中,这是不能编译的,虽然变量的类型以及数组的大小都相同,但c语言规范不允许这种赋值。当然,有许多方法可以截获值,但Blocks似乎更遵循c语言规范。

Zerlz wechat
扫码关注一个很懒的程序员!
Winter is coming, give me a penny!