理清 Block 底層結(jié)構(gòu)及其捕獲行為

來自掘金 《理清 Block 底層結(jié)構(gòu)及其捕獲行為》

image

Block 的本質(zhì)

本質(zhì)

  1. Block 的本質(zhì)是一個 Objective-C 對象,它內(nèi)部也擁有一個 isa 指針夜畴。
  2. Block 是封裝了函數(shù)及其調(diào)用環(huán)境的 Objective-C 對象

底層數(shù)據(jù)結(jié)構(gòu)

一個簡單示例:

int main(int argc, const char * argv[]) {

    void (^block)(void) = ^{
        NSLog(@"hey");
    };
    block();
    return 0;
}

將以上 Objective-C 源碼轉(zhuǎn)換成 c++ 相關(guān)源碼拖刃,使用命令行 :
xcrun -sdk iphoneos xclang -arch arm64 -rewrite-objc 文件名

c++ 的結(jié)構(gòu)體與一般的類相似。

int main(int argc, const char * argv[]) {

    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    return 0;
}

其中 Block 的數(shù)據(jù)結(jié)構(gòu)為:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
};

impl 變量數(shù)據(jù)結(jié)構(gòu):

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr; 
};

FuncPtr:函數(shù)實際調(diào)用的地址贪绘,因為 Block 可看作是捕獲自動變量的匿名函數(shù)兑牡。

Desc 變量數(shù)據(jù)結(jié)構(gòu):

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
}

Block 的類型

Objective-C 中 Block 有三種類型,其最終類型都是 NSBlock 税灌。

  • NSGlobalBlock (_NSConcreteGlobalBlock)
  • NSStackBlock (_NSConcreteStackBlock)
  • NSMallocBlock (_NSConcreteMallocBlock)

Block 類型的不同均函,主要根據(jù)捕獲變量的不同行為產(chǎn)生:

Block 類型 行為
NSGlobalBlock 沒有訪問 auto 變量
NSStackBlock 訪問 auto 變量
NSMallocBlock NSStackBlock 調(diào)用 copy

在內(nèi)存中的存儲位置

image

內(nèi)存五大區(qū):棧、堆菱涤、靜態(tài)區(qū)(BSS 段)苞也、常量區(qū)(數(shù)據(jù)段)、代碼段

copy 行為

不同類型的 Block 調(diào)用 copy 操作粘秆,也會產(chǎn)生不同的復(fù)制效果:

Block 類型 副本源的配置存儲域 復(fù)制效果
__NSConcreteStackBlock 從棧復(fù)制到堆
__NSConcreteGlobalBlock 數(shù)據(jù)段(常量區(qū)) 什么也不做
__NSConcreteMallocBlock 引用計數(shù)增加
  • 在 ARC 環(huán)境下如迟,編譯器會在以下情況自動將棧上的 Block 復(fù)制到堆上:
  1. Block 作為函數(shù)返回值
  2. 將 Block 賦值給 __strong 指針
  3. 蘋果 Cocoa、GCD 等 api 中方法參數(shù)是 block 類型

在 ARC 環(huán)境下攻走,聲明的 block 屬性用 copy 或 strong 修飾的效果是一樣的殷勘,但在 MRC 環(huán)境下,則用 copy 修飾昔搂。

捕獲變量

為了保證在 Block 內(nèi)部能夠正常訪問外部變量玲销,Block 有一套變量捕獲機制:

變量類型 是否捕獲到 Block 內(nèi)部 訪問方式
局部 auto 變量 值傳遞
局部 static 變量 指針傳遞
全局變量 直接訪問

若局部 static 變量是基礎(chǔ)類型 int val ,則訪問方式為 int *val
若局部 static 變量是對象類型 JAObject *obj 摘符,則訪問方式為 JAObject **obj

基礎(chǔ)類型變量

一個簡單示例:

int age = 10;
// static int age = 10;
void (^block)(void) = ^{
     NSLog(@"age is %d", age);
};
block();
  • 捕獲局部 auto 基礎(chǔ)類型變量生成的 Block 結(jié)構(gòu)體 struct __main_block_impl_0 變?yōu)?
struct __main_block_impl_0 {
  ···
  int age; // 傳遞值
}
  • 捕獲局部 static 基礎(chǔ)類型變量生成的 Block 結(jié)構(gòu)體 struct __main_block_impl_0 變?yōu)椋?/li>
struct __main_block_impl_0 {
  ···
  int *age; // 傳遞指針
}
  • 捕獲全局基礎(chǔ)類型變量生成的結(jié)構(gòu)體 struct __main_block_impl_0 沒有包含 age 贤斜,因為作用域為全局,可直接訪問逛裤。

