Block深度軟文

the block

前言

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

  • 指針和對象,都是內存塊。一個大,一個小。一個在棧中,一個在堆中。
  • iOS中,我們可以生命一個指針,也可以通過alloc獲取一塊內存。
  • 我們可以直接消滅一個指針,將其置為nil,但是我們沒辦法直接消滅一塊對象內存。對于對象內存,我們永遠只能依靠系統去回收。即當這個對象不被任何指針所擁有時,系統就會收回該對象內存。
  • 函數在棧區,函數調用完畢后其stack frame將被彈出結束其生命周期。
  • Objective-C的對象在內存中是以堆的方式分配空間。

正文

1、ARC strong和weak指針

在講block之前呢先講一下strong和weak指針的問題,以便于更好的理解下面block中的循環引用以及變量截獲等問題。我們知道ARC消除了手動管理內存的煩瑣,編譯器會自動在適當的地方插入適當的retain、release、autorelease語句。規則很簡單,只要還有一個變量指向對象,對象就會保持在內存中。當指針指向新值,或者指針不再存在時,相關聯的對象就會自動釋放。如下圖動畫模擬引用計數回收器,紅色閃爍表示引用計數行為,引用計數的優勢在于垃圾會被很快檢測到,你可以看到紅色閃爍過后緊接著該區域變黑(圖片來自)。

REF_COUNT
  • strong指針

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

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

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

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

我們稱name和nameField.text指針為"Strong指針",因為它們能夠保持對象的生命。默認所有成員變量和局部變量都是Strong指針。

  • weak指針

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

此時如果文本框內容重新輸入@“Leslie”,則原先的henvy對象就沒有擁有者,就會被釋放,此時name變量會自動變成nil,稱為空指針。weak型的指針變量自動變為nil避免了野指針的產生。

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

2、Block的類型

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

  • NSGlobalBlock

該類型的block存儲在程序的數據區域(text段),不引用外部變量,只對自己的參數做操作,自給自足的狀態,可以當做函數使用,例如:

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

該類型的block在非ARC模式存儲在棧區,內部引用外部變量,當棧block結束運行的時候會被請出棧,生命周期結束,再次調用當然crash掉,避免這一點可以通過手動copy將其拷貝到安全的堆上來,脫離棧的危險地帶,因為本身棧區就是過河拆橋、兔死狗烹的狀態。不像堆區講究循環利用,生死由天定(無指針擁有被系統回收)。當然在ARC模式完全不用擔心,ARC模式改寫了天規杜絕NSStackBlock狀況的發生,他會自動將block拷貝到堆上去(block作為方法或函數的參數傳遞時,編譯器不會自動調用copy方法),從而演變成了第三種NSMallocBlock,此時的堆上的block就會像一個ObjC對象一樣被放入autoreleasepool里面,從而保證了返回后的block仍然可以正確執行。因此在本該是NSStackBlock的情況下打印結果就會變成NSMallocBlock。

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

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

3、Block對外部變量的存儲管理

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

  • 靜態變量、全局變量、全局靜態變量

      - (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();
      }  
    

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

  • 局部變量

      - (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
      }
    

我們發現局部變量在block定義前在棧上開辟指針空間,在堆上開辟對象空間,當然遵循ObjC對象的規則,在block內部指針位置發生了變化,對象位置不變,在block定義后同定義前。因而我們發現block對于局部變量只對其對象的值進行了拷貝,并不關心局部變量在外面的死活,跟block內部沒有半點關系,正所謂的值傳遞。

  • 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
      }
    

我們發現__block修飾符的變量在block內部指針地址發生了變化,在block定義后地址徹底改為了新的地址,也就是說值徹底發生了變化,此時的blockString已經不是當年的那個blockString了。

總結一下:靜態變量、全局變量和全局靜態變量是通過指針傳遞,將變量傳遞到block內,進而來修改變量值。而外部變量是通過值傳遞,自然沒法對獲取到的外部變量進行修改,當我們需要修改外部變量時,可以用__block標記變量,也就是說沒有__block標記的變量,其值會被復制一份到block私有內存區,而有__block標記的變量,其地址會被記錄一份在block私有內存區。

4、Block循環引用

了解了強弱引用之后循環引用的問題就很好理解了,在ARC下,copy到堆上的block會強引用進入到該block中的外部變量,這因而導致循環引用的問題,一旦出現循環引用那么對象就會常駐內存,這顯然是誰都不想看到的結果。此時需要用到__weak來打破這個閉合的環。

  • __weak

ViewController控制器內有兩個屬性:

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

在先分析下面的代碼:

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

首先self強引用myBlock,當myBlock被copy到堆上時,myBlock開始強引用self.string,myBlock的擁有者self在Block作用域內部又引用了自己,因此導致了Block的擁有者永遠無法釋放內存,就出現了循環引用的內存泄漏。解決辦法是__weak

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

__weak就在Block內部對擁有者使用弱引用,通過這種方式告訴block,不要在block內部對self進行強制strong引用了。

  • weak-strong dance

在有些特殊情況下,我們在block中又使用__strong來修飾這個在block外剛剛用__weak修飾的變量。這么做其實是為了避免在block的執行過程中,突然出現self被釋放的尷尬情況而導致crash,官方說法weak-strong dance。列舉經典到發光的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);
}
};

為了驗證weak-strong dance下面我在一個HLBlockVC類中做如下實驗,實驗目的在于觀察block中的weakSelf到底有沒有釋放,在該類中會并發兩個線程,一個for循環到50后將weakSelf指針置空,另一個線程繼續for循環到100,實驗可能存在兩種結果,一個是for循環到50block結束運行即失敗,另一種情況block仍然繼續輸出到100即實驗成功,下面代碼說話:

在HLBlockVC類的viewDidLoad方法中加載一個線程:

__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方法,此時單單用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

代碼很清晰,看上面的打印在循環到50的時候block被干掉了,執行結束,weakSelf下沒問題。接下來換上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

通過打印的數據可以看出__strong已經安全的保護了block中的weakSelf使之運行至block結束。可以說weak-strong dance是一種強引用 --> 弱引用 --> 強引用的變換過程,可能會被誤解為繞了一圈什么都沒做,其實不然,前者的強變弱是為了打破閉環的僵局,后者弱變強是為了block能夠一直持有弱引用的對象生命,而strongSelf是一個自動變量會在函數執行完釋放。

5、寫在最后

回想一下或許很多時候我們遇到的問題很小,確實,就像文中的weak-strong dance,小到我們連遇到他犯錯的機會都甚少,但堅持把小事做透、以小見大方能防微杜漸,步步為營!

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

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

推薦閱讀更多精彩內容

  • iOS面試小貼士 ———————————————回答好下面的足夠了------------------------...
    不言不愛閱讀 1,995評論 0 7
  • 多線程、特別是NSOperation 和 GCD 的內部原理。運行時機制的原理和運用場景。SDWebImage的原...
    LZM輪回閱讀 2,020評論 0 12
  • ———————————————回答好下面的足夠了---------------------------------...
    恒愛DE問候閱讀 1,736評論 0 4
  • 2.1 Blcoks概要 2.1.1 什么是Blocks Blocks是C語言的擴充功能——“帶有自動變量(即局部...
    SkyMing一C閱讀 2,360評論 6 18
  • __block和__weak修飾符的區別其實是挺明顯的:1.__block不管是ARC還是MRC模式下都可以使用,...
    LZM輪回閱讀 3,348評論 0 6