在第一篇文章和第二篇文章我們已經(jīng)研究了一些blocks的內(nèi)部原理了忆矛。本文將進一步研究block拷貝的過程。你可能聽到過一些術(shù)語比如"blocks 起始于棧"以及"如果想保存它們以后用你必須拷貝"魄梯。但是為什么呢?拷貝到底做了什么事奶赠?我長久以來一直在好奇拷貝block的機制到底是什么雳殊。比如block捕獲的值會怎么樣。本文我將對此做些闡述臀晃。
我們已經(jīng)知道的事
從第一篇文章和第二篇文章中我們知道一個block的內(nèi)存布局長這樣:
在第二篇文章中我們看到block最開始被引用的時候是在棧上創(chuàng)建的觉渴。既然是在棧上,那么在block的封閉域結(jié)束后內(nèi)存就會被回收重用积仗。那你之后再用這個block會發(fā)生什么呢?好吧蜕猫,你必須拷貝它寂曹。這是通過調(diào)用Block_copy()
方法或者直接向他發(fā)送OC的copy
消息完成。這就是所謂的Block_copy()
回右。
Block_copy()
首先我們來看Block.h隆圆。其中有下面的定義:
#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
void *_Block_copy(const void *arg);
所以Block_copy
是一個宏,它將傳入的參數(shù)轉(zhuǎn)換為一個const void *
然后傳遞給_Block_copy()
方法翔烁。_Block_copy()
的實現(xiàn)在runtime.c:
void *_Block_copy(const void *arg) {
return _Block_copy_internal(arg, WANTS_ONE);
}
所以也就是調(diào)用_Block_copy_internal
方法渺氧,傳入block自己和WANTS_ONE
。為了明白這什么意思蹬屹,我們需要看一下實現(xiàn)代碼侣背。也在runtime.c白华。下面是方法的實現(xiàn),已經(jīng)刪掉不想干的部分(主要是垃圾收集的部分):
static void *_Block_copy_internal(const void *arg, const int flags) {
struct Block_layout *aBlock;
const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
// 1
if (!arg) return NULL;
// 2
aBlock = (struct Block_layout *)arg;
// 3
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
// 4
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
// 5
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return (void *)0;
// 6
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// 7
result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 1;
// 8
result->isa = _NSConcreteMallocBlock;
// 9
if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
(*aBlock->descriptor->copy)(result, aBlock); // do fixup
}
return result;
}
主要做了以下工作:
如果傳入?yún)?shù)是
NULL
就直接返回NULL
贩耐。防止傳入一個NULL
的Block弧腥。將參數(shù)轉(zhuǎn)換為一個
struct Block_layout
類型的指針。你也許還記得第一篇文章中提到它潮太。它就是block內(nèi)部一個包含了實現(xiàn)函數(shù)和一些元數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)管搪。如果block的
flags
字段包含BLOCK_NEEDS_FREE
,那么這是一個堆block(稍后你就明白)铡买。這里只需要增加引用計數(shù)然后返回原blcok更鲁。如果這是一個全局block(回看第一篇文章),那么不需要做任何事奇钞,直接返回原block澡为。因為全局block是一個單例。
如果走到這里蛇券,那么這一定是一個棧上分配的block缀壤。那樣的話,block需要拷貝到堆上纠亚。這才是有趣的部分塘慕。第一步,調(diào)用
malloc()
創(chuàng)建一塊特定的內(nèi)存蒂胞。如果創(chuàng)建失敗图呢,返回NULL
;否則骗随,繼續(xù)蛤织。調(diào)用
memmove()
方法將當前棧上分配的block按位拷貝到我們剛剛創(chuàng)建的堆內(nèi)存上。這樣可以保證所有的元數(shù)據(jù)都拷貝過來鸿染,比如descriptor指蚜。接下來,更新標志位涨椒。第一行確保引用計數(shù)為0摊鸡。注釋表明這行其實不需要——大概這個時候引用計數(shù)已經(jīng)是0了。我猜保留這行是因為以前有個bug導致這里的引用計數(shù)不是0(所以說runtime的代碼也會偷懶)蚕冬。下一行設(shè)置了
BLOCK_NEEDS_FREE
標志位免猾,表明這是一個堆block,一旦引用計數(shù)減為0囤热,它所占用的內(nèi)存將被釋放猎提。|1
操作設(shè)置block的引用計數(shù)為1。block的
isa
指針被設(shè)置為_NSConcreteMallocBlock
旁蔼,說明這是個堆block锨苏。最后疙教,如果block有一個拷貝輔助函數(shù),那么它將被調(diào)用蚓炬。必要的時候編譯器會生成拷貝輔助函數(shù)松逊。比如一個捕獲了對象的block就需要。那么拷貝輔助函數(shù)將持有被捕獲的對象肯夏。
哈哈经宏,已經(jīng)十分清晰了。現(xiàn)在你知道拷貝一個block到底發(fā)生了什么事驯击!但那只是圖片展示的一半內(nèi)容烁兰,對吧?釋放一個block又會怎么樣呢徊都?
Block_release()
Block_copy()
圖的另一半是Block_release()
沪斟。實際上這又是一個宏:
#define Block_release(...) _Block_release((const void *)(__VA_ARGS__))
跟Block_copy()
一樣,Block_release()
也是轉(zhuǎn)換傳入的參數(shù)然后調(diào)用一個方法暇矫。這一定程度上解放了程序員的雙手主之,他們不用自己做轉(zhuǎn)換。
我們來看看_Block_release()
的源碼(簡明起見李根,重新整理了代碼順序槽奕,并刪除了垃圾回收相關(guān)的代碼):
// 1
struct Block_layout *aBlock = (struct Block_layout *)arg;
if (!aBlock) return;
// 2
int32_t newCount;
newCount = latching_decr_int(&aBlock->flags) & BLOCK_REFCOUNT_MASK;
// 3
if (newCount > 0) return;
// 4
if (aBlock->flags & BLOCK_NEEDS_FREE) {
if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)(*aBlock->descriptor->dispose)(aBlock);
_Block_deallocator(aBlock);
}
// 5
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
;
}
// 6
else {
printf("Block_release called upon a stack Block: %p, ignored\n", (void *)aBlock);
}
}
這段代碼做了這些事:
首先,參數(shù)被轉(zhuǎn)換為一個指向
struct Block_layout
的指針房轿。如果傳入NULL粤攒,直接返回。標志位部分表示引用計數(shù)減1(之前
Block_copy()
中標志位操作代表的是引用計數(shù)置為1)囱持。如果新的引用計數(shù)值大于0夯接,說明有其他東西在引用block,所以block不應該被釋放纷妆。
否則盔几,如果標志位包含
BLOCK_NEEDS_FREE
,那么這是一個堆block而且引用計數(shù)為0掩幢,應該被釋放逊拍。首先block的處理輔助函數(shù)(dispose helper)被調(diào)用,它是拷貝輔助函數(shù)(copy helper)的反義詞粒蜈,執(zhí)行相反的操作顺献,比如釋放被捕獲的對象旗国。最后調(diào)用_Block_deallocator
方法釋放block枯怖。如果你查找runtime.c你就會發(fā)現(xiàn)這個方法最后就是一個free
的函數(shù)指針,釋放malloc
分配的內(nèi)存能曾。如果到這一步且lock是全局的度硝,什么也不做肿轨。
如果到這一步,一定是發(fā)生了未知狀況蕊程,因為一個棧block試圖在這里釋放椒袍,輸出一行警告。實際上藻茂,你應該永遠不會走到這一步驹暑。
這些就是Block! 東西也并不多嘛(呵呵)。