深入淺出 block

weixin_33751566發表於2017-12-23

前言

由於筆者前段時間一直忙著上家公司交接和找工作,近期文章很少有更新。簡單說一下前幾天的面試感觸,總共面試了七家公司,第一家奇虎360面試,同兩個技術聊了兩小時左右,有些基本東西回答的不是很好,沒有準備充分,最後不得而終;第二家公司規模一般,拿到了offer但是沒去;第三家公司便是目前所在的公司,筆試➕面試➕人事大概面了六輪;第四家是蘇寧,過了第一輪面試,複試因技術總監出差,耽誤了一週,最終通知複試筆者已經入職現在的公司;另外三家公司面試都很一般,其中一家公司掛羊頭賣狗肉,說招聘iOS開發工程師,結果去了一談原來是做遊戲開發,最終筆者建議該家公司把招聘崗位改為 Cocos-2d 工程師。此外接收到京東、優行二手車以及一一五的面試邀請,最終都沒去面試。

面試的公司中有大公司也有小公司,其中有一點最深的感觸,大公司和小公司面試的問題的差異確實很大。小公司偏重於專案經驗和業務,而大公司偏向於技術深度,什麼執行緒、執行時、block、效能優化、runloop等都會往深處去問。另外,很多人說大公司面試對演算法要求很高,實際面試過程中筆者的感覺是,演算法要求不是很高,可能會問到一丁點演算法的問題,但是很少也很基礎,能熟練掌握基本的資料結構,知道一些基礎的演算法應該足夠應對面試。當然也可能是筆者的眼界太低。

發現在之前的一些面試中,有很多知識點掌握的還是太淺,所以最近打算集中抽一些時間來研究面試中被往深處問的一些問題。廢話就到此為止,看文章!!!!!

概述

一、block是什麼?

先看看 block 的官方定義。

In programming languages, a closure is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.
翻譯: 閉包是一個函式(或指向函式的指標),再加上該函式執行的外部的上下文變數(有時候也稱作自由變數)。

4146031-d8e0ef4b48a7ac5e.png
block 結構

上圖是 block 資料結構定義,block 實際上也是一個物件。原因就在於 isa 指標。所有物件的都有isa 指標,用於實現物件相關的功能。關於 isa 指標這裡不做深入講解,如果想深入瞭解請看 runtime 相關的知識點,同時文章的末尾會推薦相關連結。

如果想進一步深入瞭解 block 的底層實現,推薦這兩篇文章(涉及 C++程式碼)。談Objective-C block的實現《Objective-C 高階程式設計》乾貨三部曲(二):Blocks篇

二、關於__block

關於 block iOS 開發者應該知道:

  • block 中不允許修改外部變數的值(棧中指標的記憶體地址)。
  • 對於 block 外的變數引用,block 預設是將其複製到其資料結構中來實現訪問的。
  • __block 所起到的作用就是隻要觀察到該變數被 block 所持有,就將“外部變數”在棧中的記憶體地址放到了堆中。進而在block內部也可以修改外部變數的值。
    可以結合下面的程式碼,以及分析理解這三句話。
//針對第一句和第二句去分析
int age = 10;
myBlock block = ^{
    NSLog(@"age = %d", age);//結果為:10
};
age = 18;
block();
//針對第三句去分析
__block int age = 10;
myBlock block = ^{
    NSLog(@"age = %d", age);//結果為:18
};
age = 18;
block();
2.1 為什麼不允許修改 block 中修改外部變數的值?

要知道 block 和函式實際非常類似,本身也是屬於"函式"範疇。變數進入block,實際就是已經改變了作用域。在幾個作用域之間進行切換時,如果不加上這樣的限制,變數的可維護性將大大降低。

試想這樣一個場景,block 內宣告瞭一個與外部同名的變數,此時是允許修改還是不允許呢?只有加上了這樣的限制,這樣的情景才更容易控制吧。

2.2 ARC下,訪問外界變數的 block 為何要自動從棧區拷貝到堆區?

棧上的 block,如果其所屬的變數作用域結束,該 block 就被廢棄,如同一般的區域性變數。同時,block中的 __block 變數也被廢棄。為了解決棧塊在其變數作用域結束之後被廢棄(釋放)的問題,我們需要把 block 複製到堆中,延長其生命週期。開啟ARC時,大多數情況下編譯器會恰當地進行判斷是否有需要將Block從棧複製到堆,如果有,自動生成將 block 從棧上覆制到堆上的程式碼。block 的複製操作執行的是 copy 例項方法。block 只要呼叫了copy方法,棧塊就會變成堆塊。

通常在ARC中,block 都是通過copy 屬性修飾符修飾的,實際上如果不寫這個 copy 也是沒有關係的,ARC情況下對block的處理比較特殊,預設是直接 執行 copy 操作的,寫上 copy 也無所謂,寫上copy的話可以時刻提醒我們block從棧區copy到堆區上。

2.3 使用__block後,棧區和堆區的驗證問題
   __block int a = 0;
   NSLog(@"1、%p", &a);         //棧區
   void (^testBlock)(void) = ^{
       a = 1;
       NSLog(@"2、%p", &a);    //堆區
   };
   NSLog(@"3、%p", &a);         //堆區
   testBlock();
//列印結果