對象類型變量

一個簡單示例:

JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
void (^block)(void) = ^{
     NSLog(@"age is %d", person.age);
};
block();
  • 捕獲局部 auto 對象類型變量生成的 Block 結(jié)構(gòu)體 struct __main_block_impl_0 變?yōu)?
struct __main_block_impl_0 {
  ···
  JAPerson *person;
}
  • 捕獲局部 static 對象類型變量生成的 Block 結(jié)構(gòu)體 struct __main_block_impl_0 變?yōu)椋?/li>
struct __main_block_impl_0 {
  ···
  JAPerson **person;
}
  • 捕獲全局對象類型變量生成的結(jié)構(gòu)體 struct __main_block_impl_0 沒有包含 person 瘩绒,因為作用域為全局,可直接訪問别凹。

copy 和 dispose 函數(shù)

當(dāng)捕獲的變量是對象類型或者使用 __Block 將變量包裝成一個 _Block_byref變量名_0 類型的 Objective-C 對象時草讶,會產(chǎn)生 copydispose 函數(shù)。

一個簡單示例:

JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
void (^block)(void) = ^{
     NSLog(@"age is %d", person.age);
};
block();

其中生成的 Block 的數(shù)據(jù)結(jié)構(gòu)中多了 JAPerson 類型指針變量 person :

struct __main_block_impl_0 {
  ···
  JAPerson *person;
}

Desc 變量數(shù)據(jù)結(jié)構(gòu)多了內(nèi)存管理相關(guān)的函數(shù):

static struct __main_block_desc_0 {
  ···
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
}

這兩個函數(shù)的調(diào)用時機:

函數(shù) 調(diào)用時機
copy 棧上的 Block 復(fù)制到堆時
dispose 堆上的 Block 被廢棄時

copy 和 dispose 底層相關(guān)源碼

// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

// Lose the reference, and if heap based and last reference, recover the memory
BLOCK_EXPORT void _Block_release(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);


// Used by the compiler. Do not call this function yourself.
BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

// Used by the compiler. Do not call this function yourself.
BLOCK_EXPORT void _Block_object_dispose(const void *, const int)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

當(dāng) Block 內(nèi)部訪問了對象類型的 auto 變量時:

  • 如果 Block 是在棧上炉菲,將不會對 auto 變量產(chǎn)生強引用堕战。
  • 如果 Block 被拷貝到堆上坤溃,會調(diào)用 Block 內(nèi)部的 copy 函數(shù),copy 函數(shù)內(nèi)部會調(diào)用 _Block_object_assign 函數(shù)嘱丢,_Block_object_assign 函數(shù)會根據(jù) auto 變量的修飾符(__strong薪介、__weak、__unsafe_unretain)作出相應(yīng)的內(nèi)存管理操作越驻。

注意:若此時變量類型為對象類型汁政,這里僅限于 ARC 時會 retain ,MRC 時不會 retain 缀旁。

  • 如果 Block 從堆上移除记劈,會調(diào)用 Block 內(nèi)部的 dispose 函數(shù),dispose 函數(shù)內(nèi)部會調(diào)用 _Block_object_dispose 函數(shù)并巍,_Block_object_dispose 函數(shù)會自動 release 引用的 auto 變量目木。

使用 __weak 修飾的 OC 代碼轉(zhuǎn)換對應(yīng)的 c++ 代碼會報錯:
error: cannot create __weak reference because the current deployment target does not support weak references
此時終端命令需支持 ARC 并指定 Runtime 版本:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

內(nèi)存管理

修改局部 auto 變量

局部 static 變量(指針訪問)、全局變量(直接訪問)都可以在 Block 內(nèi)部直接修改捕獲的變量懊渡,而局部 auto 變量則主要通過使用 __block 存儲域修飾符來修改捕獲的變量刽射。

  • __block 修飾符可以用于解決 Block 內(nèi)部無法修改局部 auto 變量值的問題
  • __block 修飾符不能用于修飾全局變量、靜態(tài)變量(static)

編譯器會將 __block 修飾的變量包裝成一個 Objective-C 對象剃执。

一個簡單示例:

__block int age = 10;
void (^block)(void) = ^{
   NSLog(@"age is %d", age);
};
block();

其中 Block 的數(shù)據(jù)結(jié)構(gòu)多了一個 __Block_byref_age_0 類型的指針:

struct __main_block_impl_0 {
  ···
  __Block_byref_age_0 *age; // by ref
}

