(译)窥探Blocks(2)

本文翻译自Matt Galloway的博客

之前的文章(译)窥探Blocks(1)我们已经了解了block的内部原理,以及编译器如何处理它。本文我将讨论一下非常量的blocks以及它们在栈上的组织方式。

Block 类型

第一篇文章中,我们看到block有__NSConcreteGlobalBlock类。block结构体和descriptor都在编译阶段基于已知的变量完全初始化了。block还有一些不同的类型,每一个类型都对应一个相关的类。为了简单起见,我们只考虑其中的三个:

  1. _NSConcreteGlobalBlock是一个全局定义的block,在编译阶段就完成创建工作。这些block没有捕获任何域,比如一个空block。
  2. _NSConcreteStackBlock是一个在栈上的block,这是所有blocks在最终拷贝到堆上之前所开始的地方。
  3. _NSConcreteMallocBlock是一个在堆上的block,这是拷贝一个block后最终的位置。它们在这里被引用计数并且在引用计数变为0时被释放。

捕获域的block

现在我们来看看下面一段代码:

#import <dispatch/dispatch.h>

typedef void(^BlockA)(void);
void foo(int);

__attribute__((noinline))
void runBlockA(BlockA block) {
    block();
}

void doBlockA() {
    int a = 128;
    BlockA block = ^{
        foo(a);
    };
    runBlockA(block);
}

这里有一个方法foo,因此block捕获了一些东西,用一个捕获到的变量来调用方法。我又看了一下armv7所产生的一小段相关代码:

    .globl  _runBlockA
    .align  2
    .code   16                      @ @runBlockA
    .thumb_func     _runBlockA
