1. 程式人生 > >Block詳解一(底層分析)

Block詳解一(底層分析)

本篇部落格不再講述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剩下的知識點,歡迎關