Block深度軟文

the block

前言

深究block可以說會(huì)涉及不少東西,筆者欲通過循序漸進(jìn)的方式來談及block相關(guān),略陳固陋。閱讀本文前,希望我們還是先一起來過一下幾個(gè)概念:

  • 指針和對(duì)象,都是內(nèi)存塊。一個(gè)大,一個(gè)小。一個(gè)在棧中,一個(gè)在堆中。
  • iOS中,我們可以生命一個(gè)指針,也可以通過alloc獲取一塊內(nèi)存。
  • 我們可以直接消滅一個(gè)指針,將其置為nil,但是我們沒辦法直接消滅一塊對(duì)象內(nèi)存。對(duì)于對(duì)象內(nèi)存,我們永遠(yuǎn)只能依靠系統(tǒng)去回收。即當(dāng)這個(gè)對(duì)象不被任何指針?biāo)鶕碛袝r(shí),系統(tǒng)就會(huì)收回該對(duì)象內(nèi)存。
  • 函數(shù)在棧區(qū),函數(shù)調(diào)用完畢后其stack frame將被彈出結(jié)束其生命周期。
  • Objective-C的對(duì)象在內(nèi)存中是以堆的方式分配空間。

正文

1、ARC strong和weak指針

在講block之前呢先講一下strong和weak指針的問題,以便于更好的理解下面block中的循環(huán)引用以及變量截獲等問題。我們知道ARC消除了手動(dòng)管理內(nèi)存的煩瑣,編譯器會(huì)自動(dòng)在適當(dāng)?shù)牡胤讲迦脒m當(dāng)?shù)膔etain、release、autorelease語(yǔ)句。規(guī)則很簡(jiǎn)單,只要還有一個(gè)變量指向?qū)ο螅瑢?duì)象就會(huì)保持在內(nèi)存中。當(dāng)指針指向新值,或者指針不再存在時(shí),相關(guān)聯(lián)的對(duì)象就會(huì)自動(dòng)釋放。如下圖動(dòng)畫模擬引用計(jì)數(shù)回收器,紅色閃爍表示引用計(jì)數(shù)行為,引用計(jì)數(shù)的優(yōu)勢(shì)在于垃圾會(huì)被很快檢測(cè)到,你可以看到紅色閃爍過后緊接著該區(qū)域變黑(圖片來自)。

REF_COUNT
  • strong指針

比如在控制器上有個(gè)nameField屬性,我在文本框中輸入henvy,那么就可以說,nameField的text屬性是NSString對(duì)象的指針,也就是擁有者,該對(duì)象保存了文本輸入框的內(nèi)容。

如果執(zhí)行了NSString *name = self.nameField.text;后,@“henvy”對(duì)象就有了多個(gè)擁有者,也就是有兩個(gè)指針指向同一個(gè)對(duì)象。

接下來我又在文本框中輸入了新的內(nèi)容比如@"Leslie",此時(shí)nameFeild的text屬性就指向了新的NSString對(duì)象。但原來的NSString對(duì)象仍然還有一個(gè)所有者(name變量),因此會(huì)繼續(xù)保留在內(nèi)存中。

當(dāng)name變量獲得新值,或者不再存在時(shí)(如局部變量方法返回時(shí)、實(shí)例變量對(duì)象釋放時(shí)),原先的NSString對(duì)象就不再擁有任何所有者,retain計(jì)數(shù)降為0,這時(shí)對(duì)象會(huì)被釋放
如,給name變量賦予一個(gè)新值name = @"Eason"時(shí)。

我們稱name和nameField.text指針為"Strong指針",因?yàn)樗鼈兡軌虮3謱?duì)象的生命。默認(rèn)所有成員變量和局部變量都是Strong指針。

  • weak指針

weak型的指針變量依然可以指向一個(gè)對(duì)象,但不屬于對(duì)象的擁有者,就像我是很喜歡你,但是卻得不到你一樣。依然是我們上面的例子在輸入框輸入henvy后執(zhí)行__weak NSString *name = self.nameField.text;后,雖然同時(shí)指向但name并不真正擁有henvy。

