本文主要在 MRC 和 ARC 環境下,通過實例來分析block
在內存中的存儲位置,閱讀本文的讀者需要提前了解block
的相關知識和使用技巧。
我們先定義一個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 環境下,能得到的結果都是__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
可以發現__NSGlobalBlock__
是NSBlock
的一個子類,而NSBlock
是block
基于Cocoa
的一層封裝。在blcok
的實現層來看,LLVM 為其給出了如下的結構體形式:
struct Block_literal_1 {
void *isa; // 初始化為 &_NSConcreteStackBlock 或 &_NSConcreteGlobalBlock
int flags; // 標志位
int reserved; // 占位用
void (*invoke)(void *, ...); // block 的實現函數指針
struct Block_descriptor_1 { // block 的附加描述信息
...
} *descriptor;
// imported variables
};
這與我們使用clang -rewrite-objc
分析出來的源碼有些不一致,但其內存布局基本相同(相關源碼中結構體的名稱叫Block_layout
)。由于block
也會被當做對象看待,該結構體中的isa
指針需要指向其所屬類型,那么_NSConcreteStackBlock
就表明了block
的具體類型。在 libclosure 源碼中還能找到其他類型的block
,block
的isa
指針始終指向下面這些指針數組的首地址,該指針也決定了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 環境下使用,我對這個也不太了解,暫且不討論。
因此,根據block
命名規則來看block
的存儲域大致有 3 個地方:全局區(數據區域 .data 區)、棧區和堆區。
接下來,我們要根據各種實例分析block
的存儲域,這里使用的打印block
的方式而非用clang -rewrite-objc
命令分析,原因是后者只是對源碼的一種改寫,并不能真正反映blcok
存儲域的變化,block
的isa
指針在這種情況下永遠只會被初始化成_NSConcreteStackBlock
或者_NSConcreteGlobalBlock
。
在上面的實例中,一個不捕獲任何外部變量的block
被存放在全局區。關于在全局區的block
,我覺得還可以補充一點,block
作為全局變量并初始化時,無論是否捕獲外部變量,在 MRC 和 ARC 環境下都會被存放在全局區,并且對處于全局區的block
進行 copy 操作是無效的(后面會解釋到)。以下代碼可以進行驗證。
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;
}
當然,還有 3 種情況下的block
會被放置在全局區中,這一部分文章后面會具體分析。
那么捕獲了外部變量的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 環境中t
被存放在棧區,這個不難理解。除了之前提到過的全局區block
在初始化時會被放置在全局區(impl.isa = _NSConcreteGlobalBlock),在其他情況下定義并初始化的block
都會被放置在棧區(impl.isa = _NSConcreteStackBlock)。然而在 ARC 環境中t
并不在棧中,它被放置于堆區,這是我們第一個遇到的存放于堆區里的block
(impl.isa = _NSConcreteMallocBlock)。block
結構關于isa
的注釋里明確表示isa
指針只會被初始化為_NSConcreteGlobalBlock
或_NSConcreteStackBlock
,那么_NSConcreteMallocBlock
一定是在運行時才存在的一種狀態。libclosure 源碼的runtime.c
文件中的_Block_copy
函數實現了更改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
執行 copy 操作需要進行哪些操作。第一個 if 很簡單,如果入參為空則返回空即可。前面提到的blcok
結構體重有一個flags
標志位,這個成員變量記錄著block
的狀態和其引用計數,一個變量記錄多種信息在 Apple 的源代碼中很常見。在這個函數中,如果flags
標志位包含BLOCK_NEEDS_FREE
,表明該block
存在于堆中,因此所需要做的就是增加其引用計數,返回原地址即可。如果block
的標志位包含BLOCK_IS_GLOBAL
,說明其存在于全局區,直接返回原來的block
即可。最后一種情況就是block
在棧中,需要重新開辟一塊內存空間將原來的block
的成員變量和函數地址全部復制到新內存空間,并重新設置其flags
,接著更改isa
指針類型為_NSConcreteMallocBlock
,最后返回新block
的首地址。
分析到這里可以發現,如果一個block
是一個堆 block
(這樣稱呼可能會比block
被存放在堆區更簡潔好聽一些??),那么它可能是從棧上 copy 過來的。這真是句沒用的廢話,不過這能解釋之前的疑問,為什么 ARC 環境下t
是一個堆 block
?原因是在 ARC 中,大多數情形下編譯器會自動將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
變量。
現在我想拋出兩個問題。
第一,如果t
里只捕獲了那個全局變量g_a
,t
會是什么類型的block
呢???
答:當然是全局 block
了。
第二,如果變量b
是靜態局部變量(static int a = 2;)t
會是什么類型的block
呢???
答:依舊是全局 block
咯。靜態變量和全局變量是都是放在全局區的嘛??。
到目前為止,3 中類型的block
都出現過了,現在我們來總結一下:
Block 的類型 | 條件 |
---|---|
全局 block |
block 被初始化為全局變量時;block 未捕獲任何外部變量時;block 只捕獲了全局/靜態變量時。 |
棧 block | MRC 中block 捕獲了auto 局部變量;ARC 中不存在 棧 block . |
堆 block | ARC 中block 捕獲了auto 局部變量;對除 全局 block 外的block 執行 copy 操作。 |
前面我們談論的都是block
在定義和初始化時的存儲域,接下來我們繼續分析block
在函數中作為形參和返回值的存儲域,這一部分非常簡單,如果你明白引用類型的參數傳遞和返回值的一些特點,這部分可以忽略不看了。
先來看看block
作為形參的存儲域,其實這個沒什么好說的。block
被當做 Objective-C 對象看待時,其是一個引用類型,其形參和實參是同一個首地址。
再來看block
作為返回值時的存儲域。定義如下函數:
Block_t func(Block_t aBlock)
{
#if __has_feature(objc_arc)
return aBlock;
#else
return [aBlock autorelease];
#endif
}
我們調用func
函數時,將一個未捕獲任何外部變量的block
作為該函數的參數:
Block_t t = ^{
NSLog(@"I am a block.");
};
NSLog(@"%@", t); // <__NSGlobalBlock__: 0x100001058>
Block_t t2 = func(t);
NSLog(@"%@", t2); // <__NSGlobalBlock__: 0x100001058>
在 ARC 和 MRC 環境下發現t
和t2
同為全局 block
,并且內存地址一致,也就是說全局 block
作為返回值時,它的存儲域并不會變化。這一點很好理解,全局 block
不依賴任何外部條件,它可以看做為字面量,其內存地址是唯一確定和共享的。
接下來,將一個捕獲auto
局部變量的block
作為該函數的參數:
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>
結果還是一樣,返回的block
和原來的block
始終是同一個。還有,綜合上面的這些例子我們可以發現 ARC 中已經不存在了棧 block
,在編譯期間棧 block
已被轉移至堆區。
那么最后現在我們來裝模作樣得總結一下block
在函數中的存儲域變化??:
原 Block 的類型 | 作為形參和返回值 |
---|---|
全局 block | 與原block 一致,為其本身。 |
棧 block | 與原block 一致,為其本身。 |
堆 block | 與原block 一致,為其本身。 |