深度围观block:第一集


blocks_2x

 

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

小引

还记得之前的两篇文章吗:iOS汇编教程:ARM(1)iOS汇编教程:ARM(2),里面介绍了Objective-C生成的汇编代码。本文介绍的内容也跟汇编相关,只不过是与block相关,如果对汇编有不了解的,可以先去看看那两篇带有启蒙性质的文章哟。本文将从汇编的角度来介绍block相关知识。另外,如果你对block还不了解的话,建议你先去看看我的上一篇文章:初识block

目录:

  • 简介
  • 基础知识
  • 深入一个简单示例
  • 源码在这里
  • 何去何从

正文

简介

今天我们从编译器的角度观察一下block内部是如何工作的。这里说的block是指苹果为C语言增加的具有闭包性(closure)的一个功能,block已经是clang/LLVM编译器所支持的一部分了。我一直在想block是什么,以及它是如何奇迹般的出现在Objective-C对象中(开发者可以像处理实例对象一样,对block进行copyretainrelease)。本文我首先深入的介绍一点关于block的那些事。

基础知识

用过block的开发者都知道,下面的代码就是一个block:

 
 
  1. void(^block)(void) = ^{
  2. NSLog(@"I'm a block!");
  3. };

上面的代码中创建了一个名为block的变量,并把一个简单的block代码赋值给这个变量。代码很简单,不是吗?不!!!在这里我想要搞清楚编译器对这点代码都做了些什么。
更进一步,下面的代码我给block传递了一个变量:

 
 
  1. void(^block)(int a) = ^{
  2. NSLog(@"I'm a block! a = %i", a);
  3. };

而下面的代码是从block中返回一个值:

 
 
  1. int(^block)(void) = ^{
  2. NSLog(@"I'm a block!");
  3. return 1;
  4. };

作为一个封闭的包,block将所处的上下文封装到了block中:

 
 
  1. int a = 1;
  2. void(^block)(void) = ^{
  3. NSLog(@"I'm a block! a = %i", a);
  4. };

编译器对上面这些代码具体是如何处理的——这才是我所感兴趣的。

深入一个简单示例

首先我的思路是看看编译器是如何编译一个非常简单的block。来看看如下代码:

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

之所以要用上面这样的代码,是因为我想看看block是如何创建的,以及如何调用一个block。如果block的创建和调用都在一个函数里面,那么优化器(optimiser)可能会对代码做优化处理,导致我们看不到任何感兴趣的东西,所以我给runBlockA函数添加了noinline,这样优化器就不会在doBlockA函数中对runBlockA的调用做内联优化处理。

上面代码通过编译器编译之后(armv7,03),会得到如下汇编指令:

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

上面的汇编代码是对应runBlockA函数——这相当的简单。注意观察之前的源码,可以知道这个函数只是简单的调用了block。在ARM EABI中,将r0(寄存器r0)设置为第一个参数。第一条指令(r1)是将存储在地址为r0 + 12的值装载到寄存器r1中。这可以理解为指针的解引用——读12个字节到寄存器中。然后跳转到这个地址执行后面的指令。注意,这里使用了r1,而r0没有被修改,仍然是原来的block。所以这里很有可能是利用第一个参数来调用block。
据此,可以确定block在结构中的一些排序规则:block被当做执行的函数时存储在某个结构中,并占据了12个字节。当传递一个block时,指向这些结构的一个指针被传递进来了。

下面来看看doBlockA函数:

 
 
  1. .globl _doBlockA
  2. .align 2
  3. .code 16 @ @doBlockA
  4. .thumb_func _doBlockA
  5. _doBlockA:
  6. movw r0, :lower16:(___block_literal_global-(LPC1_0+4))
  7. movt r0, :upper16:(___block_literal_global-(LPC1_0+4))
  8. LPC1_0:
  9. add r0, pc
  10. b.w _runBlockA

OK,上面的代码也不复杂——这是关于pc(program counter)的相关加载。你可以将其看做是把变量___block_literal_global的地址加载到r0中。然后调用runBlockA函数。因为从之前的源码中,可以知道我们把block传递给了runBlockA,所以这里的___block_literal_global一定就是那个被传递的block对象了。
到目前为止,我们对上面的源码的运作有一些眉目了!不过这里的___block_literal_global是什么呢?继续看汇编代码,可以找到如下这样的内容:

 
 
  1. .align 2 @ @__block_literal_global
  2. ___block_literal_global:
  3. .long __NSConcreteGlobalBlock
  4. .long 1342177280 @ 0x50000000
  5. .long 0 @ 0x0
  6. .long ___doBlockA_block_invoke_0
  7. .long ___block_descriptor_tmp