此時(shí)如果文本框內(nèi)容重新輸入@“Leslie”,則原先的henvy對(duì)象就沒有擁有者,就會(huì)被釋放,此時(shí)name變量會(huì)自動(dòng)變成nil,稱為空指針。weak型的指針變量自動(dòng)變?yōu)閚il避免了野指針的產(chǎn)生。

舉一個(gè)典型的weak指針的例子,即我們的代理模式,控制器ViewController強(qiáng)引用一個(gè)myTableView,myTableView的dataSource和delegate都是weak指針,指向你的ViewController。這也是cocoa設(shè)定的一個(gè)規(guī)則,即父對(duì)象建立子對(duì)象的強(qiáng)引用,而子對(duì)象只對(duì)父對(duì)象建立弱引用。

2、Block的類型

好吧原諒我前面ARC講了那么多,當(dāng)然還是希望讀者能夠體會(huì)筆者的良苦用心。

  • NSGlobalBlock

該類型的block存儲(chǔ)在程序的數(shù)據(jù)區(qū)域(text段),不引用外部變量,只對(duì)自己的參數(shù)做操作,自給自足的狀態(tài),可以當(dāng)做函數(shù)使用,例如:

typedef int (^GlobalBlock)(int);
GlobalBlock block = ^(int count){
    return count;
};  //nslog:<__NSGlobalBlock__: 0x10d090200>
  • NSStackBlock

該類型的block在非ARC模式存儲(chǔ)在棧區(qū),內(nèi)部引用外部變量,當(dāng)棧block結(jié)束運(yùn)行的時(shí)候會(huì)被請(qǐng)出棧,生命周期結(jié)束,再次調(diào)用當(dāng)然crash掉,避免這一點(diǎn)可以通過手動(dòng)copy將其拷貝到安全的堆上來,脫離棧的危險(xiǎn)地帶,因?yàn)楸旧項(xiàng)^(qū)就是過河拆橋、兔死狗烹的狀態(tài)。不像堆區(qū)講究循環(huán)利用,生死由天定(無(wú)指針擁有被系統(tǒng)回收)。當(dāng)然在ARC模式完全不用擔(dān)心,ARC模式改寫了天規(guī)杜絕NSStackBlock狀況的發(fā)生,他會(huì)自動(dòng)將block拷貝到堆上去(block作為方法或函數(shù)的參數(shù)傳遞時(shí),編譯器不會(huì)自動(dòng)調(diào)用copy方法),從而演變成了第三種NSMallocBlock,此時(shí)的堆上的block就會(huì)像一個(gè)ObjC對(duì)象一樣被放入autoreleasepool里面,從而保證了返回后的block仍然可以正確執(zhí)行。因此在本該是NSStackBlock的情況下打印結(jié)果就會(huì)變成NSMallocBlock。

typedef void (^StackBlock)();
NSString *str = @"henvy";
StackBlock block = ^{
    NSLog(@"%@",str);
};  //nslog :<__NSMallocBlock__: 0x7fe412d18790>
  • NSMallocBlock

該類型的block存儲(chǔ)在堆區(qū),引用外部變量,由NSStackBlock Block_copy()生成。在ARC模式下可以理解為只存在NSGlobalBlock和NSMallocBlock兩種類型。

3、Block對(duì)外部變量的存儲(chǔ)管理