__Block_byref_age_0 結(jié)構(gòu)體:

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age; // age 真正存儲的地方
};

兩個注意點:

    1. 此處指針 val 是指向 age 的指針誓禁,而第二個 val 指的是 age 的值。
image
    1. 源碼里面通過 age->__forwarding->age 的方式去取值肾档,是因為這兩個 age 都可能仍在棧上摹恰,此時直接 age->age 訪問會有問題,而 copy 操作時 __forwarding 會指向堆上的 __Block_byref_age_0 阁最,此時就算第一個 age 仍在棧上戒祠,通過 age->__forwarding 會重新指向堆上的 __Block_byref_age_0 骇两,此時再訪問 age 便不會有問題 age->__forwarding->age 速种。
image
image

__block 的內(nèi)存管理

使用 __block 修飾符時的內(nèi)存管理情況:

  • 當(dāng) Block 存儲在棧上時,并不會對 __block 變量強引用低千。
  • 當(dāng) Block 被 copy 到堆上時配阵,會調(diào)用 Block 內(nèi)部的 copy 函數(shù),copy 函數(shù)會調(diào)用 __main_block_copy_0 函數(shù)對 __block 變量產(chǎn)生一個強引用示血。如下圖
image
image
  • 當(dāng) Block 從堆上被移除時棋傍,會調(diào)用 Block 內(nèi)部的 dispose 函數(shù),dispose 函數(shù)會調(diào)用 _Block_object_dispose 函數(shù)自動 release __block 變量难审。如下圖
image
image

__weak 和 __block 修飾時的引用情況

    1. 僅用 __weak 修飾

一個簡單的示例:

JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
__weak typeof(person) weakPerson = person;
void (^block)(void) = ^{
    NSLog(@"person‘s age is %d", weakPerson.age);
};
image
    1. 使用 __block __weak 修飾

一個簡單的示例:

JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
__block __weak typeof(person) weakPerson = person;
void (^block)(void) = ^{
     NSLog(@"person‘s age is %d", weakPerson.age);
};
block();
return 0;
image

循環(huán)引用

常見的循環(huán)引用問題:

image

ARC 環(huán)境下解決循環(huán)引用

    1. 弱引用持有:使用 __weak 或 __unsafe__unretain 解決
image
    1. 手動將一方置為 nil :使用 __block 解決瘫拣,在 block 內(nèi)部將一方置為 nil ,因此必須執(zhí)行該 block
image

MRC 環(huán)境下解決循環(huán)引用

    1. 弱引用持有:使用 __unsafe__unretain 解決
    1. 直接使用 __block 解決告喊,無需手動將一方置為 nil 麸拄,因為底層 _Block_object_assign 函數(shù)在 MRC 環(huán)境下對 block 內(nèi)部的對象不會進(jìn)行 retain 操作派昧。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市拢切,隨后出現(xiàn)的幾起案子蒂萎,更是在濱河造成了極大的恐慌,老刑警劉巖淮椰,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件五慈,死亡現(xiàn)場離奇詭異,居然都是意外死亡主穗,警方通過查閱死者的電腦和手機泻拦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來忽媒,“玉大人聪轿,你說我怎么就攤上這事』郑” “怎么了陆错?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長金赦。 經(jīng)常有香客問我音瓷,道長,這世上最難降的妖魔是什么夹抗? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任绳慎,我火速辦了婚禮,結(jié)果婚禮上漠烧,老公的妹妹穿的比我還像新娘杏愤。我一直安慰自己,他們只是感情好已脓,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布珊楼。 她就那樣靜靜地躺著,像睡著了一般度液。 火紅的嫁衣襯著肌膚如雪厕宗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天堕担,我揣著相機與錄音已慢,去河邊找鬼。 笑死霹购,一個胖子當(dāng)著我的面吹牛佑惠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼膜楷,長吁一口氣:“原來是場噩夢啊……” “哼乍丈!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起把将,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤轻专,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后察蹲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體请垛,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年洽议,在試婚紗的時候發(fā)現(xiàn)自己被綠了宗收。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡亚兄,死狀恐怖混稽,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情审胚,我是刑警寧澤匈勋,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站膳叨,受9級特大地震影響洽洁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜菲嘴,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一饿自、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧龄坪,春花似錦昭雌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至抄课,卻和暖如春唱星,著一層夾襖步出監(jiān)牢的瞬間雳旅,已是汗流浹背跟磨。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留攒盈,地道東北人抵拘。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像型豁,于是被迫代替她去往敵國和親僵蛛。 傳聞我的和親對象是個殘疾皇子尚蝌,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

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