深度围观block:第二集


blocks

本文由破船译自galloway转载请注明出处!

小引

今天翻译了第二篇,这个翻译是比较痛苦(其实不止这篇,所有的都是), 不比单纯的阅读,许多地方需要查阅资料,并细心的遣词造句,还得注意词不达意的地方(例如文中的A block that captures scope我翻译为block的拷贝范围,总感觉缺了一些作者原意,功力有限啊)。所以,我劝大家要是能看原文尽量去看原文吧,我这翻译的权当参考。

目录

  • 介绍
  • block类型
  • block的拷贝范围
  • block拷贝对象的类型
  • 何去何从

正文

介绍

本文接着上一篇文章(深度围观block:第一集),继续从编译器的角度深度围观block。在本文中,将介绍block并不是一成不变的,以及block在栈上的构成。

block类型

第一篇文章中,我们已经看到block有一个_NSConcreteGlobalBlock这样的类。由于所有变量都是已知的,所以在编译期间,block的结构(structure)和描述(descriptor)都将全部被初始化。关于block这里有几种不同的类型,每种类型都有对应的类。为了简单起见,这里只考虑其中三种:

  1. _NSConcreteGlobalBlock是定义一个全局的block,在编译器就已经完成相关初始化任务。这种类型的block不会涉及到任何拷贝,例如一个空的block。
  2. _NSConcreteStackBlock是一个分配在栈上的block。这里是所有最终被拷贝到堆(heap)上的block的开始。
  3. _NSConcreteMallocBlock是分配到堆(heap)上的block。拷贝完一个block之后,这就会结束。当block的引用计数变为0,该block就会被释放。

block拷贝范围

这次我们来看看另外一些代码,如下所示:

 
 
  1. #import <dispatch/dispatch.h>
  2.  
  3. typedef void(^BlockA)(void);
  4. void foo(int);
  5.  
  6. __attribute__((noinline))
  7. void runBlockA(BlockA block) {
  8. block();
  9. }
  10.  
  11. void doBlockA() {
  12. int a = 128;
  13. BlockA block = ^{
  14. foo(a);
  15. };
  16. runBlockA(block);
  17. }