我們都知道內(nèi)存有堆和棧兩個(gè)部分,堆在高地址向下走,棧在低地址向上走。在每個(gè)函數(shù)調(diào)用的時(shí)候,系統(tǒng)都會(huì)為其生成一個(gè)棧的stack frame,該函數(shù)結(jié)束后這個(gè)frame被彈出去;然而堆對(duì)象的生存不從屬于某個(gè)函數(shù),即便是創(chuàng)建這個(gè)堆對(duì)象的函數(shù)結(jié)束了,堆對(duì)象也可以繼續(xù)存在,因此內(nèi)存泄漏都是堆對(duì)象惹的禍,ObjC里的引用計(jì)數(shù)就是用來管理堆對(duì)象這個(gè)東西,由于arc中沒有引用計(jì)數(shù)的概念,只有強(qiáng)引用和弱引用的概念。當(dāng)一個(gè)變量沒有指針指向它時(shí),就會(huì)被系統(tǒng)釋放。因此我們通過下面的代碼分別來測(cè)試。

  • 靜態(tài)變量、全局變量、全局靜態(tài)變量

      - (void)testStaticObj
      {
      static NSString *staticString = nil;
      staticString = @"henvy";
    
      printf("%p\n", &staticString);//0x10b0d6138
      printf("%p\n", staticString);//0x10b0d5290
    
      void (^testBlock)() = ^{
      
      printf("%p\n", &staticString);//0x10b0d6138
      printf("%p\n", staticString);//0x0
    
      NSLog(@"%@", staticString);//null
      };
      staticString = nil;
    
      testBlock();
      }  
    

我這里只放上靜態(tài)變量的測(cè)試代碼,同全局變量、全局靜態(tài)變量。我們發(fā)現(xiàn)staticString對(duì)象在block的外部和內(nèi)部對(duì)象地址、指針地址都不變,且都在堆區(qū)。全局變量和全局靜態(tài)變量由于作用域在全局,所以在block內(nèi)訪問和讀寫這兩類變量和普通函數(shù)沒什么區(qū)別,而靜態(tài)變量作用域在block之外,靜態(tài)變量通過指針傳遞,將變量傳遞到block內(nèi),進(jìn)而來修改變量的值,即所謂的地址傳遞。

  • 局部變量

      - (void)testLocalObj
      {
      NSString *localString = nil;
      localString = @"henvy";
    
      printf("%p\n", &localString); //0x7fff569cca48
      printf("%p\n", localString); //0x109234290
    
      void (^testBlock)(void) = ^{
      
      printf("%p\n", &localString); //0x7fcd20511100
      printf("%p\n", localString); //0x109234290
    
      NSLog(@"%@", localString); //henvy
      };
      localString = nil;
    
      testBlock();
      printf("%p\n", &localString); //0x7fff569cca48
      printf("%p\n", localString); //0x0
      }
    

我們發(fā)現(xiàn)局部變量在block定義前在棧上開辟指針空間,在堆上開辟對(duì)象空間,當(dāng)然遵循ObjC對(duì)象的規(guī)則,在block內(nèi)部指針位置發(fā)生了變化,對(duì)象位置不變,在block定義后同定義前。因而我們發(fā)現(xiàn)block對(duì)于局部變量只對(duì)其對(duì)象的值進(jìn)行了拷貝,并不關(guān)心局部變量在外面的死活,跟block內(nèi)部沒有半點(diǎn)關(guān)系,正所謂的值傳遞。

  • block變量

      - (void)testBlockObj
      {
      __block NSString *blockString = @"henvy";
      printf("%p\n", &blockString); //0x7fff54507a38
      printf("%p\n", blockString); //0x10b6f9290
    
      void (^testBlock)(void) = ^{
      printf("%p\n", &blockString); //0x7feb79c1e4b8
      printf("%p\n", blockString); //0x10b6f9290
      
      NSLog(@"%@", blockString);//henvy
      };
    
      testBlock();
      printf("%p\n", &blockString); //0x7feb79c1e4b8
      printf("%p\n", blockString); //0x10b6f9290
      }
    

我們發(fā)現(xiàn)__block修飾符的變量在block內(nèi)部指針地址發(fā)生了變化,在block定義后地址徹底改為了新的地址,也就是說值徹底發(fā)生了變化,此時(shí)的blockString已經(jīng)不是當(dāng)年的那個(gè)blockString了。

