本文主要在 MRC 和 ARC 環(huán)境下困檩,通過實例來分析block
在內(nèi)存中的存儲位置虱而,閱讀本文的讀者需要提前了解block
的相關(guān)知識和使用技巧葛菇。
我們先定義一個Block_t
類型:
typedef void (^Block_t)(void);
然后再定義一個不捕獲任何外部變量的block
油额,嘗試去打印這個block
isa 指針指向的類型:
Block_t t = ^{
NSLog(@"I'm a block.");
};
Class cls = object_getClass(t);
NSLog(@"%@", cls); // __NSGlobalBlock__
無論在 MRC 還是在 ARC 環(huán)境下泞坦,能得到的結(jié)果都是__NSGlobalBlock__
斑鸦。這個__NSGlobalBlock__
是什么東西呢愕贡?使用以下方式打印cls
的各級父類:
Class superCls = class_getSuperclass(cls);
Class superSuperCls = class_getSuperclass(superCls);
Class rootCls = class_getSuperclass(superSuperCls);
NSLog(@"%@", superCls); // __NSGlobalBlock
NSLog(@"%@", superSuperCls); // NSBlock
NSLog(@"%@", rootCls); // NSObject
可以發(fā)現(xiàn)__NSGlobalBlock__
是NSBlock
的一個子類,而NSBlock
是block
基于Cocoa
的一層封裝巷屿。在blcok
的實現(xiàn)層來看固以,LLVM 為其給出了如下的結(jié)構(gòu)體形式:
struct Block_literal_1 {
void *isa; // 初始化為 &_NSConcreteStackBlock 或 &_NSConcreteGlobalBlock
int flags; // 標(biāo)志位
int reserved; // 占位用
void (*invoke)(void *, ...); // block 的實現(xiàn)函數(shù)指針
struct Block_descriptor_1 { // block 的附加描述信息
...
} *descriptor;
// imported variables
};
這與我們使用clang -rewrite-objc
分析出來的源碼有些不一致,但其內(nèi)存布局基本相同(相關(guān)源碼中結(jié)構(gòu)體的名稱叫Block_layout
)嘱巾。由于block
也會被當(dāng)做對象看待憨琳,該結(jié)構(gòu)體中的isa
指針需要指向其所屬類型,那么_NSConcreteStackBlock
就表明了block
的具體類型旬昭。在 libclosure 源碼中還能找到其他類型的block
篙螟,block
的isa
指針始終指向下面這些指針數(shù)組的首地址,該指針也決定了block
的類名稱问拘。
BLOCK_EXPORT void * _NSConcreteMallocBlock[32];
BLOCK_EXPORT void * _NSConcreteAutoBlock[32];
BLOCK_EXPORT void * _NSConcreteFinalizingBlock[32];
BLOCK_EXPORT void * _NSConcreteWeakBlockVariable[32];
// declared in Block.h
// BLOCK_EXPORT void * _NSConcreteGlobalBlock[32];
// BLOCK_EXPORT void * _NSConcreteStackBlock[32];
其中
_NSConcreteFinalizingBlock
遍略,_NSConcreteWeakBlockVariable
和_NSConcreteAutoBlock
只在 GC 環(huán)境下使用,我對這個也不太了解骤坐,暫且不討論绪杏。
因此,根據(jù)block
命名規(guī)則來看block
的存儲域大致有 3 個地方:全局區(qū)(數(shù)據(jù)區(qū)域 .data 區(qū))纽绍、棧區(qū)和堆區(qū)蕾久。
接下來,我們要根據(jù)各種實例分析block
的存儲域拌夏,這里使用的打印block
的方式而非用clang -rewrite-objc
命令分析僧著,原因是后者只是對源碼的一種改寫叫编,并不能真正反映blcok
存儲域的變化,block
的isa
指針在這種情況下永遠(yuǎn)只會被初始化成_NSConcreteStackBlock
或者_NSConcreteGlobalBlock
霹抛。
在上面的實例中搓逾,一個不捕獲任何外部變量的block
被存放在全局區(qū)。關(guān)于在全局區(qū)的block
杯拐,我覺得還可以補充一點霞篡,block
作為全局變量并初始化時,無論是否捕獲外部變量端逼,在 MRC 和 ARC 環(huán)境下都會被存放在全局區(qū)朗兵,并且對處于全局區(qū)的block
進(jìn)行 copy 操作是無效的(后面會解釋到)。以下代碼可以進(jìn)行驗證顶滩。
int a = 1;
Block_t t = ^{
NSLog(@"I'm a block.");
};
Block_t t1 = ^{
a = 2;
NSLog(@"I'm a block too.");
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%@", t); // <__NSGlobalBlock__: 0x100001050>
NSLog(@"%@", t1); // <__NSGlobalBlock__: 0x100001090>
NSLog(@"%@", [t1 copy]); // <__NSGlobalBlock__: 0x100001090>
}
return 0;
}
當(dāng)然余掖,還有 3 種情況下的block
會被放置在全局區(qū)中,這一部分文章后面會具體分析礁鲁。
那么捕獲了外部變量的block
會被存放在哪里盐欺?這個問題需要分幾種情況具體分析。
我們先來看block
捕獲了一個auto
局部變量的情況仅醇,代碼如下:
{
int a = 1;
Block_t t = ^{
NSLog(@"I'm a block. %d", a);
};
NSLog(@"%@", t);
}
在 MRC 和 ARC 中分別打印如下:
MRC: <__NSStackBlock__: 0x7ffeefbff558>
ARC: <__NSMallocBlock__: 0x100443280>
在 MRC 環(huán)境中t
被存放在棧區(qū)冗美,這個不難理解。除了之前提到過的全局區(qū)block
在初始化時會被放置在全局區(qū)(impl.isa = _NSConcreteGlobalBlock)析二,在其他情況下定義并初始化的block
都會被放置在棧區(qū)(impl.isa = _NSConcreteStackBlock)粉洼。然而在 ARC 環(huán)境中t
并不在棧中,它被放置于堆區(qū)叶摄,這是我們第一個遇到的存放于堆區(qū)里的block
(impl.isa = _NSConcreteMallocBlock)属韧。block
結(jié)構(gòu)關(guān)于isa
的注釋里明確表示isa
指針只會被初始化為_NSConcreteGlobalBlock
或_NSConcreteStackBlock
,那么_NSConcreteMallocBlock
一定是在運行時才存在的一種狀態(tài)蛤吓。libclosure 源碼的runtime.c
文件中的_Block_copy
函數(shù)實現(xiàn)了更改block isa
指針的操作:
// Copy, or bump refcount, of a block. If really copying, call the copy helper if present.
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
if (!arg) return NULL;
// The following would be better done as a switch statement
aBlock = (struct Block_layout *)arg;
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
else {
// Its a stack block. Make a copy.
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return NULL;
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// reset refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
result->isa = _NSConcreteMallocBlock;
return result;
}
}
以上代碼表明了對一個block
執(zhí)行 copy 操作需要進(jìn)行哪些操作宵喂。第一個 if 很簡單,如果入?yún)榭談t返回空即可柱衔。前面提到的blcok
結(jié)構(gòu)體重有一個flags
標(biāo)志位樊破,這個成員變量記錄著block
的狀態(tài)和其引用計數(shù),一個變量記錄多種信息在 Apple 的源代碼中很常見唆铐。在這個函數(shù)中哲戚,如果flags
標(biāo)志位包含BLOCK_NEEDS_FREE
,表明該block
存在于堆中艾岂,因此所需要做的就是增加其引用計數(shù)顺少,返回原地址即可。如果block
的標(biāo)志位包含BLOCK_IS_GLOBAL
,說明其存在于全局區(qū)脆炎,直接返回原來的block
即可梅猿。最后一種情況就是block
在棧中,需要重新開辟一塊內(nèi)存空間將原來的block
的成員變量和函數(shù)地址全部復(fù)制到新內(nèi)存空間秒裕,并重新設(shè)置其flags
袱蚓,接著更改isa
指針類型為_NSConcreteMallocBlock
,最后返回新block
的首地址几蜻。
分析到這里可以發(fā)現(xiàn)喇潘,如果一個block
是一個堆 block
(這樣稱呼可能會比block
被存放在堆區(qū)更簡潔好聽一些??),那么它可能是從棧上 copy 過來的梭稚。這真是句沒用的廢話颖低,不過這能解釋之前的疑問,為什么 ARC 環(huán)境下t
是一個堆 block
弧烤?原因是在 ARC 中忱屑,大多數(shù)情形下編譯器會自動將block
copy 到堆中,也就是編譯器自己幫我們 copy 一個block
的副本暇昂,我們使用的是它的副本莺戒,并不是原來的block
。
上面的例子還能引申出另外一種情況话浇,如果block
同時捕獲了auto
局部變量和全局變量脏毯,它又會在哪里,還和上面那個例子一樣么幔崖?
int g_a = 1;
int main(int argc, const char * argv[])
{
int b = 2;
Block_t t = ^{
NSLog(@"a = %d, b = %d", g_a, b);
};
NSLog(@"%@", t);
return 0;
}
答案很簡單,確實是一樣的渣淤。MRC 中t
是棧 block
赏寇,ARC 中t
是堆 block
,它一定不會是全局 block
价认,因為在使用全局變量的地方不能使用auto
變量嗅定。
現(xiàn)在我想拋出兩個問題。
第一用踩,如果t
里只捕獲了那個全局變量g_a
渠退,t
會是什么類型的block
呢???
答:當(dāng)然是全局 block
了脐彩。
第二碎乃,如果變量b
是靜態(tài)局部變量(static int a = 2;)t
會是什么類型的block
呢???
答:依舊是全局 block
咯惠奸。靜態(tài)變量和全局變量是都是放在全局區(qū)的嘛??梅誓。
到目前為止,3 中類型的block
都出現(xiàn)過了,現(xiàn)在我們來總結(jié)一下:
Block 的類型 | 條件 |
---|---|
全局 block |
block 被初始化為全局變量時梗掰;block 未捕獲任何外部變量時嵌言;block 只捕獲了全局/靜態(tài)變量時。 |
棧 block | MRC 中block 捕獲了auto 局部變量及穗;ARC 中不存在 棧 block . |
堆 block | ARC 中block 捕獲了auto 局部變量摧茴;對除 全局 block 外的block 執(zhí)行 copy 操作。 |
前面我們談?wù)摰亩际?code>block在定義和初始化時的存儲域埂陆,接下來我們繼續(xù)分析block
在函數(shù)中作為形參和返回值的存儲域蓬蝶,這一部分非常簡單,如果你明白引用類型的參數(shù)傳遞和返回值的一些特點猜惋,這部分可以忽略不看了丸氛。
先來看看block
作為形參的存儲域,其實這個沒什么好說的著摔。block
被當(dāng)做 Objective-C 對象看待時缓窜,其是一個引用類型,其形參和實參是同一個首地址谍咆。
再來看block
作為返回值時的存儲域禾锤。定義如下函數(shù):
Block_t func(Block_t aBlock)
{
#if __has_feature(objc_arc)
return aBlock;
#else
return [aBlock autorelease];
#endif
}
我們調(diào)用func
函數(shù)時,將一個未捕獲任何外部變量的block
作為該函數(shù)的參數(shù):
Block_t t = ^{
NSLog(@"I am a block.");
};
NSLog(@"%@", t); // <__NSGlobalBlock__: 0x100001058>
Block_t t2 = func(t);
NSLog(@"%@", t2); // <__NSGlobalBlock__: 0x100001058>
在 ARC 和 MRC 環(huán)境下發(fā)現(xiàn)t
和t2
同為全局 block
摹察,并且內(nèi)存地址一致恩掷,也就是說全局 block
作為返回值時,它的存儲域并不會變化供嚎。這一點很好理解黄娘,全局 block
不依賴任何外部條件,它可以看做為字面量克滴,其內(nèi)存地址是唯一確定和共享的逼争。
接下來,將一個捕獲auto
局部變量的block
作為該函數(shù)的參數(shù):
int a = 1;
Block_t t = ^{
NSLog(@"a = %d", a);
};
NSLog(@"%@", t);
// ARC: <__NSMallocBlock__: 0x10060ef10>
// MRC: <__NSStackBlock__: 0x7ffeefbff558>
Block_t t2 = func(t);
NSLog(@"%@", t2);
// ARC: <__NSMallocBlock__: 0x10060ef10>
// MRC: <__NSStackBlock__: 0x7ffeefbff558>
結(jié)果還是一樣劝赔,返回的block
和原來的block
始終是同一個誓焦。還有,綜合上面的這些例子我們可以發(fā)現(xiàn) ARC 中已經(jīng)不存在了棧 block
着帽,在編譯期間棧 block
已被轉(zhuǎn)移至堆區(qū)杂伟。
那么最后現(xiàn)在我們來裝模作樣得總結(jié)一下block
在函數(shù)中的存儲域變化??:
原 Block 的類型 | 作為形參和返回值 |
---|---|
全局 block | 與原block 一致,為其本身仍翰。 |
棧 block | 與原block 一致赫粥,為其本身。 |
堆 block | 與原block 一致歉备,為其本身傅是。 |