刨根问底,Block竟然是?

对于iOS开发者来说,Block就像一件哆啦A梦口袋中的宝贝,帮助我们简化代码,实现功能。但是哆啦A梦这部动画片中并没有人好奇这些宝贝是如何实现的,但是作为程序猿,应该要学会刨根问底,了解本质,本文,就深入浅出地讲解Block的实现。

初步了解Block

block是基于C语言的扩展功能。block有一个比较常见的说法,叫做:带有自动变量的匿名函数,第一眼看上去有些陌生,我们抓住关键字来理解一下,首先匿名函数,即为没有名字的函数,我们尝试用函数指针来实现一下

int function(int a) {
  cout << a << endl;
}
int (* functionPointer) (int) = &function;
int result = (*function)(19);

注意看最后一行,我们通过函数指针去调用了函数,并不知道函数名。接着还有关键字是带有自动变量,这里的自动变量也就是局部变量,到这里,我们来看看block语法:
^ 返回值类型 参数列表 表达式
举个例子看看

^ int (int a) { return a * a; };

同样我们像之前函数指针的赋值一样,将Block语法赋值给声明为Block类型的变量中

int (^blk) (int) = ^ int (int a) { return a * a; };

调用则为:

blk(19);

通过对比赋值匿名函数和赋值Block类型变量可以发现,两者的写法即为相似,区别在于block中为^符号,而函数为*符号
这里可能大家有个疑惑,那截获自动变量是什么意思呢,我们带着这个问题继续往下。

截获自动变量

我们先从下面一个场景入手

int value = 0;
void (^blk) (void) = ^{ printf("%d", value); };
value = 19;
blk();

对block有了解的人都知道,以上输出应该是0,而不是19,在表面上大家就可以理解为block截获了变量的瞬间值

一、通过源码来究其本质
int main(int argc, const char * argv[]) {

    void(^blk)(void) = ^ {
        printf("%d", 19);
    };
    blk();
    return 0;

}

我们使用clang -rewrite-objc指令来将以上代码解析为C++源代码:具体转化的代码如下(这段源码看上去很复杂,但却是我们理解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("%d", 19);
    }

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

我们从main函数入手,看看到底发生了什么变化,我们将main函数中类型转换的操作去掉,简化后的代码如下:

int main(int argc, const char * argv[]) {
    void(*blk)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
}

进一步分解为:

int main(int argc, const char * argv[]) {
    struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0, &_main_block_desc_0_DATA);
    struct __main_block_impl_0 *blk = temp;
}

从上面可以理解为,编译之后的block是结构体类型的,声明的blk是一个指向结构体类型block的指针。
了解了基本的之后,再来看其中所涉及到的结构体类型是如何定义的,我们从__main_block_impl_0开始(这个结构体名的命名规则是:Block所属的函数+Blcok语法在该函数出现的顺序):

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

该结构体中定义了两个成员,implDesc,同时拥有一个结构体初始化方法(main函数中调用的方法),其中对impl的几个成员变量赋值,同时赋值了Desc
再来看看成员之一的__block_impl结构体是如何定义的:

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

我们主要关注这个几个成员:

  • *isa:指向类的指针
  • *FuncPtr:在main函数中初始化时我们可以注意到是将__main_block_func_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)};

从main函数中初始化可以看出将__main_block_des_0类型的变量__main_block_desc_0_DATA传入赋值

基本了解了各个部分之后,我们先进行一下总结:

  • 声明block:创建了一个__main_block_imp_0类型的结构体,并用一个该类型的指针指向这个结构体
  • 使用block:调用了结构体中的成员__block_implFuncPtr方法
二、截获自动变量的源代码分析

通过刚才的理解,大致知道了编译之后的代码构成,但是上述代码并没有截获自动变量,我们又重新写了一份截获自动变量的代码:

int main(int argc, const char * argv[]) {

    int value = 3;
    void(^blk)(void) = ^ {
        printf("%d", value);
    };
    blk();
        
    return 0;
}

并且继续使用clang将其编译成C++的源代码

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;
  int value;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _value, int flags=0) : value(_value) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int value = __cself->value; // bound by copy

        printf("%d", value);
    }

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

    int value = 3;
    void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, value));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
}

此时,我们只要看一下捕获自动变量和没有捕获自动变量时的区别就能够理解它的实现原理。
我们仍然从main函数入手,可以发现唯一发生变化的是:__main_block_impl_0结构体初始化参数多了一个value,这个value也正是我们block截获的自动变量。
那么我们就再来看看__main_block_impl_0结构体定义中出现了什么变化

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int value; // 1
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _value, int flags=0) : value(_value) { // 2
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

从上面的标记可以看出,有两个地方存在不同

  1. 结构体中存在一个int类型的成员(和截获值相同类型和名字)
  2. 初始化方法中将传入值赋值给该成员变量

我们返回到main函数中初始化方法中看传入值是被截获的值。

接着我们来看静态方法__main_block_func_0的区别:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int value = __cself->value; // bound by copy

        printf("%d", value);
    }

我们可以发现,参数中的*_cself指针终于派上用场了,这个参数是在main函数中调用该静态方法的时候传入的

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