2 和 3 兩者地址時一樣的, block 內部的變數被 copy 到堆區,所以可以知道 3 的地址也是堆地址。把上述答應的地址由16進位制轉為10進位制,則對應的轉化結果為:

  • --->
  • --->
    兩個十進位制地址相差438851376個位元組,大約是 418.5M 的空間。因為堆地址要小於棧地址,又因為 iOS 中一個程式的棧區記憶體只有 1 M,OS X 也只有 8 M,顯然 2 和 3 地址屬於堆區。

三、block 的型別

block 有三種型別:

  • NSConcreteGlobalBlock
  • NSConcreteStackBlock
  • NSConcreteMallocBlock
4146031-69496e9548e92b0d.png
三種 block 對應的儲存區域

通過上圖我們可以看到,三種 block 對應的儲存區域。儲存在棧中的Block就是棧塊、儲存在堆中的就是堆塊、既不在棧中也不在堆中的塊就是全域性塊。

如果碰到一個 block 怎麼知道它是三種型別的哪一種,我們先做一個簡單的總結,接下來再針對每種情況細說。

  • block不訪問外界變數(包括棧中和堆中的變數)
    此時 block 既不在棧也不在堆中,而是在程式碼段中,ARC和MRC下都是如此。此時為全域性塊。>>>>NSConcreteGlobalBlock
  • block訪問外界變數
    1、MRC 環境下:訪問外界變數的 block 預設儲存棧中。>>>>NSConcreteStackBlock
    2、ARC 環境下:訪問外界變數的 block 預設儲存在堆中(實際是放在棧區,然後ARC情況下自動又拷貝到堆區),自動釋放。>>>>NSConcreteMallocBlock
3.1 NSConcreteGlobalBlock

如果一個 block 中沒有引用外部變數並且沒有被其他物件持有,就是NSConcreteGlobalBlock。

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"%@",^{printf("NSConcreteGlobalBlock");});
}
3.2 NSConcreteStackBlock

可以這麼理解,形式上NSConcreteStackBlock同NSConcreteGlobalBlock相比,引用了外部變數的block。如下程式碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    int a = 10;
    NSLog(@"%@",^{
        printf("NSConcreteStackBlock");
        printf("%d", a);
    });
}

除此之外,還要知道 NSConcreteStackBlock 內部會有一個結構體__main_block_impl_0,這個結構體會儲存外部變數,使其體積變大。這就導致了NSConcreteStackBlock並不像巨集一樣,而是一個動態的物件。而它由於沒有被持有,所以在它的內部,它也不會持有其外部引用的物件。下面的程式碼可以驗證這一切:

- (void)test{
    NSObject *obj = [[NSObject alloc]init];
    NSLog(@"1、%lu",obj.retainCount);
    void(^bloc)(void) = ^{
        NSLog(@"2、%lu",obj.retainCount);
    };
    bloc();
    NSLog(@"3、%lu",obj.retainCount);
}

//列印結果,引用計數始終未發生變化(注意僅在MRC環境下才能使用retainCount屬性)
Test[17014:1040370] 1、1
2017-12-23 15:30:59.560310+0800 Test[17014:1040370] 2、1
2017-12-23 15:30:59.560434+0800 Test[17014:1040370] 3、1
3.3 NSConcreteMallocBlock

當一個block被copy時,將生成 NSConcreteMallocBlock。同 NSConcreteStackBlock 相比,不同的是 NSConcreteMallocBlock 會持有外部物件!

- (void)test{
    NSObject *obj = [[NSObject alloc]init];
    NSLog(@"1、%lu",obj.retainCount);
    void(^bloc)(void) = [^{
        NSLog(@"2、%lu",obj.retainCount);
    } copy];
    bloc();
    NSLog(@"3、%lu",obj.retainCount);
}
//列印結果
Test[17064:1046117] 1、1
2017-12-23 15:40:05.311172+0800 Test[17064:1046117] 2、2
2017-12-23 15:40:05.311301+0800 Test[17064:1046117] 3、2

3.4 小結
有人認為:在 ARC 開啟的情況下,將只會有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 型別的 block。其實這種說法是錯誤的,ARC下預設的賦值操作是strong的,到了 block 身上自然就成了copy,所以常常列印出來的 block 就是NSConcreteMallocBlock了。只是在 ARC 中,系統預設做了 copy 操作,我們無法看到而已。

四、關於 block 的生命週期(Strong Weak Dance)

下面的寫法可以避免迴圈引用。但是有發生崩潰的可能,假設block被放在子執行緒中執行,而且執行過程中self在主執行緒被釋放了。由於wself是一個弱引用,因此會自動變為nil。在 KVO 中這會導致崩潰。

__weak MyViewController *wself = self;
self.completionHandler = ^(NSInteger result) {
    [wself.property removeObserver: wself forKeyPath:@"pathName"];
};

解決辦法,先將強引用的物件轉為弱引用指標,防止了Block和物件之間的迴圈引用。再在Block的中,將weakSelf的弱引用轉換成strongSelf這樣的強引用指標,防止了多執行緒和ARC環境下弱引用隨時被釋放的問題。

__weak MyViewController *wself = self;
self.completionHandler = ^(NSInteger result) {
    __strong __typeof(wself) sself = wself; // 強引用一次
    [sself.property removeObserver: sself forKeyPath:@"pathName"];
};

五、參考