關(guān)于Block一些記錄

大概兩三周前通過(guò)學(xué)習(xí)《Objective-C高級(jí)編程 iOS與OS X多線程和內(nèi)存管理》中的Block章節(jié)慈鸠,系統(tǒng)深入了解了Block相關(guān)原理和內(nèi)存管理的內(nèi)容起暮,昨天閑暇時(shí)回想起來(lái)麦向,感覺(jué)有些東西又模糊了,內(nèi)容記得七七八八,太碎片化了。索性好記性不如爛筆頭惰蜜,把自己的理解整理記錄一下。

將Objective-C代碼轉(zhuǎn)換為C\C++代碼

ClangLLVM編譯器)具有轉(zhuǎn)換為我們可讀源代碼的功能受神。

//如果需要鏈接其他框架抛猖,使用-framework參數(shù)。比如-framework UIKit
xcrun  -sdk  iphoneos  clang  -arch  arm64  -rewrite-objc  OC源文件  -o  輸出的cpp文件

設(shè)置了sdk的平臺(tái)和cpu架構(gòu),減少轉(zhuǎn)換出來(lái)的代碼量财著,方便查閱养交。

可能會(huì)遇到以下問(wèn)題:

cannot create __weak reference in file using manual reference

解決方案:支持ARC、指定運(yùn)行時(shí)系統(tǒng)版本瓢宦,比如

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 OC源文件 -o  輸出的cpp文件

Block底層結(jié)構(gòu)

當(dāng)Block沒(méi)有自動(dòng)捕獲變量時(shí):

//Block定義的結(jié)構(gòu)體
struct __main_block_impl_0 {
struct _block_impl impl;
struct __main_block_desc_0 *Desc;
__main_block_impl_0(void *fp,struct __main_block_desc_0 *desc, int flags=0)
{
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
}
};

我們代碼中寫的Block在底層會(huì)被轉(zhuǎn)換成類似上面這樣子的結(jié)構(gòu)體類型,struct __main_block_impl_0灰羽。

struct __main_block_impl_0中包含了兩個(gè)結(jié)構(gòu)體:struct _block_impl implstruct __main_block_desc_0 *Desc驮履,以及一個(gè)構(gòu)造函數(shù)。再看一下兩個(gè)結(jié)構(gòu)體的定義廉嚼。

struct _block_impl {
void *isa;  //指向了block所屬的類型
int Flags;  
int Reserved;   // 預(yù)留
void *FuncPtr;  // 函數(shù)指針玫镐,指向block中方法實(shí)現(xiàn)
};
//存儲(chǔ)block的其他信息,大小等
struct __main_block_desc_0 {
unsigned long reserved;   // 預(yù)留
unsigned long Block_size; // Block的大小
}

通過(guò)上面可以看出Blcok也是包含一個(gè)isa指針怠噪,因此也是一種OC對(duì)象恐似。具體是什么類,因?yàn)樯婕暗?code>Blcok的內(nèi)存管理傍念,所以后面篇幅再深入討論矫夷。

再看一下給Blcok結(jié)構(gòu)體賦值和調(diào)用的代碼:

//賦值部分
struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0,&__mainBlock_desc_0_DATA);
struct __main_block_impl_0 *blk = &temp;
//調(diào)用部分
(*blk->impl.FuncPtr)(blk);

賦值部分就是調(diào)用了__main_block_impl_0的構(gòu)造函數(shù),將方法和__main_block_desc_0類型的結(jié)構(gòu)體作為參數(shù)傳遞進(jìn)入憋槐。
方法調(diào)用是通過(guò)Blcok的結(jié)構(gòu)體取出其中的函數(shù)指針双藕,直接調(diào)用該函數(shù),同時(shí)將Block自身作為參數(shù)傳遞給方法實(shí)現(xiàn)阳仔。

先對(duì)簡(jiǎn)單的Block有個(gè)印象忧陪。

Block變量捕獲機(jī)制

int c = 30; // 全局變量(數(shù)據(jù)段,不需要捕獲)

- (void)blockTest {
    auto int a = 10;//局部auto變量(棧區(qū),值捕獲)
    auto __strong NSObject *object = [[NSObject alloc] init];//局部auto變量(棧區(qū)近范,值捕獲)
    static int b = 20;//局部static變量(數(shù)據(jù)段嘶摊,指針捕獲)
    void (^block)(void) = ^(void) {
        NSLog(@"a:%d b:%d c:%d",a,b,c);
        NSLog(@"object:%@",object);
    };
    block();
}