为了让block拷贝一些内容,上面的代码中调用了foo函数,并给这个函数传递了一个变量。再说一下,本文涉及到的汇编代码是与armv7相关指令。下面是其中一部分汇编指令:

 
 
  1. .globl _runBlockA
  2. .align 2
  3. .code 16 @ @runBlockA
  4. .thumb_func _runBlockA
  5. _runBlockA:
  6. ldr r1, [r0, #12]
  7. bx r1

上面的汇编代码与runBlockA函数相关,这跟第一篇文章中的相同——都是调用了block中的invoke函数。接着是doBlockA汇编代码,如下所示:

 
 
  1. .globl _doBlockA
  2. .align 2
  3. .code 16 @ @doBlockA
  4. .thumb_func _doBlockA
  5. _doBlockA:
  6. push {r7, lr}
  7. mov r7, sp
  8. sub sp, #24
  9. movw r2, :lower16:(L__NSConcreteStackBlock$non_lazy_ptr-(LPC1_0+4))
  10. movt r2, :upper16:(L__NSConcreteStackBlock$non_lazy_ptr-(LPC1_0+4))
  11. movw r1, :lower16:(___doBlockA_block_invoke_0-(LPC1_1+4))
  12. LPC1_0:
  13. add r2, pc
  14. movt r1, :upper16:(___doBlockA_block_invoke_0-(LPC1_1+4))
  15. movw r0, :lower16:(___block_descriptor_tmp-(LPC1_2+4))
  16. LPC1_1:
  17. add r1, pc
  18. ldr r2, [r2]
  19. movt r0, :upper16:(___block_descriptor_tmp-(LPC1_2+4))
  20. str r2, [sp]
  21. mov.w r2, #1073741824
  22. str r2, [sp, #4]
  23. movs r2, #0
  24. LPC1_2:
  25. add r0, pc
  26. str r2, [sp, #8]
  27. str r1, [sp, #12]
  28. str r0, [sp, #16]
  29. movs r0, #128
  30. str r0, [sp, #20]
  31. mov r0, sp
  32. bl _runBlockA
  33. add sp, #24
  34. pop {r7, pc}

看看,这跟之前的代码有所不同了。看起来这不仅仅是从一个全局的符号中加载block,而且还做了额外的一些事情。乍一看这么多代码让人有点无从下手,不过认真看,还是很容易理解的。从上面的代码可以看出,编译器已经忽略了对代码排序的优化,为了方便阅读代码,我对上面的汇编代码重新进行排序(当然,请相信我,这不会影响任何功能)。下面是我重排好的代码效果:

 
 
  1. _doBlockA:
  2. // 1
  3. push {r7, lr}
  4. mov r7, sp
  5.  
  6. // 2
  7. sub sp, #24
  8.  
  9. // 3
  10. movw r2, :lower16:(L__NSConcreteStackBlock$non_lazy_ptr-(LPC1_0+4))
  11. movt r2, :upper16:(L__NSConcreteStackBlock$non_lazy_ptr-(LPC1_0+4))
  12. LPC1_0:
  13. add r2, pc
  14. ldr r2, [r2]
  15. str r2, [sp]
  16.  
  17. // 4
  18. mov.w r2, #1073741824
  19. str r2, [sp, #4]
  20.  
  21. // 5
  22. movs r2, #0
  23. str r2, [sp, #8]
  24.  
  25. // 6
  26. movw r1, :lower16:(___doBlockA_block_invoke_0-(LPC1_1+4))
  27. movt r1, :upper16:(___doBlockA_block_invoke_0-(LPC1_1+4))
  28. LPC1_1:
  29. add r1, pc
  30. str r1, [sp, #12]
  31.  
  32. // 7
  33. movw r0, :lower16:(___block_descriptor_tmp-(LPC1_2+4))
  34. movt r0, :upper16:(___block_descriptor_tmp-(LPC1_2+4))
  35. LPC1_2:
  36. add r0, pc
  37. str r0, [sp, #16]
  38.  
  39. // 8
  40. movs r0, #128
  41. str r0, [sp, #20]
  42.  
  43. // 9
  44. mov r0, sp
  45. bl _runBlockA
  46.  
  47. // 10
  48. add sp, #24
  49. pop {r7, pc}

下面我们来看看这些代码都做了什么:

  1. 开场白。首先将 r7 push到栈上面——因为r7会被覆盖,而r7寄存器中的内容在跨函数调用时是需要用到的。lr是链接寄存器(link register),该寄存器中存储着当这个函数返回时需要执行下一条指令的地址。接着mov这条指令的作用是把栈指针保存到r7寄存器中。
  2. 从栈指针所处位置开始减去24,也就是在栈空间上开辟24字节来存储数据。
  3. 这里涉及到的代码是为了对符号L__NSConcreteStackBlock$non_lazy_ptr进行寻址,由于跟pc(program counter)相关联,所以无论代码处于二进制文件中任何位置,当最终链接时,都能对该符号做到正确的寻址。
  4. 将值1073741824存储到栈指针 + 4 的位置。
  5. 将值0存储到栈指针 + 8 的位置。现在,将要发生什么可能已经变得逐渐清晰了——在栈上创建了一个Block_layout结构的对象!到现在为止,已经设置了该结构的3个值:isa指针,flagsreserved值。
  6. ___doBlockA_block_invoke_0存储至栈指针 + 12的位置。这是block结构中的invoke
  7. ___block_descriptor_tmp存储至栈指针 + 16的位置。这是block结构中的descriptor
  8. 将值128存储到栈指针 + 20的位置。如果回头看看Block_layout结构,可以看到里面只应该有5个值。那么在这个block结构体后面存储的128是什么呢?——注意到这个128实际上就是在block中拷贝的变量的值。所以这肯定就是存储block使用到的值的地方——在Block_layout结构尾部。
  9. 现在栈指针指向了已经完成初始化之后的block结构,在这里的汇编指令是将栈指针装载到r0中,然后调用runBlockA函数。(记住:在ARM EABI中,r0中存储的内容被当做函数的第一个参数)。
  10. 最后将栈指针加上24,这样就能够把最开始减去的24(在栈上开辟的24位空间)收回来。接着将栈中的两个值pop到r7pc寄存器中。这里pop到r7中的,跟最开始从r7中push至栈中的内容是一致的,而pc的值则是最开始push lr到栈中的值,这样当函数返回时,可以让CPU能够正确的继续执行后续指令。

Cooool!如果你一直认真看到这里,那么相信你的收获已经非常多了!

下面我们再看看block中的invoke函数和descriptor。希望跟第一集中的不要有太大差别。如下汇编代码:

 
 
  1. .align 2
  2. .code 16 @ @__doBlockA_block_invoke_0
  3. .thumb_func ___doBlockA_block_invoke_0
  4. ___doBlockA_block_invoke_0:
  5. ldr r0, [r0, #20]
  6. b.w _foo
  7.  
  8. .section __TEXT,__cstring,cstring_literals
  9. L_.str: @ @.str
  10. .asciz "v4@?0"
  11.  
  12. .section __TEXT,__objc_classname,cstring_literals
  13. L_OBJC_CLASS_NAME_: @ @"1L_OBJC_CLASS_NAME_"
  14. .asciz "01P"
  15.  
  16. .section __DATA,__const
  17. .align 2 @ @__block_descriptor_tmp
  18. ___block_descriptor_tmp:
  19. .long 0 @ 0x0
  20. .long 24 @ 0x18
  21. .long L_.str
  22. .long L_OBJC_CLASS_NAME_

看着没错,跟第一集中的没多大区别。唯一不同的就是block descriptor中的size——现在是24(之前是20)。这是因为block拷贝了一个整型值,所以block的结构需要24个字节,而不再是标准的20个字节了。在之前的代码中,我们已经分析了在创建block时,多出的4个字节被添加到block结构的尾部。
在实际的block函数中,例如___doBlockA_block_invoke_0,可以看到从block结构尾部读取出相关值,如r0 + 20,就是在block中拷贝的变量。

block拷贝对象的类型

下面我们来看看如果block拷贝的是别的对象类型(例如 NSString),而不是integer,会发生什么呢?如下代码:

 
 
  1. #import <dispatch/dispatch.h>
  2.  
  3. typedef void(^BlockA)(void);
  4. void foo(NSString*);
  5.  
  6. __attribute__((noinline))
  7. void runBlockA(BlockA block) {
  8. block();
  9. }
  10.  
  11. void doBlockA() {
  12. NSString *= @"A";
  13. BlockA block = ^{
  14. foo(a);
  15. };
  16. runBlockA(block);
  17. }

由于doBlockA变化不大,所以在此不深入介绍。这里感兴趣的是根据上面代码创建的block descriptor结构:

 
 
  1. .section __DATA,__const
  2. .align 4 @ @__block_descriptor_tmp
  3. ___block_descriptor_tmp:
  4. .long 0 @ 0x0
  5. .long 24 @ 0x18
  6. .long ___copy_helper_block_
  7. .long ___destroy_helper_block_
  8. .long L_.str1
  9. .long L_OBJC_CLASS_NAME_

注意看上面的汇编代码中有指向两个函数(___copy_helper_block____destroy_helper_block_)的指针。下面是这两个函数的定义:

 
 
  1. .align 2
  2. .code 16 @ @__copy_helper_block_
  3. .thumb_func ___copy_helper_block_
  4. ___copy_helper_block_:
  5. ldr r1, [r1, #20]
  6. adds r0, #20
  7. movs r2, #3
  8. b.w __Block_object_assign
  9.  
  10. .align 2
  11. .code 16 @ @__destroy_helper_block_
  12. .thumb_func ___destroy_helper_block_
  13. ___destroy_helper_block_:
  14. ldr r0, [r0, #20]
  15. movs r1, #3
  16. b.w __Block_object_dispose

这里我先假设当block被拷贝和销毁时,都会调用这里的函数。那么被block拷贝的对象肯定会发生reatain和release。上面的代码中,可以看出如果r0和r1包含有效数据时,拷贝函数接收两个参数(r0r1)。而销毁函数接收一个参数。可以看出所有的拷贝和销毁任务都应该是由__Block_object_assign__Block_object_dispose两个函数完成的。这两个函数位于block的运行时代码中(是LLVM里面compiler-rt工程的一部分)。
如果你希望了解一下block运行时相关代码,可以来这里下载源码:http://compiler-rt.llvm.org。特别关注一下里面的runtime.c文件。

何去何从

在下一集中我将调查Block_copy相关代码,并看看相关工作处理情况,以此来深度围观一下block运行时。通过下一集的学习,你也将会深入了解拷贝和销毁函数(也就是本文中我们刚刚看到的在block拷贝对象时使用的函数)。

本文由破船翻译●转载请注明出处●2013-07-10

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值