天天看點

block底層源碼分析前言block的使用block調用外部變量block代碼分析3、__block在block中的作用。

前言

要探究block前先說一下我對block的了解,我把它了解為;能夠捕獲他所有函數内部的變量的函數指針,匿名函數或者閉包。

block的使用

iOS4.0開始進入block特性。也叫做閉包。是一個函數(或指向函數的指針),再加上該函數執行的外部的上下文變量(有時候也稱作自由變量)。

  • block的聲明:

void (^blockName)(int arg1, int arg2);

中文翻譯:傳回值(^block變量名)(block的參數)

  • block的定義:

^void(int arg1, int arg2) {};

中文翻譯:^傳回類型(block的參數)

  • block隻有在調用的時候才能執行裡面的函數内容

- (void)viewDidLoad {
    
     void (^blockName)(int, int) = ^(int arg1, int arg2) {
        NSLog(@"arg1 + arg2 = %d", arg1 + arg2);
    };
    blockName(1,2);

    //2、沒有參數
    void (^blockName2)() = ^() {
        NSLog(@"block2");
    };
    blockName2();
    
    //3、block有傳回值
    int (^blockName3)(int) = ^(int n) {
        return n * 2;
    };
    //4、block作為方法的參數
    [self testBlock2:10];
}

- (void)testBlock2:(int(^)(int))myBlock {
    NSLog(@"調用了")
}
           

block調用外部變量

block隻能讀取,不能修改局部變量。這個時候是值傳遞。

如果想修改局部變量,要用__block來修飾。這個時候是引用傳遞。下面會聊下block的實作原理。

//1、調用局部變量,不用__block
    NSInteger testNum2 = 10;
    void (^block2)() = ^() {
        //testNum = 1000; 這樣是編譯不通過的
        NSLog(@"修改局部變量: %ld", testNum2); //列印:10
    };
    testNum2 = 20;
    block2();
    NSLog(@"testNum最後的值: %ld", testNum2);//列印:20
    
    //2、修改局部變量,要用__block
    __block NSInteger testNum3 = 10;
    void (^block3)() = ^() {
        NSLog(@"讀取局部變量: %ld", testNum3); //列印:20
        testNum3 = 1000;
        NSLog(@"修改局部變量: %ld", testNum3); //列印:1000
    };
    testNum3 = 20;
    block3();
    testNum3 = 30;
    NSLog(@"testNum最後的值: %ld", testNum3);//列印:30
           

block代碼分析

網上很多通過Clang(LLVM編譯器)将OC的代碼轉換成C++源碼,來進行分析的。但是這些轉換的代碼并不是block的源代碼,隻是用來了解用的過程代碼。

1、block不包含任何變量

建立一個testBlock.m檔案。檔案中代碼為:

block底層源碼分析前言block的使用block調用外部變量block代碼分析3、__block在block中的作用。

執行clang指令:

clang -rewrite-objc testBlock.m
           

生成.cpp的核心代碼主要在.cpp檔案的底部,大家可以看下圖:

block底層源碼分析前言block的使用block調用外部變量block代碼分析3、__block在block中的作用。

我加了比較詳細的注釋,具體的看圖檔就好。這裡重點強調下關鍵的東西:

  • 1.1、其中block的結構體:
struct __block_impl
{
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};
           

isa:isa指針,在Objective-C中,任何對象都有isa指針。block 有三種類型:

_NSConcreteGlobalBlock:全局的靜态 block,類似函數。如果block裡不擷取任何外部變量。或者的變量是全局作用域時,如成員變量、屬性等; 這個時候就是Global類型

_NSConcreteStackBlock:儲存在棧中的 block,棧都是由系統管理記憶體,當函數傳回時會被銷毀。__block類型的變量也同樣被銷毀。為了不被銷毀,block會将block和__block變量從棧拷貝到堆。