總結(jié)一下:靜態(tài)變量、全局變量和全局靜態(tài)變量是通過指針傳遞,將變量傳遞到block內(nèi),進(jìn)而來修改變量值。而外部變量是通過值傳遞,自然沒法對(duì)獲取到的外部變量進(jìn)行修改,當(dāng)我們需要修改外部變量時(shí),可以用__block標(biāo)記變量,也就是說沒有__block標(biāo)記的變量,其值會(huì)被復(fù)制一份到block私有內(nèi)存區(qū),而有__block標(biāo)記的變量,其地址會(huì)被記錄一份在block私有內(nèi)存區(qū)。

4、Block循環(huán)引用

了解了強(qiáng)弱引用之后循環(huán)引用的問題就很好理解了,在ARC下,copy到堆上的block會(huì)強(qiáng)引用進(jìn)入到該block中的外部變量,這因而導(dǎo)致循環(huán)引用的問題,一旦出現(xiàn)循環(huán)引用那么對(duì)象就會(huì)常駐內(nèi)存,這顯然是誰(shuí)都不想看到的結(jié)果。此時(shí)需要用到__weak來打破這個(gè)閉合的環(huán)。

  • __weak

ViewController控制器內(nèi)有兩個(gè)屬性:

@property (nonatomic, copy)NSString *string;
@property (nonatomic, copy)void(^myBlock)();

在先分析下面的代碼:

self.string = @"henvy";
self.myBlock = ^{
    NSLog(@"%@",self.string);
};
self.myBlock();

首先self強(qiáng)引用myBlock,當(dāng)myBlock被copy到堆上時(shí),myBlock開始強(qiáng)引用self.string,myBlock的擁有者self在Block作用域內(nèi)部又引用了自己,因此導(dǎo)致了Block的擁有者永遠(yuǎn)無(wú)法釋放內(nèi)存,就出現(xiàn)了循環(huán)引用的內(nèi)存泄漏。解決辦法是__weak

#define HLWeakSelf(type)  __weak typeof(type) weak##type = type
self.string = @"henvy";
HLWeakSelf(self);
self.myBlock = ^{
    NSLog(@"%@",weakself.string);
};
self.myBlock();

__weak就在Block內(nèi)部對(duì)擁有者使用弱引用,通過這種方式告訴block,不要在block內(nèi)部對(duì)self進(jìn)行強(qiáng)制strong引用了。

  • weak-strong dance

在有些特殊情況下,我們?cè)赽lock中又使用__strong來修飾這個(gè)在block外剛剛用__weak修飾的變量。這么做其實(shí)是為了避免在block的執(zhí)行過程中,突然出現(xiàn)self被釋放的尷尬情況而導(dǎo)致crash,官方說法weak-strong dance。列舉經(jīng)典到發(fā)光的AFNetworking中AFNetworkReachabilityManager.m的一段代碼:

__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
__strong __typeof(weakSelf)strongSelf = weakSelf;

strongSelf.networkReachabilityStatus = status;
if (strongSelf.networkReachabilityStatusBlock) {
    strongSelf.networkReachabilityStatusBlock(status);
}
};

為了驗(yàn)證weak-strong dance下面我在一個(gè)HLBlockVC類中做如下實(shí)驗(yàn),實(shí)驗(yàn)?zāi)康脑谟谟^察block中的weakSelf到底有沒有釋放,在該類中會(huì)并發(fā)兩個(gè)線程,一個(gè)for循環(huán)到50后將weakSelf指針置空,另一個(gè)線程繼續(xù)for循環(huán)到100,實(shí)驗(yàn)可能存在兩種結(jié)果,一個(gè)是for循環(huán)到50block結(jié)束運(yùn)行即失敗,另一種情況block仍然繼續(xù)輸出到100即實(shí)驗(yàn)成功,下面代碼說話:

在HLBlockVC類的viewDidLoad方法中加載一個(gè)線程:

__block HLBlockVC *block = [[HLBlockVC alloc]init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [block toPrintNum];
});
for (int i = 0; i < 51; i ++) {
    if (i == 50) {
        block = nil;
        NSLog(@"BLOCK WAS NIL");
    }
}

