Block 的存儲域

本文主要在 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的一個子類,而NSBlockblock基于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 源碼中還能找到其他類型的blockblockisa指針始終指向下面這些指針數組的首地址,該指針也決定了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存儲域的變化,blockisa指針在這種情況下永遠只會被初始化成_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_at會是什么類型的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 環境下發現tt2同為全局 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一致,為其本身。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容