為了在Block內(nèi)部可以訪問(wèn)外部的變量,Block有個(gè)變量捕獲機(jī)制评矩。那么什么樣的變量才會(huì)捕獲叶堆,什么樣的不會(huì)捕獲呢?

  • 局部變量

    1. auto變量(平時(shí)我們?cè)诜椒ㄖ新暶鞯姆?code>static局部變量稚照,只是省略了auto關(guān)鍵字)蹂空,這種情況是值捕獲。
    2. static變量和結(jié)構(gòu)體果录,這種情況是指針捕獲上枕。
  • 全局變量:不會(huì)捕獲,因?yàn)椴恍枰东@就可以訪問(wèn)弱恒。

總結(jié)就是:只捕獲局部變量辨萍。

Block捕獲變量之后代碼什么樣子?

將上面的- (void)blockTest轉(zhuǎn)換C看一下:

struct __blockTest_block_impl_0 {
  struct __block_impl impl;
  struct __blockTest_block_desc_0* Desc;
  int a;
  int *b;
  NSObject *object;
  // 省略構(gòu)造函數(shù)...
};

嗯,備注的沒(méi)有錯(cuò)。變量aobject都是值捕獲锈玉,而變量b捕獲的是*b爪飘,是指針的捕獲,而c沒(méi)有捕獲拉背。

Block的內(nèi)存管理

Block有3種類型师崎,可以通過(guò)調(diào)用class方法或者isa指針查看具體類型,最終都是繼承自NSBlock類型椅棺。

  • NSGlobalBlock ( _NSConcreteGlobalBlock )//全局
  • NSStackBlock ( _NSConcreteStackBlock ) //棧
  • NSMallocBlock ( _NSConcreteMallocBlock )//堆

除了打印犁罩,那么怎么判斷一個(gè)Block的具體類型...?

  1. NSGlobalBlock : 沒(méi)有訪問(wèn)auto變量两疚。
  2. NSStackBlock : 訪問(wèn)了auto變量床估。
  3. NSMallocBlock : __NSStackBlock__調(diào)用了copy

可能有的同學(xué)在這里這樣子測(cè)試一下诱渤,發(fā)現(xiàn)上面的判斷依據(jù)并不對(duì)...

001.png

明明Block訪問(wèn)的是auto變量丐巫,但是Block的類型是__NSMallocBlock__吶,并不是__NSStackBlock__,你說(shuō)的不對(duì)勺美。不著急递胧,其實(shí)這里還涉及到另外一個(gè)問(wèn)題:Block的內(nèi)存管理。

對(duì)一個(gè)Blcok進(jìn)行copy操作后赡茸,對(duì)三種類型的Blcok產(chǎn)生的影響:

  1. __NSGlobalBlock__ :
    • copy前:Block位于數(shù)據(jù)段中谓着;
    • copy后:不產(chǎn)生任何影響。
  2. __NSStackBlock__ :
    • copy前:Block位于函數(shù)棧中坛掠;
    • copy后:從棧中復(fù)制一份到堆中赊锚。
  3. __NSMallocBlock__ :
    • copy前:Block位于堆中;
    • copy后:Block的引用計(jì)數(shù)增加屉栓。

在ARC環(huán)境下舷蒲,編譯器會(huì)根據(jù)情況自動(dòng)將棧上的Block復(fù)制到堆上,比如以下情況:

  1. Block作為函數(shù)返回值時(shí)友多。
  2. Block賦值給__strong指針時(shí)牲平。
  3. Block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時(shí)。
  4. Block作為GCD API的方法參數(shù)時(shí)域滥。

在之前的圖片(001)中纵柿,就是其中的第二種情況,Block被賦值給__strong指針启绰。

這也是為什么我們習(xí)慣于用copy關(guān)鍵字昂儒,來(lái)修飾一個(gè)Block。以及將Block當(dāng)做參數(shù)傳遞時(shí)委可,安全起見渊跋,會(huì)對(duì)Block參數(shù)執(zhí)行copy操作。

Block對(duì)對(duì)象類型變量的強(qiáng)弱引用問(wèn)題

  1. 當(dāng)Block內(nèi)部訪問(wèn)了對(duì)象類型的auto變量時(shí):

    • 如果Block是在棧上,將不會(huì)對(duì)auto變量產(chǎn)生強(qiáng)引用拾酝。就是說(shuō)棧上的Block不會(huì)強(qiáng)引用一個(gè)對(duì)象
  2. 當(dāng)Block被拷貝到堆上時(shí):

    • 會(huì)調(diào)用Block內(nèi)部的copy函數(shù)
    • copy函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_assign函數(shù)
    • _Block_object_assign函數(shù)會(huì)根據(jù)auto變量的修飾符(__strong燕少、__weak__unsafe_unretained)做出相應(yīng)的操作蒿囤,形成強(qiáng)引用(retain)或者弱引用
  3. 當(dāng)Block從堆上移除時(shí):

    • 會(huì)調(diào)用Block內(nèi)部的dispose函數(shù)
    • dispose函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_dispose函數(shù)
    • _Block_object_dispose函數(shù)會(huì)自動(dòng)釋放引用的auto變量release