Cool!上面的汇编代码看起来像是一个结构体。在结构体中又5个值,每个值有4个字节(long)。这肯定就是RunBlockA调用中涉及到的那个block对象。再细看一下,12个字节所在处就像一个函数指针:___doBlockA_block_invoke_0。这也是runBlockA函数中跳转执行的那个分支(bx r1)。

那么上面的汇编代码中__NSConcreteGlobalBlock又是何物?OK,现在先不介绍这个,后面会做介绍哦!下面我们来看看另外两个感兴趣的东西:___doBlockA_block_invoke_0___block_descriptor_tmp,这两个东东同样出现在了汇编代码中:

 
 
  1. .align 2
  2. .code 16 @ @__doBlockA_block_invoke_0
  3. .thumb_func ___doBlockA_block_invoke_0
  4. ___doBlockA_block_invoke_0:
  5. bx lr
  6.  
  7. .section __DATA,__const
  8. .align 2 @ @__block_descriptor_tmp
  9. ___block_descriptor_tmp:
  10. .long 0 @ 0x0
  11. .long 20 @ 0x14
  12. .long L_.str
  13. .long L_OBJC_CLASS_NAME_
  14.  
  15. .section __TEXT,__cstring,cstring_literals
  16. L_.str: @ @.str
  17. .asciz "v4@?0"
  18.  
  19. .section __TEXT,__objc_classname,cstring_literals
  20. L_OBJC_CLASS_NAME_: @ @"1L_OBJC_CLASS_NAME_"
  21. .asciz "01"

上面的代码中___doBlockA_block_invoke_0看起来有点像block的实现部分,只不过这里的block是空的,所以会立即返回(刚开始我们就期望编译一个空的block哦)。
接着看看___block_descriptor_tmp。这里可以看到另外一个数据结构——有4个值。其中第2个是20,这表示___block_literal_global的大小。接着是一个名为.str的C字符串,它的值为v4@?0,看起来有点像某个类型的编码形式。这可能是block 类型的编码(例如返回void和不携带任何参数)。上面代码中别的一些值我暂时还不清楚。

源码在这里

没错,这里有源代码!这是LLVM中compiler-rt项目的一部分。查看代码,我发现在Block_private.h文件中,有如下相关代码:

 
 
  1. struct Block_descriptor {
  2. unsigned long int reserved;
  3. unsigned long int size;
  4. void (*copy)(void *dst, void *src);
  5. void (*dispose)(void *);
  6. };
  7.  
  8. struct Block_layout {
  9. void *isa;
  10. int flags;
  11. int reserved;
  12. void (*invoke)(void *, ...);
  13. struct Block_descriptor *descriptor;
  14. /* Imported variables. */
  15. };

这看起来很熟悉吧!其中Block_layout结构体就是___block_literal_global,而Block_descriptor结构体则是__block_descriptor_tmp。细看Block_descriptor中的第2个变量size正如我之前描述的一样(表示___block_literal_global的大小)。在Block_descriptor中的第3和第4个值有点奇怪。这看起来有点想函数指针,但是在上面的汇编代码中看起来更像是两个字符串。现在我忽略掉这个细节。

Block_layout中的isa肯定就是__NSConcreteGlobalBlock,这也将确定block如何能够模拟Objective-C对象。如果__NSConcreteGlobalBlock是一个Class,那么Objective-C消息派送系统会将block对象当做一个普通的对象来处理。这跟如何处理toll-free bridging工作类似。更多相关toll-free bridging信息,可以阅读Mike Ash写的一篇优秀文章

将所有的代码片段拼凑起来,编译器做的工作内容看起来如下所示:

 
 
  1. #import <dispatch/dispatch.h>
  2.  
  3. __attribute__((noinline))
  4. void runBlockA(struct Block_layout *block) {
  5. block-&gt;invoke();
  6. }
  7.  
  8. void block_invoke(struct Block_layout *block) {
  9. // Empty block function
  10. }
  11.  
  12. void doBlockA() {
  13. struct Block_descriptor descriptor;
  14. descriptor-&gt;reserved = 0;
  15. descriptor-&gt;size = 20;
  16. descriptor-&gt;copy = NULL;
  17. descriptor-&gt;dispose = NULL;
  18. struct Block_layout block;
  19. block-&gt;isa = _NSConcreteGlobalBlock;
  20. block-&gt;flags = 1342177280;
  21. block-&gt;reserved = 0;
  22. block-&gt;invoke = block_invoke;
  23. block-&gt;descriptor = descriptor;
  24. runBlockA(&amp;block);
  25. }

非常不错!通过上面的介绍,我们可以了解很多关于block内部的东西。

何去何从

下一步我将介绍携带一个参数的block,以及从封闭范围内拷贝一个变量的block。这些内容跟本文介绍的又稍显不同!期待吧!

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值