深度圍觀block:第二集


今天翻譯了第二篇,這個翻譯是比較痛苦(其實不止這篇,所有的都是), 不比單純的閱讀,許多地方需要查閱資料,並細心的遣詞造句,還得注意詞不達意的地方(例如文中的A block that captures scope我翻譯爲block的拷貝範圍,總感覺缺了一些作者原意,功力有限啊)。所以,我勸大家要是能看原文儘量去看原文吧,我這翻譯的權當參考。
 
目錄
介紹
block類型
block的拷貝範圍
block拷貝對象的類型
何去何從
 
正文
介紹
接着-深度圍觀block:第一集,繼續從編譯器的角度深度圍觀block。在本文中,將介紹block並不是一成不變的,以及block在棧上的構成。
 
block類型
在第一篇文章中,我們已經看到block有一個_NSConcreteGlobalBlock這樣的類。由於所有變量都是已知的,所以在編譯期間,block的結構(structure)和描述(descriptor)都將全部被初始化。關於block這裏有幾種不同的類型,每種類型都有對應的類。爲了簡單起見,這裏只考慮其中三種:
 
_NSConcreteGlobalBlock是定義一個全局的block,在編譯器就已經完成相關初始化任務。這種類型的block不會涉及到任何拷貝,例如一個空的block。
_NSConcreteStackBlock是一個分配在棧上的block。這裏是所有最終被拷貝到堆(heap)上的block的開始。
_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. void doBlockA() { 
  11.     int a = 128; 
  12.     BlockA block = ^{ 
  13.         foo(a); 
  14.     }; 
  15.     runBlockA(block); 
 
爲了讓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指針,flags和reserved值
 
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能夠正確的繼續執行後續指令。
 
下面我們再看看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_:                     @ @"\01L_OBJC_CLASS_NAME_" 
  14.     .asciz   "\001P" 
  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. void doBlockA() { 
  11.     NSString *a = @"A"
  12.     BlockA block = ^{ 
  13.         foo(a); 
  14.     }; 
  15.     runBlockA(block); 
 
由於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. opy_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. estroy_helper_block_: 
  14. ldr     r0, [r0, #20] 
  15. movs    r1, #3 
  16. b.w     __Block_object_dispose 
 
這裏我先假設當block被拷貝和銷燬時,都會調用這裏的函數。那麼被block拷貝的對象肯定會發生reatain和release。上面的代碼中,可以看出如果r0和r1包含有效數據時,拷貝函數接收兩個參數(r0和r1)。而銷燬函數接收一個參數。可以看出所有的拷貝和銷燬任務都應該是由__Block_object_assign__Block_object_dispose兩個函數完成的。這兩個函數位於block的運行時代碼中(是LLVM裏面compiler-rt工程的一部分)。
 
如果你希望瞭解一下block運行時相關代碼,可以來這裏下載源碼:http://compiler-rt.llvm.org。特別關注一下里面的runtime.c文件。

何去何從
在下一集中我將調查Block_copy相關代碼,並看看相關工作處理情況,以此來深度圍觀一下block運行時。通過下一集的學習,你也將會深入瞭解拷貝和銷燬函數(也就是本文中我們剛剛看到的在block拷貝對象時使用的函數)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章