Block 的存儲域

本文主要在 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的一個子類,而NSBlockblock基于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篙螟,blockisa指針始終指向下面這些指針數(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存儲域的變化,blockisa指針在這種情況下永遠(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)tt2同為全局 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一致歉备,為其本身傅是。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子喧笔,更是在濱河造成了極大的恐慌帽驯,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件书闸,死亡現(xiàn)場離奇詭異尼变,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)浆劲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門嫌术,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人牌借,你說我怎么就攤上這事度气。” “怎么了膨报?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵磷籍,是天一觀的道長。 經(jīng)常有香客問我现柠,道長院领,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任够吩,我火速辦了婚禮比然,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘周循。我一直安慰自己强法,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布鱼鼓。 她就那樣靜靜地躺著拟烫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪迄本。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天课竣,我揣著相機(jī)與錄音嘉赎,去河邊找鬼。 笑死于樟,一個胖子當(dāng)著我的面吹牛公条,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播迂曲,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼靶橱,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起关霸,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤传黄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后队寇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體膘掰,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年佳遣,在試婚紗的時候發(fā)現(xiàn)自己被綠了识埋。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡零渐,死狀恐怖窒舟,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情诵盼,我是刑警寧澤惠豺,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站拦耐,受9級特大地震影響耕腾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜杀糯,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一扫俺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧固翰,春花似錦狼纬、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至歉铝,卻和暖如春盈简,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背太示。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工柠贤, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人类缤。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓臼勉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親餐弱。 傳聞我的和親對象是個殘疾皇子宴霸,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內(nèi)容