参数为blk,前面也提到了blk是一个指向结构体的指针。
可以发现,该静态方法中操作的实际上是结构体本身的成员,而不是block之外的value变量。

到了这里,我们又该进行一次总结了:
截获自动变量,从本质上是在结构体内部增加了一个类型和名字一样的成员,并且赋值,从而block操作的都是成员。

三、使用__block修饰符的源码分析

以上我们分析了没有捕获自动变量的情况以及捕获自动变量的情况,在捕获自动变量时,如果我们修改了捕获的值,就会报错

Variable is not assignable(missing,__block type specifier)

因为在底层实现上,不允许改变结构体自己的内部成员值,这时,我们就需要加上__block修饰符。
我们写以下代码,来实现对捕获的自动变量修改:

int main(int argc, const char * argv[]) {

    __block int value = 3;
    void(^blk)(void) = ^ {
        value = 19;
    };
    blk();
        
    return 0;
}

同样地,我们通过clang来编程成C++代码:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
struct __Block_byref_value_0 {
  void *__isa;
__Block_byref_value_0 *__forwarding;
 int __flags;
 int __size;
 int value;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_value_0 *value; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_value_0 *_value, int flags=0) : value(_value->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_value_0 *value = __cself->value; // bound by ref

        (value->__forwarding->value) = 19;
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->value, (void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);}

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

int main(int argc, const char * argv[]) {

    __attribute__((__blocks__(byref))) __Block_byref_value_0 value = {(void*)0,(__Block_byref_value_0 *)&value, 0, sizeof(__Block_byref_value_0), 3};
    void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_value_0 *)&value, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
}

可以发现,仅仅是多了一个__block修饰符,就多了这么多代码,那么让我们来研究一下,苹果是如何通过__block来实现修改截获的自动变量的呢。
这一次我们不从main函数入手,我们从上往下看发现了一个新的结构体类型__Block_byref_value_0,我们来看看这是什么:

struct __Block_byref_value_0 {
  void *__isa; // 1
__Block_byref_value_0 *__forwarding; // 2
 int __flags;
 int __size;
 int value; // 3
};

从上面的三处标记来看:

  1. 是一个指向类的指针,之前__block_imp中也存在这个指针
  2. __forwarding是一个指向自己的指针
  3. 被截获的自动变量作为了这个结构体的成员

看到这里好像有点明白,苹果将用__block修饰的变量转变成了一个结构体,并在其中持有截获的自动变量。

我们继续往下看,发现__main_block_impl_0中也存在不同,具体如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_value_0 *value; // by ref // 1
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_value_0 *_value, int flags=0) : value(_value->__forwarding) { // 2
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以发现两有个地方不一样:

  1. 成员变量变成了刚刚的__Block_byref_value_0类型的指针,名字仍然和截获的自动变量名一致
  2. 初始化的时候将传入对象的__forwarding指针赋值给了成员变量的指针

我们继续看main函数中是如何初始化的:

int main(int argc, const char * argv[]) {

    __attribute__((__blocks__(byref))) __Block_byref_value_0 value = {(void*)0,(__Block_byref_value_0 *)&value, 0, sizeof(__Block_byref_value_0), 3};
    void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_value_0 *)&value, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
}

我们仍然采用简化的方式来看看逻辑:

int main(int argc, const char * argv[]) {
    // 1
    __Block_byref_value_0 value = {
        0,
        &value,
        0,
        sizeof(__Block_byref_value_0), 
        3
    };
    struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0, &_main_block_desc_0_DATA, &value, 570425344); // 2
    struct __main_block_impl_0 *blk = temp;
    blk->FuncPtr(blk);
    return 0;
}

通过标记中的两点:

  1. __block修饰的变量被编译成了一个结构体变量,并进行了初始化,对照结构体参数可以发现,第二个参数赋值了自身的引用,最后一个参数赋值了截获的自动变量的值。
  2. __main_block_impl_0的初始化参数中,传入了value的引用

那值是如何改变的呢,我们去看一下调用的这个静态方法的实现:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_value_0 *value = __cself->value; // bound by ref

        (value->__forwarding->value) = 19;
    }

可以发现改变的操作是,利用成员变量value,调用指向自己的指针__forwarding,然后访问成员value的值,从而进行改变。
这里可能大家都有个疑问,为什么不直接访问value就行了,还要先调用指向自己的指针,这岂不是多此一举。
其实不然,苹果这样设计是有它自己的目的,本文打算不做扩展,我们还是围绕这主题继续进行下去。

除了以上不同之外,还可以发现多了两个方法:

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->value, (void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);}

外层的方法名叫做

  1. __main_block_copy_0
  2. __main_block_dispose_0

内层分别调用

  1. _Block_object_assign
  2. _Block_object_dispose

本文中,我们仅仅知道这两个方法是帮助我们自动进行__Block_byref_value_0结构体变量内存管理的。

总结

本文主要以block源码分析为主,重点理解了block是如何捕获自动变量的,又是如何修改自动变量的。但是关于block,还有很多精妙的设计和知识点,需要我们继续探究。

参考资料
  • 《Objective-C高级编程(iOS与OS X多线程和内存管理)》
  • 官方文档
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容