本文翻譯自Matt Galloway的博客
之前的文章(譯)窺探Blocks(1)我們已經(jīng)了解了block的內(nèi)部原理,以及編譯器如何處理它。本文我將討論一下非常量的blocks以及它們?cè)跅I系慕M織方式。
Block 類型
在第一篇文章中,我們看到block有__NSConcreteGlobalBlock
類。block結(jié)構(gòu)體和descriptor都在編譯階段基于已知的變量完全初始化了。block還有一些不同的類型,每一個(gè)類型都對(duì)應(yīng)一個(gè)相關(guān)的類。為了簡(jiǎn)單起見,我們只考慮其中的三個(gè):
-
_NSConcreteGlobalBlock
是一個(gè)全局定義的block,在編譯階段就完成創(chuàng)建工作。這些block沒有捕獲任何域,比如一個(gè)空block。 -
_NSConcreteStackBlock
是一個(gè)在棧上的block,這是所有blocks在最終拷貝到堆上之前所開始的地方。 -
_NSConcreteMallocBlock
是一個(gè)在堆上的block,這是拷貝一個(gè)block后最終的位置。它們?cè)谶@里被引用計(jì)數(shù)并且在引用計(jì)數(shù)變?yōu)?時(shí)被釋放。
捕獲域的block
現(xiàn)在我們來看看下面一段代碼:
#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);
}
這里有一個(gè)方法foo
,因此block捕獲了一些東西,用一個(gè)捕獲到的變量來調(diào)用方法。我又看了一下armv7所產(chǎn)生的一小段相關(guān)代碼:
.globl _runBlockA
.align 2
.code 16 @ @runBlockA
.thumb_func _runBlockA
_runBlockA:
ldr r1, [r0, #12]
bx r1
首先,runBlockA
方法與之前的結(jié)果一樣,它調(diào)用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}
這下看起來比之前的復(fù)雜多了。與從一個(gè)全局符號(hào)加載一個(gè)block不同,這看起來做了許多工作。看起來可能有點(diǎn)麻煩,但其實(shí)也非常簡(jiǎn)單。我們最好考慮重新整理這些方法,但請(qǐng)相信我這樣做不會(huì)沒有改變?nèi)魏喂δ堋>幾g器之所以這樣安排它的指令順序,是為了優(yōu)化編譯性能,減少流水線氣泡。重新整理后的方法如下:
_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}
這就是它所做的事:
方法開始。
r7
被壓入棧,因?yàn)樗磳⒈恢貙懀易鳛橐粋€(gè)寄存器必須在方法調(diào)用時(shí)候保存值。lr
是一個(gè)鏈接寄存器,也被壓入棧,保存了下一個(gè)指令的地址,好讓方法返回時(shí)繼續(xù)執(zhí)行下一個(gè)指令。可以在方法結(jié)尾看到。 棧指針(sp)也被保存在r7
中。棧指針(sp)減去24,留出24字節(jié)的棧空間存儲(chǔ)數(shù)據(jù)。
這一小塊代碼正在相對(duì)于程序計(jì)數(shù)器查找
L__NSConcreteStackBlock$non_lazy_ptr
符號(hào),這樣最后鏈接成功的二進(jìn)制文件,不管代碼結(jié)束于任何地方,它都可以正常工作(這句話有點(diǎn)繞,翻譯的不好,需要好好理解一下)。這個(gè)值最后存儲(chǔ)在棧指針指向的位置。1073741824
存儲(chǔ)在sp + 4 的位置上。0
存儲(chǔ)在sp + 8的位置上。現(xiàn)在可能情況比較清晰了。回顧上一篇文章中提到的Block_layout
結(jié)構(gòu)體,可以看出一個(gè)Block_layout
結(jié)構(gòu)體在棧上創(chuàng)建了!目前為止已經(jīng)有了isa
指針,flags
和reserved
值被設(shè)置了。___doBlockA_block_invoke_0
的地址存儲(chǔ)在sp + 12位置。這就是block結(jié)構(gòu)體的invoke
參數(shù)。___block_descriptor_tmp
的地址存儲(chǔ)在sp + 16位置。這就是block結(jié)構(gòu)體的descriptor
參數(shù)。128
存儲(chǔ)在sp + 20的位置。啊!如果你回看Block_layout
結(jié)構(gòu)體你會(huì)發(fā)現(xiàn)里面只有5個(gè)值。那么存在這個(gè)結(jié)構(gòu)體末尾的是什么呢?哈哈,別忘記了,這個(gè)128
就是在這個(gè)block前定義的、被block捕獲的值。所以這一定是存儲(chǔ)它們使用變量的地方——在Block_layout
最后。sp現(xiàn)在指向一個(gè)完全初始化的block結(jié)構(gòu)體,它被放入
r0
寄存器,然后runBlockA
被調(diào)用。(記住在ARM EABI中r0包含了方法的第一個(gè)參數(shù))最后sp + 24 已抵消最開始減去的24。然后分別從棧彈出兩個(gè)值到
r7
和pc
中。r7
抵消一開始?jí)簵5牟僮鳎?code>pc將獲得方法開始時(shí)lr
里面的值。這樣有效地完成了方法返回的操作,讓CPU繼續(xù)(程序計(jì)數(shù)器pc)從方法返回的地方(鏈接寄存器lr)執(zhí)行。
哇哦!你還在跟著我學(xué)?太牛逼啦!
這一小段的最后一部分是來看看invoke方法和descriptor長(zhǎng)什么樣。我們希望它們不要與第一篇文章中的全局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_
還真是相差不大。唯一的區(qū)別在于block descriptor的size
值。現(xiàn)在它是24而不是20。因?yàn)閎lock此時(shí)捕獲了一個(gè)整形數(shù)值。我們已經(jīng)看到在創(chuàng)建block結(jié)構(gòu)體時(shí),這額外的4字節(jié)被放在了最后。
同樣地,你在實(shí)際執(zhí)行的方法__doBlockA_block_invoke_0
中也會(huì)發(fā)現(xiàn)參數(shù)值從結(jié)構(gòu)體末尾處(r0 + 20)讀取出來,這就是block捕獲的值。
捕獲對(duì)象類型的值會(huì)怎樣?
下面要考慮的是捕獲的不再是一個(gè)整形,而是一個(gè)對(duì)象,比如NSString
。欲知詳情,請(qǐng)看下面代碼:
#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
的細(xì)節(jié),因?yàn)樽兓淮蟆1容^有意思的是它創(chuàng)建的block descriptor結(jié)構(gòu)體。
.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_
注意現(xiàn)在有了名為___copy_helper_block_
和___destroy_helper_block_
的函數(shù)指針。這里是這些函數(shù)的定義:
.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拷貝和銷毀的時(shí)候調(diào)用,它們一定是在持有或釋放被block捕獲的對(duì)象。看起來拷貝函數(shù)用了兩個(gè)參數(shù),因?yàn)?code>r0和r1
被尋址,它們兩可能有有效的數(shù)據(jù)。銷毀函數(shù)好像就一個(gè)參數(shù)。所有復(fù)雜的操作貌似都是_Block_object_assign
和_Block_object_dispose
干的。這部分代碼在block runtime里。
如果你想了解更多關(guān)于block runtime的代碼,可以去http://compiler-rt.llvm.org下載源碼,重點(diǎn)看看runtime.c
。
下一篇我們將研究一下Block_Copy
的原理。