_NSConcreteMallocBlock:儲存在堆中的 block,堆記憶體可以由開發人員來控制。當引用計數為 0 時會被銷毀。

代碼執行的時候,block的isa有上面3中值。後面還會進行詳細的說明。

  • 1.2、__main_block_func_0 是block要執行的函數:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        printf("列印block函數");
}
           
  • 1.3、__main_block_desc_0 是block的描述資訊 的結構體
  • 1.4、block的類型。

 在上圖中可以看到:

impl.isa = &_NSConcreteStackBlock; 
           

這裡 impl.isa 的類型為_NSConcreteStackBlock,由于 clang 改寫的具體實作方式和 LLVM 不太一樣,是以這裡 isa 指向的還是

_NSConcreteStackBlock

。但在 LLVM 的實作中,開啟 ARC 時,block 應該是 _NSConcreteGlobalBlock 類型。

是以 block是什麼類型 在 clang代碼裡是看不出來的。

如果要檢視block的類型還是要通過Xcode進行列印:

block底層源碼分析前言block的使用block調用外部變量block代碼分析3、__block在block中的作用。

列印的結果:

clangBlk = <__NSGlobalBlock__: 0x100054240>
列印block函數
           

上面block代碼,沒有擷取任何外部變量,應該是 _NSConcreteGlobalBlock類型的。該類型的block和函數一樣 存放在 代碼段 記憶體段。記憶體管理簡單。

2、block 通路 局部變量 

 建立testBlock2.m檔案,代碼如下:

block底層源碼分析前言block的使用block調用外部變量block代碼分析3、__block在block中的作用。

通過clang指令生成 的核心代碼如下,和剛才clang的代碼 不同的地方 已經加了注釋:

block底層源碼分析前言block的使用block調用外部變量block代碼分析3、__block在block中的作用。
  • 2.1、可以看到 __main_block_impl_0 中添加了 一個int num的變量。在 __main_block_func_0中使用了該變量。

從這裡可以看出來 這裡是 值拷貝,不能修改,隻能通路。

  • 2.2、用Xcode列印上面block代碼,得到的類型為:__NSMallocBlock。

在說_NSConcreteMallocBlock類型前,我們先說下_NSConcreteStackBlock類型。

_NSConcreteStackBlock類型的block存在棧區,當變量作用域結束的時候,這個block和block上的__block變量就會被銷毀。

這樣當block擷取了局部變量,在其他地方通路的時候就會崩潰。block通過copy來解決了這個問題,可以将block從棧拷貝到堆。這樣當棧上的作用域結束後,仍然可以通路block和block中的外部變量。

我們現在看下本文開頭的問題1:

block底層源碼分析前言block的使用block調用外部變量block代碼分析3、__block在block中的作用。

為什麼局部變量muArray出了作用域 還能存在?

captureBlk為預設的__strong類型,當block被指派給__strong類型的對象或者block的成員變量時,編譯器會自動調用block的copy方法。

執行copy方法,block拷貝到堆上,mutArray變量指派給block的成員變量。是以列印的結果就為1,2,3。

如果把上面代碼中的mutArray改為weak類型,那麼列印的就都是0了。因為當出去作用域的時候,mutArray就已經被釋放了。

同時,因為NSMutableArray *mutArray 是引用類型,用clang指令執行後,發現:

struct __main_block_impl_0
{
    struct __block_impl impl;
    struct __main_block_desc_0 *Desc;
    id __strong mutArray;
    .....

}
           

mutArray在block中是id類型,因為是指針 是以在block中mutArray是可以修改的,而int類型的不能修改。當然如果用__block也能修改int類型的外部變量,下面我們會詳說。

下面這個列印的結果是1,也是這個道理:

block底層源碼分析前言block的使用block調用外部變量block代碼分析3、__block在block中的作用。

同時通路外部變量是block進行的值傳遞,是以列印的還是1,不是2。

  • 2.3、什麼情況下block會進行copy操作。

