Block詳解一(底層分析)

  • 2020 年 3 月 17 日
  • 筆記

本篇部落格不再講述Block的基本定義使用,最近而是看了很多的block部落格講述的太亂太雜,所以抽出時間整理下block的相關底層知識,在講述之前,提出幾個問題,如果都可以回答出來以及知道原理,大神繞過,反之,希望本篇部落格對大家面試或者block不熟悉者有所幫助,以後會不斷更新部落格,歡迎關注和指正!!!

  1. blcok的原理是怎樣的?本質又是什麼?
  2. __block的作用是什麼?有什麼使用注意點?
  3. block的屬性修飾詞為什麼是copy修飾?使用block有哪些使用注意事項?
  4. block在修改NSMutableArray,需要不需要添加__block?

一、 block本質

  • blcok本質是OC對象,它內部也有個isa指針,在OC中有isa指針的對象,可以認定為OC對象
  • block對象是封裝了函數調用以及函數調用環境的OC對象

通過下面例子看下block結構

文件結構

int main(int argc, const char * argv[]) {      @autoreleasepool {          int age = 10;          void (^block)(int, int) = ^(int a, int b){              NSLog(@"This is a block  --%d",age);              NSLog(@"This is a block");              NSLog(@"This is a block");          };          block(10, 20);      }      return 0;  }

通過clang編譯器將OC程式碼編譯成C語言程式碼,並生成了在後綴名為.cpp的C++文件中,clang命令為

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

然後查看編譯出來的c++ main.cpp文件,和main.m同一個地方,將它移入到項目中,並在Build Phases->Compile Sources中刪除main.cpp,然後就可以編譯成功

 這次開始查看main.cpp的,對比兩個文件,找出main.cpp的對應的main函數

 看到上面main.cpp左邊的調用block,10,20前面有很多的強制類型轉換,最後可以是funcPtr (block,10,20)在.cpp中海油一個__main_block_imp_0地址,查看其地址內容

 

二、block變數捕獲機制

 舉例1: block變數捕獲-auto變數

經常書寫int age = 10,前面都是有默認關鍵字auto的(也可以不書寫,經常這樣的),下面的結果是什麼?

int age = 10 等價於 auto int age = 10 (auto自動變數,離開作用域就會銷毀)

int main(int argc, const char * argv[]) {      @autoreleasepool {          int age = 10;          void (^block)(void) = ^{              NSLog(@"age is %d", age);          };          age = 20;          block();      }      return 0;  }

猜一下運行結果

 再次運行查看編譯出來的main.cpp,重複上面的步驟,對比main.cpp

 查看__main_block_impl_0的結構,傳入的參數age = 10,block內部新增了一個變數用於存儲age

 block的內部的age = 10 ,並不會改變,所以列印結果為10(當創建block的內容,age = 10已經存在了block中,並不會隨外部改變而改變)

舉例2:block變數捕獲-static變數

int main(int argc, const char * argv[]) {      @autoreleasepool {          auto int age = 10;          static int height = 10;          void (^block)(void) = ^{              NSLog(@"age is %d, height is %d", age, height);          };          age = 20;          height = 20;          block();      }      return 0;  }

結果為age = 10,height = 20

將其編譯為cpp,對比下 

 查看__main_block_impl_0的程式碼結構

發現block捕獲到了age和height,所以block會捕獲到局部變數,而靜態局部變數block存放的是地址,所以未來修改height的值時,取出的是所指向的最新的height值

舉例3:block變數捕獲-全局變數 

int age = 10;  static int height = 10;  int main(int argc, const char * argv[]) {      @autoreleasepool {          void (^block)(void) = ^{              NSLog(@"age is %d, height is %d", age, height);          };          age = 20;          height = 20;          block();      }      return 0;  }

查看.cpp文件有沒有捕獲到全局變數?–直接訪問

 對於上面的block變數捕捉機制,總結如下:

 

 三、block的類型

block的底層結構如下

block有3種類型,可以通過調用class方法或者isa指針查看具體類型,最終都是繼承自NSBlock類型

 到底什麼類型的block屬於__NSGlobalBlock__,__NSStackBlock__, __NSMallocBlock__?

 先不鋪墊那麼多,直接給出結論:

 下面來一一驗證結論

 就是像上面提問那樣,按總結的訪問了auto變數應該是NSStackBlock,怎麼成為了NSMallocBlock了呢?

這是因為編譯器默認在ARC環境下,應該切換到MRC環境下,看一下真正的block類型,至於ARC的,下篇講述!!!

將編譯器改為去除ARC,在Build Settings -> automatic Reference Counting 中將ARC改為MRC

 改成MRC後再次允許查看結果

 那什麼時候是NSMallocBlock呢?

NSStackBlock調用copy變為NSMallocBlock,但是NSGlobalBlock調用了copy依然是NSGlobalBlock,NSMallocBlock調用了copy方法引用計數會+1

 那麼在ARC環境下什麼時候block會調用copy呢?(從棧空間->堆空間)

在ARC環境下,編譯器會根據情況自動將棧上的block複製到堆上,比如有以下情況:

  • block作為函數返回值時
  • block賦值給__strong指針時
  • block作為Cocoa API中方法名有usingBlock的方法參數時 如:[array enumeratorObjectsUsingBlock]
  • block作為GCD API的方法參數時

 

四、對象類型的auto變數

上面講述auto變數是Int等基本類型,現在改成對象類型,如Person類對象[ARC環境下面]

 1 typedef void(^ZXYBlock)(void);   2 int main(int argc, const char * argv[]) {   3     @autoreleasepool {   4         ZXYBlock block ;   5         {   6             Person *person = [[Person alloc]init];   7             person.age = 10;   8             block = ^{   9                 NSLog(@"---------%d", person.age);  10             };  11         }  12         NSLog(@"block執行完");  13     }  14     return 0;  15 }  16  17 @interface Person : NSObject  18 @property (nonatomic, assign) int age;  19 @end  20  21 @implementation Person  22 -(void)dealloc {  23     NSLog(@"person對象已釋放");  24 }  25 @end

將上面的程式碼打breakPoint斷點在12行處,查看Person對象是否在打括弧{}內釋放

發現在列印之前並沒有釋放person對象,猜想block引用了person,導致block執行完之後才被釋放(block當autoReleasePool執行完之後才會被釋放) 查看c++程式碼

 查看main函數調用

 通過上面查看結構體struct __main_block_desc 裡面多了兩個copy和dispose(相當於retain)對person進行捕捉到age變數,當block不被釋放,person對象也不會被釋放

當斷點改到14行,執行完block時,查看結果

 block被釋放,完成列印釋放

 總結:對象訪問的auto變數

當block內部訪問了對象類型的auto變數時

  • 如果block在棧上,將不會對auto變數產生強引用
  • 如果block被拷貝到堆上
  1. 會調用block內部的copy函數
  2. copy函數內部會調用Block_object_assign 函數
  3. Block_object_assign函數會根據auto變數的修飾符(__strong、__weak、__unsafe_unretained)做出相應的操作,形成強引用(retain)或者弱引用

  • 如果block從堆上移除
  1. 會調用block內部的dispose函數
  2. dispose函數內部會調用_Block_object_dispose函數
  3. _Block_object_dispose函數會自動釋放引用的auto變數

 

以上就是block詳解一的內容,下一篇講述block剩下的知識點,歡迎關注!!!