__block修飾符

__block的作用:

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

編譯器會(huì)將__block變量包裝成一個(gè)對(duì)象:

void blockTest() {
    __block int a = 10;
    __block NSObject *object = [[NSObject alloc] init];
    NSLog(@"a:%d",a);
    void (^block)(void) = ^(void) {
        a = 20;
        NSLog(@"object --- %@",object);
    };
    block();
}

將上面的代碼轉(zhuǎn)換成C++之后可以看到:

// __block int a 被轉(zhuǎn)換為下面的結(jié)構(gòu)體

struct __Block_byref_a_0 {
 void *__isa;
 __Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};
//  __block NSObject *object 被轉(zhuǎn)換為下面的結(jié)構(gòu)體

struct __Block_byref_object_1 {
  void *__isa;
__Block_byref_object_1 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *object;
};

__Block_byref_a_0 *__forwarding是一個(gè)指向自身的指針镶摘。

Snip20180612_3.png

當(dāng)我們的OC代碼中,去訪問(wèn)被__block修飾的變量岳守,在底層中是如何去讀取變量呢?
上面的代碼中有一段打印的代碼:

NSLog(@"a:%d",a);

在C++中被轉(zhuǎn)成了:

NSLog((NSString *)//此處省略碌冶,影響閱讀//,(a.__forwarding->a));

在Blcok中修改a的值:

a = 20;

在C++中被轉(zhuǎn)成了:

// 從blcok取出blcok捕獲的被`__block`修飾的變量
__Block_byref_a_0 *a = __cself->a; //__cself是blcok
(a->__forwarding->a) = 20;

Block外部外部訪問(wèn)變量是通過(guò)a.__forwarding->a湿痢,訪問(wèn)結(jié)構(gòu)體的__forwarding指針找到值∑伺樱可能有的同學(xué)有疑問(wèn):不是多此一舉的嘛譬重?結(jié)構(gòu)體->__forwarding->結(jié)構(gòu)體->val,直接結(jié)構(gòu)體->val不就可以了嗎?

目前能看出的作用是保持統(tǒng)一的寫法罐氨,當(dāng)然還有其他的原因臀规,后面講解。

總結(jié):

  • 當(dāng)沒(méi)有使用__block時(shí)栅隐,由于是值捕獲塔嬉,所以哪怕在Block內(nèi)修改,也不能影響到Block外變量的值租悄,因此蘋果不允許直接修改谨究。
  • 而當(dāng)我們?cè)?code>Block去修改被__block修飾的變量時(shí),由于是捕獲到__block結(jié)構(gòu)體的指針泣棋,這樣就可以我們可以修改Block外面的值了胶哲。

__block的內(nèi)存管理

  • 開始時(shí)__block結(jié)構(gòu)體是一個(gè)在上的結(jié)構(gòu)體,在棧上的內(nèi)存無(wú)所謂強(qiáng)弱引用的關(guān)系潭辈。而__block結(jié)構(gòu)體包裝的對(duì)象是強(qiáng)或弱引用鸯屿,是通過(guò)你使用__weak__strong哪個(gè)來(lái)修飾決定的。
 NSObject *object = [[NSObject alloc] init];
__block __weak typeof(object) weakObject = object;
__block  NSObject *strongObjce = object;
  • 當(dāng)Block被拷貝到上時(shí)把敢,會(huì)自動(dòng)將捕獲的__block結(jié)構(gòu)體也拷貝到堆上寄摆。由于__block的結(jié)構(gòu)體也有isa指針,同時(shí)還在堆空間中修赞,我們可以將它理解成一個(gè)OC的對(duì)象冰肴,__block對(duì)象。
Snip20180612_4.png
  • 于此同時(shí)還會(huì)將棧上的__block結(jié)構(gòu)體中的__forwarding指針,指向堆空間中的__block對(duì)象熙尉。Block捕獲的指針联逻,會(huì)從棧上的__block結(jié)構(gòu)體變?yōu)槎芽臻g中的__block對(duì)象,同時(shí)對(duì)__block對(duì)象強(qiáng)引用检痰。
Snip20180612_6.png
  • 當(dāng)Block從堆中移除時(shí):
    Snip20180612_5.png

這樣分析一下包归,除了會(huì)將__block結(jié)構(gòu)體從棧移動(dòng)到堆之外,和普通形式的auto對(duì)象內(nèi)存管理铅歼,流程上沒(méi)有什么差別公壤。當(dāng)然具體內(nèi)部調(diào)用的函數(shù)參數(shù)還是有點(diǎn)區(qū)別的:

當(dāng)Block拷貝到堆上時(shí),都會(huì)通過(guò)copy函數(shù)來(lái)處理它們

__block變量(假設(shè)變量名叫做a)
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
對(duì)象類型的auto變量(假設(shè)變量名叫做p)
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

當(dāng)Block從堆上移除時(shí)椎椰,都會(huì)通過(guò)dispose函數(shù)來(lái)釋放它們:

__block變量(假設(shè)變量名叫做a)
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
對(duì)象類型的auto變量(假設(shè)變量名叫做p)
_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

能看到,雖然調(diào)用的方法相同厦幅,但是傳遞的參數(shù)類型不同:3和8。這決定了方法內(nèi)部如何去處理流程吧慨飘。

循環(huán)引用

想必讀到這里确憨,應(yīng)該可以理解循環(huán)引用是怎么產(chǎn)生的了。

Snip20180612_7.png

注意:self是方法調(diào)用時(shí)傳遞的第一個(gè)參數(shù)瓤的,是局部變量休弃。

怎么解決?
不讓Block強(qiáng)引用self,斷掉一條線圈膏,就不會(huì)產(chǎn)生循環(huán)引用塔猾。

我們一般通過(guò)__weak來(lái)修飾變量,比如這樣:

__weak typeof(self) weakSelf = self;

也可以使用__unsafe_unretained修飾變量解決稽坤。關(guān)于__unsafe_unretained可以看這篇文章丈甸。

他們的區(qū)別:
__weak: 對(duì)于__weak,指針的對(duì)象在它指向的對(duì)象釋放的時(shí)候回轉(zhuǎn)換為nil尿褪,這是一種特別安全的行為老虫。
__unsafe_unretained: 就像他的名字表達(dá)那樣,__unsafe_unretained會(huì)繼續(xù)指向?qū)ο蟠嬖诘哪莻€(gè)內(nèi)存茫多,即使是在它已經(jīng)銷毀之后祈匙。這會(huì)導(dǎo)致因?yàn)樵L問(wèn)那個(gè)已釋放對(duì)象引起的崩潰。

為了更安全的使用天揖,我們經(jīng)常是這樣寫:

__weak typeof(self) weakSelf = self; //解決循環(huán)應(yīng)用
self.block = ^{
    __strong typeof(self) strongSelf = weakSelf; 
    //在Block方法內(nèi)部夺欲,即:局部變量?jī)?nèi);對(duì)weakSelf進(jìn)行一個(gè)強(qiáng)引用今膊,
    //這樣可以確保些阅,當(dāng)self其他的強(qiáng)引用都釋放時(shí),仍然保持有一個(gè)強(qiáng)引用斑唬,
    //這樣self不會(huì)再block內(nèi)部突然釋放掉市埋,導(dǎo)致后面的代碼出現(xiàn)未知的問(wèn)題黎泣。
    //do someThing...//
};
以上就是我個(gè)人對(duì)Block的一些理解仰禀,如有錯(cuò)誤的地方勘究,希望各位大俠不吝賜教!判莉!
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末坷澡,一起剝皮案震驚了整個(gè)濱河市托呕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌频敛,老刑警劉巖项郊,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異斟赚,居然都是意外死亡着降,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門拗军,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)任洞,“玉大人,你說(shuō)我怎么就攤上這事食绿。” “怎么了公罕?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵器紧,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我楼眷,道長(zhǎng)铲汪,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任罐柳,我火速辦了婚禮掌腰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘张吉。我一直安慰自己齿梁,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布肮蛹。 她就那樣靜靜地躺著勺择,像睡著了一般。 火紅的嫁衣襯著肌膚如雪伦忠。 梳的紋絲不亂的頭發(fā)上省核,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音昆码,去河邊找鬼气忠。 笑死邻储,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的旧噪。 我是一名探鬼主播吨娜,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼舌菜!你這毒婦竟也來(lái)了萌壳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤日月,失蹤者是張志新(化名)和其女友劉穎袱瓮,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體爱咬,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡尺借,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了精拟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片燎斩。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蜂绎,靈堂內(nèi)的尸體忽然破棺而出栅表,到底是詐尸還是另有隱情,我是刑警寧澤师枣,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布怪瓶,位于F島的核電站,受9級(jí)特大地震影響践美,放射性物質(zhì)發(fā)生泄漏洗贰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一陨倡、第九天 我趴在偏房一處隱蔽的房頂上張望敛滋。 院中可真熱鬧,春花似錦兴革、人聲如沸绎晃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)箕昭。三九已至,卻和暖如春解阅,著一層夾襖步出監(jiān)牢的瞬間落竹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工货抄, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留述召,地道東北人朱转。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像积暖,于是被迫代替她去往敵國(guó)和親藤为。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

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