添加toPrintNum方法,此時(shí)單單用weakSelf:

- (void) toPrintNum{
typedef void (^testBlock)();
__weak __typeof(self)weakSelf = self;
testBlock block = ^{        
    for (int i = 0; i < 100; i ++) {
        [weakSelf printNum:i];
    }
};
block();
}

-(void)printNum:(int)number{
NSLog(@"%d",number);
}

2016-12-30 17:02:23.791 blockDemo[8520:343178] 48
2016-12-30 17:02:23.791 blockDemo[8520:343098] BLOCK WILL NIL
2016-12-30 17:02:23.792 blockDemo[8520:343178] 49
2016-12-30 17:02:23.792 blockDemo[8520:343098] BLOCK WILL NIL
2016-12-30 17:02:23.792 blockDemo[8520:343178] 50
2016-12-30 17:02:23.792 blockDemo[8520:343098] BLOCK WAS NIL

代碼很清晰,看上面的打印在循環(huán)到50的時(shí)候block被干掉了,執(zhí)行結(jié)束,weakSelf下沒問題。接下來?yè)Q上weak-strong dance:

- (void) toPrintNum{
typedef void (^testBlock)();
__weak __typeof(self) weakSelf = self;
testBlock block = ^{
    __strong __typeof(weakSelf) strongSelf = weakSelf;
    
    for (int i = 0; i < 100; i ++) {
        [strongSelf printNum:i];
    }
};
block();
}

2016-12-30 18:03:42.934 blockDemo[8752:353486] 97
2016-12-30 18:03:42.935 blockDemo[8752:353486] 98
2016-12-30 18:03:42.935 blockDemo[8752:353486] 99

通過打印的數(shù)據(jù)可以看出__strong已經(jīng)安全的保護(hù)了block中的weakSelf使之運(yùn)行至block結(jié)束??梢哉fweak-strong dance是一種強(qiáng)引用 --> 弱引用 --> 強(qiáng)引用的變換過程,可能會(huì)被誤解為繞了一圈什么都沒做,其實(shí)不然,前者的強(qiáng)變?nèi)跏菫榱舜蚱崎]環(huán)的僵局,后者弱變強(qiáng)是為了block能夠一直持有弱引用的對(duì)象生命,而strongSelf是一個(gè)自動(dòng)變量會(huì)在函數(shù)執(zhí)行完釋放。

5、寫在最后

回想一下或許很多時(shí)候我們遇到的問題很小,確實(shí),就像文中的weak-strong dance,小到我們連遇到他犯錯(cuò)的機(jī)會(huì)都甚少,但堅(jiān)持把小事做透、以小見大方能防微杜漸,步步為營(yíng)!

夜深人靜,除了鍵盤聲,就是耳機(jī)里傳來的歌聲【旅行團(tuán)--生命是場(chǎng)馬拉松】夾雜著深夜瑣碎的思緒,希望每一次發(fā)文都是對(duì)自己的一次洗禮。最后,晚安。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • iOS面試小貼士 ———————————————回答好下面的足夠了------------------------...
    不言不愛閱讀 2,249評(píng)論 0 7
  • 多線程、特別是NSOperation 和 GCD 的內(nèi)部原理。運(yùn)行時(shí)機(jī)制的原理和運(yùn)用場(chǎng)景。SDWebImage的原...
    LZM輪回閱讀 2,120評(píng)論 0 12
  • ———————————————回答好下面的足夠了---------------------------------...
    恒愛DE問候閱讀 1,843評(píng)論 0 4
  • 2.1 Blcoks概要 2.1.1 什么是Blocks Blocks是C語(yǔ)言的擴(kuò)充功能——“帶有自動(dòng)變量(即局部...
    SkyMing一C閱讀 2,449評(píng)論 6 18
  • __block和__weak修飾符的區(qū)別其實(shí)是挺明顯的:1.__block不管是ARC還是MRC模式下都可以使用,...
    LZM輪回閱讀 3,594評(píng)論 0 6

友情鏈接更多精彩內(nèi)容