用代碼顯示的調用copy操作:

[captureBlk2 copy];
           

在MRC下block定義的屬性都要加上copy,ARC的時候block定義copy或strong都是可以的,因為ARC下strong類型的block會自動完成copy的操作。

@property (nonatomic, strong) captureObjBlk2 captureBlk21;
           

當 block 作為函數傳回值傳回時。

當 block 被指派給 __strong id 類型的對象或 block 的成員變量時。

當 block 作為參數被傳入方法名帶有 

usingBlock

 的 Cocoa Framework 方法或 GCD 的 API 時。

3、__block在block中的作用。

建立testBlock3.m,代碼如下:

block底層源碼分析前言block的使用block調用外部變量block代碼分析3、__block在block中的作用。

用clang生成的代碼如下,進行了詳細的注釋:

block底層源碼分析前言block的使用block調用外部變量block代碼分析3、__block在block中的作用。

block通路的外部變量,在block中就是一個結構體:__Block_byref_num_0:

// 一、用于封裝 __block 修飾的外部變量
struct __Block_byref_num_0 {
    void *__isa;    // 對象指針
    __Block_byref_num_0 *__forwarding;  // 指向 拷貝到堆上的 指針
    int __flags;    // 标志位變量
    int __size;     // 結構體大小
    int num;        // 外部變量
};
           

 其中 int num 為外部變量名。

__Block_byref_num_0 *__forwarding; 這個是指向自己堆上的指針,這個後面會詳細說明。

為了對__Block_byref_num_0結構體進行記憶體管理。新加了copy和dispose函數:

//四、對__Block_byref_num_0結構體進行記憶體管理。新加了copy和dispose函數。
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    // _Block_object_assign 函數:當 block 從棧拷貝到堆時,調用此函數。
    _Block_object_assign((void*)&dst->num, (void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);
}

// 當 block 從堆記憶體釋放時,調用此函數:__main_block_dispose_0
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);}
           

__main_block_impl_0 中增加了 __Block_byref_num_0類型的指針變量。是以__block的變量之是以可以修改 是因為 指針傳遞。是以block内部修改了值,外部也會改變:

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_num_0 *num; // 二、__block int num  變成了 __Block_byref_num_0指針變量。也就是 __block的變量通過指針傳遞給block
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_num_0 *_num, int flags=0) : num(_num->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
           

在block要執行的函數 __main_block_func_0中,我們通過__Block_byref_num_0的__forwarding指針來修改的 外部變量,即:(num->__forwarding->num) = 10;

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_num_0 *num = __cself->num; // bound by ref
    
    (num->__forwarding->num) = 10;  //三、這裡修改的是__forwarding 指向的記憶體的值
    printf("num = %d", (num->__forwarding->num));
}
           

這是為什麼呢?

我們先來看下文章開頭的第二個問題:

block底層源碼分析前言block的使用block調用外部變量block代碼分析3、__block在block中的作用。

當外部的局部變量testNum3 改變後,block内的testNum3變量也變了。 

在block中修改的testNum3值,在block外部testNum3也改變了。

我們看下剛才clang生成的main方法,上面有截圖:

block底層源碼分析前言block的使用block調用外部變量block代碼分析3、__block在block中的作用。

類似的邏輯:

用__block修改後,testNum3變量轉換為__Block_byref_num_0 的結構體。

上面說過copy操作會将block從棧拷貝到堆上, 會把 testNum3轉成的__Block_byref_num_0 結構體  指派給block的變量。

同時 會把 __Block_byref_num_0 的結構體中的 __forwarding指針指向拷貝到堆上 結構體。

就是棧上和拷貝到堆上的 的__Block_byref_num_0都用__forwarding指向堆上的自己。

這樣在棧上修改 testNum3變量的時候,實際修改的是堆上值,是以block内外的值是互相影響。

本文中的所有代碼還有clang生成的.cpp檔案,都放到了github上。

參考部落格位址