原文地址:http://www.galloway.me.uk/2012/10/a-look-inside-blocks-episode-1/
如原作者發(fā)現(xiàn)有侵權(quán)行為可責(zé)令我在24小時之內(nèi)刪除婚脱,前提是你能看到。
這是我從編譯器角度了解block內(nèi)部實(shí)現(xiàn)的第二篇文章。在這篇文章里障贸,我將會研究一下block在棧上的幾種類型错森。
Block類型
在第一篇文章里我們了解到,block中有一個isa指針指向了_NSConcreteGlobalBlock這個類篮洁,因為block結(jié)構(gòu)體和block中的descriptor結(jié)構(gòu)體在編譯時就全部初始化完成了涩维,所以我們可以看到里面的變量。block有幾種不同的類型嘀粱,每種類型都有與其關(guān)聯(lián)的類激挪,但是我們只需要考慮他們中常見的三種即可:
1._NSConcreteGlobalBlock是一種在編譯期間就完全定義好的全局block,這類block沒有捕獲任何外部變量锋叨,例如一個空的block垄分。
2._NSConcreteStackBlock是一種位于棧上的block,這種block在最終被copy到堆中之前一直都存儲在棧上薄湿。
3._NSConcreteMallocBlock是一種位于堆上的block,在對block進(jìn)行copy操作之后听诸,它的引用計數(shù)會增加坐求,知道引用計數(shù)減為0晌梨,這個block就會被釋放桥嗤。
有捕獲范圍的block
來看看下面的代碼:
名為foo的函數(shù)有一個參數(shù),在block中調(diào)用foo函數(shù)的時候?qū)⑼饷娴淖兞?em>a捕獲并傳給函數(shù)仔蝌,同樣的渊鞋,我們來看看在armv7架構(gòu)下匯編的過程锡宋,相關(guān)代碼如下:
這段匯編代碼和第一篇中的一樣奠滑,調(diào)用了block的invoke函數(shù)摊崭,接下來看看doBlockA函數(shù):
這段和之前的比起來有點(diǎn)難度了,和之前的加載全局block不同,這里做了更多的事根时,雖然看起來有點(diǎn)恐怖含友,但認(rèn)真看其實(shí)很容易看到它做了些什么窘问。因為編譯器沒有對這些指令進(jìn)行優(yōu)化惠赫,所以我將要對這些代碼在不改變原有功能的基礎(chǔ)上重新整理一下把鉴,下面是整理后的代碼:
主要的功能點(diǎn)如下:
1.一開始讓r7入棧是為了避免r7的內(nèi)容被覆蓋峰搪,因為r7里面的內(nèi)容在函數(shù)間調(diào)用時需要用到。lr(鏈接寄存器)保存了函數(shù)返回時要執(zhí)行的下一條指令的地址。最后將棧指針保存在r7中奉呛。
2.從棧頂指針減去24個字節(jié),也就是從棧中開辟出24字節(jié)用來存儲數(shù)據(jù)。
3.這里的代碼是為了對符號L__NSConcreteStackBlock$non_lazy_ptr進(jìn)行尋址,跟pc有關(guān),當(dāng)代碼最終被鏈接時不管這段代碼在二進(jìn)制文件中的什么位置計算機(jī)都能通過pc找到它半等,并將它存儲在棧指針?biāo)傅膬?nèi)存地址中杀饵。
4.值1073741824被存儲在棧頂指針+4的位置谜悟。
5.將0存儲到棧指針 + 8 的位置∥颠叮現(xiàn)在蔑水,將要發(fā)生什么可能已經(jīng)變得逐漸清晰了——在棧上創(chuàng)建了一個Block_layout結(jié)構(gòu)歇父!到現(xiàn)在為止,已經(jīng)設(shè)置了該結(jié)構(gòu)的3個值:isa指針,flags和reserved值肺樟。
6.___doBlockA_block_invoke_0存儲在棧指針+12的地址處,也就是block結(jié)構(gòu)體的invoke屬性。
7.___block_descriptor_tmp存儲在棧指針+16的地址處擎鸠,也就是block結(jié)構(gòu)體的descriptor屬性袜蚕。
8.值128存儲在棧指針+20的地址處雄可,你可能發(fā)現(xiàn)了Block_layout結(jié)構(gòu)體中只有5個值,并且這5個值已經(jīng)都存儲在棧中了室梅,那么接下來棧中要存儲什么敷待?你會發(fā)現(xiàn)正是從外面捕獲到的值128。所以這肯定就是存儲block使用到的值的地方——在Block_layout結(jié)構(gòu)尾部迅矛。
9.現(xiàn)在棧指針指向的是一個完整的block機(jī)構(gòu)體,然后將棧指針存入r0中悴势,然后調(diào)用runBlockA函數(shù)。(注意:在ARM EABI中r0通常用來存放函數(shù)的第一個參數(shù))粪躬。
10.最后笨腥,讓棧指針+24收回之前在棧中開辟的24字節(jié)的空間士鸥,接著將棧中的內(nèi)容分別pop到r7和pc中玻侥,pop回r7的就是函數(shù)一開始從r7push到棧中的內(nèi)容,pc的值是函數(shù)開始時push到棧中的lr的值音半,這樣在函數(shù)返回時程序可以繼續(xù)執(zhí)行下面的指令斥铺。
哇洼哎,如果你看到這的話抽兆,說明你太棒了壕探!
最后一部分讓俺們來看看block中的invoke函數(shù)和descriptor編譯的結(jié)果。我希望他們要比第一篇中的global block的這兩個屬性簡單郊丛±钋耄看代碼:
的確和第一篇中的沒有什么差別,唯一不同的是descriptor中的size值的大小不同厉熟,現(xiàn)在是24而不是20导盅,這是因為block捕獲到一個整數(shù)所以block的大小變?yōu)榱?4,之前也看到在block創(chuàng)建的時候有額外的4個字節(jié)在block的尾部一起被存入了棧中揍瑟。
在實(shí)際的block調(diào)用函數(shù)里白翻,比如__doBlockA_block_invoke_0,我們能看到在調(diào)用___doBlockA_block_invoke_0函數(shù)中先從r0 + 20地址初開始讀取了4個字節(jié)的數(shù)據(jù)到r0中,這額外的4個字節(jié)也就是block從外部捕獲到那個整數(shù)滤馍,然后調(diào)用foo函數(shù)岛琼。
如果捕獲的是一個對象類型呢?
我們需要考慮的下一個問題是如果block捕獲的外部變量是一個對象類型比如NSString而不是一個整數(shù)巢株,那么會發(fā)生什么槐瑞,看看下面的代碼:
doBlockA函數(shù)我就不講了,和之前的一樣阁苞,沒有太多的變化困檩。有點(diǎn)意思的是這個block的descriptor結(jié)構(gòu)體指針:
注意這邊有兩個函數(shù)指針:__copy_helper_block和__destroy_helper_block。下面是這些函數(shù)的定義:
我假設(shè)block被拷貝和銷毀正是因為有這兩個函數(shù)那槽,那么他們一定會對block捕獲的對象進(jìn)行retain和release操作悼沿。拷貝函數(shù)接收兩個參數(shù)(r0和r1)骚灸,而銷毀函數(shù)接收一個參數(shù)糟趾。可以看出所有的拷貝和銷毀任務(wù)都應(yīng)該是由__Block_object_assign和__Block_object_dispose兩個函數(shù)完成的甚牲。這兩個函數(shù)位于block的運(yùn)行時代碼中拉讯,是LLVM里面compiler-rt工程的一部分。
如果你想繼續(xù)了解runtime中有關(guān)block的代碼鳖藕,可以去http://compiler-rt.llvm.org下載相關(guān)源代碼魔慷。尤其要去看看runtime.c這個文件。
下一篇講啥?
下一篇準(zhǔn)備深入了解一下block在runtime中的Block_copy著恩,看看它是如何工作的院尔。這也能讓我們能更好的理解上面在block捕獲的外部對象時創(chuàng)建的__copy_helper_block和__destroy_helper_block函數(shù)。