_runBlockA:
    ldr     r1, [r0, #12]
    bx      r1 

首先,runBlockA方法与之前的结果一样,它调用block的invoke方法。然后看看doBlockA

.globl  _doBlockA
    .align  2
    .code   16                      @ @doBlockA
    .thumb_func     _doBlockA
_doBlockA:
    push    {r7, lr}
    mov     r7, sp
    sub     sp, #24
    movw    r2, :lower16:(L__NSConcreteStackBlock$non_lazy_ptr-(LPC1_0+4))
    movt    r2, :upper16:(L__NSConcreteStackBlock$non_lazy_ptr-(LPC1_0+4))
    movw    r1, :lower16:(___doBlockA_block_invoke_0-(LPC1_1+4))
LPC1_0:
    add     r2, pc
    movt    r1, :upper16:(___doBlockA_block_invoke_0-(LPC1_1+4))
    movw    r0, :lower16:(___block_descriptor_tmp-(LPC1_2+4))
LPC1_1:
    add     r1, pc
    ldr     r2, [r2]
    movt    r0, :upper16:(___block_descriptor_tmp-(LPC1_2+4))
    str     r2, [sp]
    mov.w   r2, #1073741824
    str     r2, [sp, #4]
    movs    r2, #0
LPC1_2:
    add     r0, pc
    str     r2, [sp, #8]
    str     r1, [sp, #12]
    str     r0, [sp, #16]
    movs    r0, #128
    str     r0, [sp, #20]
    mov     r0, sp
    bl      _runBlockA
    add     sp, #24
    pop     {r7, pc}

这下看起来比之前的复杂多了。与从一个全局符号加载一个block不同,这看起来做了许多工作。看起来可能有点麻烦,但其实也非常简单。我们最好考虑重新整理这些方法,但请相信我这样做不会没有改变任何功能。编译器之所以这样安排它的指令顺序,是为了优化编译性能,减少流水线气泡。重新整理后的方法如下:

_doBlockA:
        // 1
        push    {r7, lr}
        mov     r7, sp

        // 2
        sub     sp, #24

        // 3
        movw    r2, :lower16:(L__NSConcreteStackBlock$non_lazy_ptr-(LPC1_0+4))
        movt    r2, :upper16:(L__NSConcreteStackBlock$non_lazy_ptr-(LPC1_0+4))
LPC1_0:
        add     r2, pc
        ldr     r2, [r2]
        str     r2, [sp]

        // 4
        mov.w   r2, #1073741824
        str     r2, [sp, #4]

        // 5
        movs    r2, #0
        str     r2, [sp, #8]

        // 6
        movw    r1, :lower16:(___doBlockA_block_invoke_0-(LPC1_1+4))
        movt    r1, :upper16:(___doBlockA_block_invoke_0-(LPC1_1+4))
LPC1_1:
        add     r1, pc
        str     r1, [sp, #12]

        // 7
        movw    r0, :lower16:(___block_descriptor_tmp-(LPC1_2+4))
        movt    r0, :upper16:(___block_descriptor_tmp-(LPC1_2+4))
LPC1_2:
        add     r0, pc
        str     r0, [sp, #16]

        // 8
        movs    r0, #128
        str     r0, [sp, #20]

        // 9
        mov     r0, sp
        bl      _runBlockA

        // 10
        add     sp, #24
        pop     {r7, pc}

这就是它所做的事:

  1. 方法开始。r7被压入栈,因为它即将被重写,而且作为一个寄存器必须在方法调用时候保存值。lr是一个链接寄存器,也被压入栈,保存了下一个指令的地址,好让方法返回时继续执行下一个指令。可以在方法结尾看到。 栈指针(sp)也被保存在r7中。

  2. 栈指针(sp)减去24,留出24字节的栈空间存储数据。

  3. 这一小块代码正在相对于程序计数器查找L__NSConcreteStackBlock$non_lazy_ptr符号,这样最后链接成功的二进制文件,不管代码结束于任何地方,它都可以正常工作(这句话有点绕,翻译的不好,需要好好理解一下)。这个值最后存储在栈指针指向的位置。

  4. 1073741824存储在sp + 4 的位置上。

  5. 0存储在sp + 8的位置上。现在可能情况比较清晰了。回顾上一篇文章中提到的Block_layout结构体,可以看出一个Block_layout结构体在栈上创建了!目前为止已经有了isa指针,flagsreserved值被设置了。

  6. ___doBlockA_block_invoke_0的地址存储在sp + 12位置。这就是block结构体的invoke参数。

  7. ___block_descriptor_tmp的地址存储在sp + 16位置。这就是block结构体的descriptor参数。

  8. 128存储在sp + 20的位置。啊!如果你回看Block_layout结构体你会发现里面只有5个值。那么存在这个结构体末尾的是什么呢?哈哈,别忘记了,这个128就是在这个block前定义的、被block捕获的值。所以这一定是存储它们使用变量的地方——在Block_layout最后。

  9. sp现在指向一个完全初始化的block结构体,它被放入r0寄存器,然后runBlockA被调用。(记住在ARM EABI中r0包含了方法的第一个参数)

  10. 最后sp + 24 已抵消最开始减去的24。然后分别从栈弹出两个值到r7pc中。r7抵消一开始压栈的操作,pc将获得方法开始时lr里面的值。这样有效地完成了方法返回的操作,让CPU继续(程序计数器pc)从方法返回的地方(链接寄存器lr)执行。

哇哦!你还在跟着我学?太牛逼啦!

这一小段的最后一部分是来看看invoke方法和descriptor长什么样。我们希望它们不要与第一篇文章中的全局block差太多。

.align  2
    .code   16                      @ @__doBlockA_block_invoke_0
    .thumb_func     ___doBlockA_block_invoke_0
___doBlockA_block_invoke_0:
    ldr     r0, [r0, #20]
    b.w     _foo

    .section        __TEXT,__cstring,cstring_literals
L_.str:                                 @ @.str
    .asciz   "v4@?0"

    .section        __TEXT,__objc_classname,cstring_literals
L_OBJC_CLASS_NAME_:                     @ @"\01L_OBJC_CLASS_NAME_"
    .asciz   "\001P"

    .section        __DATA,__const
    .align  2                       @ @__block_descriptor_tmp
___block_descriptor_tmp:
    .long   0                       @ 0x0
    .long   24                      @ 0x18
    .long   L_.str
    .long   L_OBJC_CLASS_NAME_

还真是相差不大。唯一的区别在于block descriptor的size值。现在它是24而不是20。因为block此时捕获了一个整形数值。我们已经看到在创建block结构体时,这额外的4字节被放在了最后。

同样地,你在实际执行的方法__doBlockA_block_invoke_0中也会发现参数值从结构体末尾处(r0 + 20)读取出来,这就是block捕获的值。

捕获对象类型的值会怎样?

下面要考虑的是捕获的不再是一个整形,而是一个对象,比如NSString。欲知详情,请看下面代码:

#import <dispatch/dispatch.h>

typedef void(^BlockA)(void);
void foo(NSString*);

__attribute__((noinline))
void runBlockA(BlockA block) {
    block();
}

void doBlockA() {
    NSString *a = @"A";
    BlockA block = ^{
        foo(a);
    };
    runBlockA(block);
}

我不再研究doBlockA的细节,因为变化不大。比较有意思的是它创建的block descriptor结构体。

 .section        __DATA,__const
    .align  4                       @ @__block_descriptor_tmp
___block_descriptor_tmp:
    .long   0                       @ 0x0
    .long   24                      @ 0x18
    .long   ___copy_helper_block_
    .long   ___destroy_helper_block_
    .long   L_.str1
    .long   L_OBJC_CLASS_NAME_

注意现在有了名为___copy_helper_block____destroy_helper_block_的函数指针。这里是这些函数的定义:

.align  2
    .code   16                      @ @__copy_helper_block_
    .thumb_func     ___copy_helper_block_
___copy_helper_block_:
    ldr     r1, [r1, #20]
    adds    r0, #20
    movs    r2, #3
    b.w     __Block_object_assign

    .align  2
    .code   16                      @ @__destroy_helper_block_
    .thumb_func     ___destroy_helper_block_
___destroy_helper_block_:
    ldr     r0, [r0, #20]
    movs    r1, #3
    b.w     __Block_object_dispose

我猜这些方法是在block拷贝和销毁的时候调用,它们一定是在持有或释放被block捕获的对象。看起来拷贝函数用了两个参数,因为r0r1被寻址,它们两可能有有效的数据。销毁函数好像就一个参数。所有复杂的操作貌似都是_Block_object_assign_Block_object_dispose干的。这部分代码在block runtime里。

如果你想了解更多关于block runtime的代码,可以去http://compiler-rt.llvm.org下载源码,重点看看runtime.c

下一篇我们将研究一下Block_Copy的原理。

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

推荐阅读更多精彩内容

  • 目录 Block底层解析什么是block?block编译转换结构block实际结构block的类型NSConcre...
    tripleCC阅读 33,052评论 32 388
  • 前言 Blocks是C语言的扩充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了这...
    小人不才阅读 3,729评论 0 23
  • 1: 什么是block?1.0: Block的语法1.1: block编译转换结构1.2: block实际结构 2...
    iYeso阅读 796评论 0 5
  • 原文地址:Objective-C中的Block 1.相关概念 在这篇笔记开始之前,我们需要对以下概念有所了解。 1...
    默默_David阅读 386评论 0 1
  • 《Objective-C高级编程》这本书就讲了三个东西:自动引用计数、block、GCD,偏向于从原理上对这些内容...
    WeiHing阅读 9,731评论 10 69