本文翻譯自Matt Galloway的部落格
之前的文章(譯)窺探Blocks(1)我們已經瞭解了block的內部原理,以及編譯器如何處理它。本文我將討論一下非常量的blocks以及它們在棧上的組織方式。
Block 型別
在第一篇文章中,我們看到block有__NSConcreteGlobalBlock
類。block結構體和descriptor都在編譯階段基於已知的變數完全初始化了。block還有一些不同的型別,每一個型別都對應一個相關的類。為了簡單起見,我們只考慮其中的三個:
_NSConcreteGlobalBlock
是一個全域性定義的block,在編譯階段就完成建立工作。這些block沒有捕獲任何域,比如一個空block。_NSConcreteStackBlock
是一個在棧上的block,這是所有blocks在最終拷貝到堆上之前所開始的地方。_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}複製程式碼
這就是它所做的事:
-
方法開始。
r7
被壓入棧,因為它即將被重寫,而且作為一個暫存器必須在方法呼叫時候儲存值。lr
是一個連結暫存器,也被壓入棧,儲存了下一個指令的地址,好讓方法返回時繼續執行下一個指令。可以在方法結尾看到。 棧指標(sp)也被儲存在r7
中。 -
棧指標(sp)減去24,留出24位元組的棧空間儲存資料。
-
這一小塊程式碼正在相對於程式計數器查詢
L__NSConcreteStackBlock$non_lazy_ptr
符號,這樣最後連結成功的二進位制檔案,不管程式碼結束於任何地方,它都可以正常工作(這句話有點繞,翻譯的不好,需要好好理解一下)。這個值最後儲存在棧指標指向的位置。 -
1073741824
儲存在sp + 4 的位置上。 -
0
儲存在sp + 8的位置上。現在可能情況比較清晰了。回顧上一篇文章中提到的Block_layout
結構體,可以看出一個Block_layout
結構體在棧上建立了!目前為止已經有了isa
指標,flags
和reserved
值被設定了。 -
___doBlockA_block_invoke_0
的地址儲存在sp + 12位置。這就是block結構體的invoke
引數。 -
___block_descriptor_tmp
的地址儲存在sp + 16位置。這就是block結構體的descriptor
引數。 -
128
儲存在sp + 20的位置。啊!如果你回看Block_layout
結構體你會發現裡面只有5個值。那麼存在這個結構體末尾的是什麼呢?哈哈,別忘記了,這個128
就是在這個block前定義的、被block捕獲的值。所以這一定是儲存它們使用變數的地方——在Block_layout
最後。 -
sp現在指向一個完全初始化的block結構體,它被放入
r0
暫存器,然後runBlockA
被呼叫。(記住在ARM EABI中r0包含了方法的第一個引數) -
最後sp + 24 已抵消最開始減去的24。然後分別從棧彈出兩個值到
r7
和pc
中。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_: @ @" 1L_OBJC_CLASS_NAME_"
.asciz " 01P"
.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捕獲的物件。看起來拷貝函式用了兩個引數,因為r0
和r1
被定址,它們兩可能有有效的資料。銷燬函式好像就一個引數。所有複雜的操作貌似都是_Block_object_assign
和_Block_object_dispose
乾的。這部分程式碼在block runtime裡。
如果你想了解更多關於block runtime的程式碼,可以去compiler-rt.llvm.org下載原始碼,重點看看runtime.c
。
下一篇我們將研究一下Block_Copy
的原理。