深入淺出 block
前言
由於筆者前段時間一直忙著上家公司交接和找工作,近期文章很少有更新。簡單說一下前幾天的面試感觸,總共面試了七家公司,第一家奇虎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.
翻譯: 閉包是一個函式(或指向函式的指標),再加上該函式執行的外部的上下文變數(有時候也稱作自由變數)。
上圖是 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
通過上圖我們可以看到,三種 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"];
};
五、參考
相關文章
- block深入淺出BloC
- 讓我們來深入淺出block吧BloC
- 深入淺出FE(十四)深入淺出websocketWeb
- 深入淺出——MVCMVC
- 深入淺出mongooseGo
- HTTP深入淺出HTTP
- 深入淺出IO
- 深入淺出 RabbitMQMQ
- 深入淺出PromisePromise
- ArrayList 深入淺出
- mysqldump 深入淺出MySql
- 深入淺出decorator
- 深入淺出 ZooKeeper
- 機器學習深入淺出機器學習
- 深入淺出HTTPHTTP
- http 深入淺出HTTP
- 深入淺出 ARCore
- 深入淺出 synchronizedsynchronized
- 深入淺出WebpackWeb
- 淺讀-《深入淺出Nodejs》NodeJS
- 《深入淺出webpack》有感Web
- 深入淺出 Laravel MacroableLaravelMac
- 反射的深入淺出反射
- Flutter | 深入淺出KeyFlutter
- 深入淺出 CSS 動畫CSS動畫
- 深入淺出:HTTP/2HTTP
- 深入淺出 Laravel EchoLaravel
- 深入淺出Spark JoinSpark
- 深入淺出 ThreadLocalthread
- 深入淺出JavaScript之thisJavaScript
- 深入淺出談 socket
- 深入淺出this的理解
- [譯] 深入淺出 SVGSVG
- 深入淺出理解ReduxRedux
- 深入淺出ClassLoader
- 深入淺出timestamp
- 深入淺出JS動畫JS動畫
- 深入淺出 JSONPJSON