歡迎光臨
每天分享高質量文章

iOS原始碼解析:Block的本質<一>

作者:雪山飛狐_91ae

連結:https://www.jianshu.com/p/1f271940e5cc

 

Block在iOS開發中的用途非常廣,今天我們就來一起探索一下Block的底層結構。

1、Block的底層結構

下麵是一個沒有引數和傳回值的簡單的Block:

 

int main(int argc, char * argv[]) {
    @autoreleasepool {

        void (^block)(void) = ^{

            NSLog(@"Hello World!");
        };

        block();

        return 0;
    }
}

 

為了探索Block的底層結構,我們將main.m檔案轉化為C++的原始碼、我們開啟命令列。cd到包含main.m檔案的檔案夾,然後輸入:clang -rewrite-objc main.m,這個時候在該檔案夾的目錄下會生成main.cpp檔案。


這個檔案非常長,我們直接拉到檔案的最下麵,找到main函式:

 

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
         //定義block
        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
         //呼叫block
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

        return 0;
    }
}

 

這第一行程式碼是定義一個block變數,第二行程式碼是呼叫block。這兩行程式碼看起來非常複雜。但是我們可以去簡化一下,怎麼簡化呢?

 

變數前面的()一般是做強制型別轉換的,比如在呼叫block這一行,block前面有一個()是(__block_impl *),這就是進行了一個強制型別轉換,將其轉換為一個_block_impl型別的結構體指標,那像這樣的強制型別轉換非常妨礙我們理解程式碼,我們可以暫時將這些強制型別轉換去掉,這樣可以幫助我們理解程式碼。

 

化簡後的程式碼如下:

 

//定義block
void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
//呼叫block
block->FuncPtr(block);

 

這樣化簡後的程式碼就要清爽多了。我們一句一句的看,先看第一句:

 

void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

 

這句程式碼的意思好像就是呼叫_main_block_impl_0這個函式,給這個函式傳進兩個引數_main_block_func_0&_main_block_desc_0_DATA,然後得到這個函式的傳回值,取函式傳回值的地址,賦值給block這個指標。

我們在稍微上一點的位置可以找到_main_block_impl_0這個結構:

 

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0Desc;
//建構式,類似於OC的init方法
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

 

__block_impl這個結構體的結構我們可以command+f在main.cpp檔案中搜索得到:

 

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

 

_main_block_desc_0結構體的結構在main.cpp檔案的最下麵可以找到:

 

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0sizeof(struct __main_block_impl_0)};

 

這是一個C++的結構體。而且在這個結構體內還包含一個函式,這個函式的函式名和結構體名稱一致,這在C語言中是沒有的,這是C++特有的。

 

在C++的結構體包含的函式稱為結構體的建構式,它就相當於是OC中的init方法,用來初始化結構體。OC中的init方法傳回的是物件本身,C++的結構體中的構造方法傳回的也是結構體物件。

 

那麼我們就知道了,__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);傳回的就是_main_block_impl_0這個結構體物件,然後取結構體物件的地址賦值給block指標。換句話說,block指向的就是初始化後的_main_block_impl_0結構體物件。
我們再看一下初始化
_main_block_impl_0結構體傳進去的引數:

 

  • 第一個引數是_main_block_func_0,這個引數的結構在上面一點的位置也能找到:

 

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {


  NSLog((NSString *)&__NSConstantStringImpl__var_folders_74_wk04zv690mz36wn0g18r5nxm0000gn_T_main_3b803f_mi_0);
 }

 

這個函式其實就是把我們Block中要執行的程式碼封裝到這個函式內部了。我們可以看到這個函式內部就一行程式碼,就是一個NSlog函式,這也就是NSLog(@"Hello World!");這句程式碼。

把這個函式指標傳給_main_block_impl_0的建構式的第一個引數,然後用這個函式指標去初始化_main_block_impl_0這個結構體的第一個成員變數impl的成員變數FuncPtr。也就是說FuncPtr這個指標指向_main_block_func_0這個函式。

 

  • 第二個引數是&_main_block_desc_0_DATA
    我們看一下這個結構:

 

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0sizeof(struct __main_block_impl_0)};

 

在結構體的建構式中,0賦值給了sizeof(struct __main_block_impl_0)是賦值給了Block_size,可以看出這個結構體存放的是_main_block_impl_0這個結構體的資訊。在_main_block_impl_0的建構式中我們可以看到,_main_block_desc_0這個結構體的地址被賦值給了_main_block_impl_0的第二個成員變數Desc這個結構體指標。也就是說Desc這個結構體指標指向_main_block_desc_0_DATA這個結構體。
那麼我們總結一下:

 

所以第一句定義block

 

void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

 

總結起來就是:

  • 1.建立一個函式_main_block_func_0,這個函式
    的作用就是將我們block中要執行的程式碼封裝到函式內部,方便呼叫。

  • 2.建立一個結構體_main_block_desc_0,這個結構體中主要包含_main_block_impl_0這個結構體佔用的儲存空間大小等資訊。

  • 3.將1中建立的_main_block_func_0這個函式的地址,和2中建立的_main_block_desc_0這個結構體的地址傳給_main_block_impl_0的建構式。

  • 4.利用_main_block_func_0初始化_main_block_impl_0結構體的第一個成員變數impl的成員變數FuncPtr。這樣_main_bck_impl_0這個結構體也就得到了block中那個程式碼塊的地址。

  • 5.利用_mian_block_desc_0_DATA去初始化_mian_block_impl_0的第二個成員變數Desc
    下麵我們再看第二步呼叫block:

 

block->FuncPtr(block);

 

我們知道,block實質上就是指向_main_block_impl_0這個結構體的指標,而FuncPtr是_main_block_impl_0的第第一個成員變數impl的成員變數,正常來講,block想要呼叫自己的成員變數的成員變數的成員變數,應該像下麵這樣呼叫:

 

block->impl->FuncPtr

 

然而事實卻不是這樣,這是為什麼呢?

 

原因就在於之前我們把所有的強制型別轉換給刪掉了,之前block前面的()是(__block_impl *),為什麼可以這樣強制轉換呢?因為block指向的是_main_block_impl_0這個結構體的首地址,而_main_block_impl_0
的第一個成員變數是struct __block_impl impl;,所以impl和_main_block_impl_0的首地址是一樣的,因此指向_main_block_impl_0的首地址的指標也就可以被強制轉換為指向impl的首地址的指標。

 

之前說過,FuncPtr這個指標在建構式中是被初始化為指向_mian_block_func_0這個函式的地址。因此透過block->FuncPtr呼叫也就獲取了_main_block_func_0這個函式的地址,然後對_main_block_func_0進行呼叫,也就是執行block中的程式碼了。這中間block又被當做引數傳進了_main_block_func_0這個函式。

2、變數捕獲-auto變數

auto變數是宣告在函式內部的變數,比如int a = 0;;這句程式碼宣告在函式內部,那a就是auto變數,等價於auto int a = 0;auto變數時分配在棧區,當超出作用域時,其佔用的記憶體會被系統自動銷毀並生成。下麵看一段程式碼:

 

        int a = 10;

        void (^block)(void) = ^{

            NSLog(@"%d", a);
        };

        a = 20;

        block();

 

這是一個很簡單的Block捕獲自動變數的例子,我們看一下列印結果:

 

2018-09-04 20:39:45.436534+0800 copytest[17163:477148] 10

 

自動變數a的值明明已經變為了20,為什麼輸出結果還是10呢?我們把這段程式碼轉化為C++的原始碼看看。

 

int main(int argc, char * argv[]) {    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;         int a = 10;        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));        a = 20;        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);        return 0;    }}

我們還是把程式碼化簡一下來看:

 

        int a = 10;

        void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a);

        a = 20;

        block->FuncPtr)(block);

 

對比一下上面分析的沒有捕獲自動變數的原始碼,我們發現這裡_main_block_impl_0中傳入的引數多了一個a。然後我們往上翻看看_main_block_impl_0的結構:

 

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0Desc;
  int a; //這是新加入的成員變數
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

 

在_main_block_impl_0這個結構體中我們發現多了一個int型別的成員變數a,在結構體的建構式中多了一個引數int _a,並且用這個int _a去初始化成員變數a。


所以在void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a);中傳入了自動變數a用來初始化_main_block_impl_0的成員變數a。那這個時候_main_block_impl_0的成員變數a就被賦值為10了。


由於上面這一步是值傳遞,所以當執行
a = 20時,_main_block_impl_0結構體的成員變數a的值是不會隨之改變的,仍然是10。


然後我們再來看一下_main_block_func_0的結構有何變化:

 

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_74_wk04zv690mz36wn0g18r5nxm0000gn_T_main_fb5f0d_mi_0, a);
        }

 

可以看到,透過傳入的_main_block_impl_0這個結構體獲得其成員變數a的值。

3、變數捕獲-static變數

上面講的捕獲的是自動變數,在函式內部宣告的變數預設為自動變數,即預設用auto修飾。那麼如果在函式內部宣告的變數用static修飾,又會帶來哪些不同呢?static變數和auto變數的不同之處在於變數的記憶體的回收時機。auto變數在其作用域結束時就會被系統自動回收,而static變數在變數的作用域結束時並不會被系統自動回收。


先看一段程式碼:

 

       static int a = 10;

        void (^block)(void) = ^{

            NSLog(@"%d", a);
        };

        a = 20;

        block();

 

我們看一下列印結果:

 

2018-09-04 21:09:40.440020+0800 copytest[17949:499740] 20

 

結果是20,這個和2中的列印結果不一樣,為什麼區域性變數從auto變成了static結果會不一樣呢?我們還是從原始碼來分析:

 

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

       static int a = 10;

        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a;));

        a = 20;

        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

        return 0;
    }
}

 

我們把程式碼化簡一下:

 

       static int a = 10;

        void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &a;);

        a = 20;

        block->FuncPtr(block);

 

和2不一樣的是,這裡傳入_main_block_impl_0的是&a;,也即是a這個變數的地址值。那麼這個&a;是賦值給誰了呢?我們上翻找到_main_block_impl_0的結構:

 

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0Desc;
  int *a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

 

這裡我們可以看到結構體多了一個指標型別的成員變數int *a,然後在建構式中,將傳遞過來的&a;,賦值給這個指標變數。也就是說,在_main_block_impl_0這個結構體中多了一個成員變數,這個成員變數是指標,指向a這個變數。所以當a變數的值發生變化時,能夠得到最新的值。

4、變數捕獲-全域性變數

2和3分析了兩種型別的區域性變數,auto區域性變數和static區域性變數。這一部分則分析全域性變數。全域性變數會不會像區域性變數一樣被block所捕獲呢?我們還是看一下實體:

 

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

列印結果:

 

2018-09-04 21:41:19.016278+0800 copytest[18774:524773] 30 40

 

我們還是檢視一下原始碼:

 

int height = 10;
static int weight = 20;
int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

        height = 30;
        weight = 40;

        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

        return 0;
    }
}

 

這裡我們可以看到,height和weight這兩個全域性變數沒有作為引數傳入_main_block_impl_0中去。然後我們再檢視一下_main_block_impl_0的結構:

 

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

 

可以看到,_main_block_impl_0中並沒有增加成員變數。然後我們再看_main_block_func_0的結構:

 

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {


            NSLog((NSString *)&__NSConstantStringImpl__var_folders_74_wk04zv690mz36wn0g18r5nxm0000gn_T_main_46c51b_mi_0, height, weight);
        }

 

可以看到,這個地方在呼叫的時候是直接呼叫的全域性變數height和weight。
所以我們可以得出結論,block並不會不會全域性變數。


總結:

 

變數型別 是否捕獲到block內部 訪問方式
區域性變數auto 值傳遞
區域性變數static 指標傳遞
全域性變數 直接訪問
思考 為什麼對於不同型別的變數,block的處理方式不同呢?

 

這是由變數的生命週期決定的。對於自動變數,當作用域結束時,會被系統自動回收,而block很可能是在超出自動變數作用域的時候去執行,如果之前沒有捕獲自動變數,那麼後面執行的時候,自動變數已經被回收了,得不到正確的值。對於static區域性變數,它的生命週期不會因為作用域結束而結束,所以block只需要捕獲這個變數的地址,在執行的時候透過這個地址去獲取變數的值,這樣可以獲得變數的最新的值。gao’mi而對於全域性變數,在任何位置都可以直接讀取變數的值。思考 為什麼對於不同型別的變數,block的處理方式不同呢?

5、變數捕獲-self變數

看下麵一段程式碼:

 

@implementation Person

- (void)test{

    void(^block)(void) = ^{

        NSLog(@"%@"self);
    };
}

@end

 

這個Person類中只有一個東西,就是test這個函式,那麼這個block有沒有捕獲self變數呢?
要搞清這個問題,我們只需要知道搞清楚這裡self變數是區域性變數還是全域性變數,如果是區域性變數,那麼是一定會捕獲的,而如果是全域性變數,則一定不會被捕獲。


我們把這個Person.m檔案轉化為c++的原始碼,然後找到test函式在c++中的表示:

 

static void _I_Person_test(Person * self, SEL _cmd) {

    void(*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self570425344));
}

 

我們可以看到,本來Person.m中,這個test函式我是沒有傳任何引數的,但是轉化為c++的程式碼後,這裡傳入了兩個引數,一個是self引數,一個是_cmd。self很常見,_cmd表示test函式本身。所以我們就很清楚了,self是作為引數傳進來,也就是區域性變數,那麼block應該是捕獲了self變數,事實是不是這樣呢?我們只需要檢視一下_Person_test_block_impl_0的結構就可以知道了。
_Person_test_block_impl_0的結構:

 

struct __Person__test_block_impl_0 {
  struct __block_impl impl;
  struct __Person__test_block_desc_0* Desc;
  Person *self;
  __Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_selfint flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

 

可以看到,self確實是作為成員變數被捕獲了。

6、Block的型別

前面已經說過了,Block的本質就是一個OC物件,既然它是OC物件,那麼它就有型別。


在搞清楚Block的型別之前,先把ARC關掉,因為ARC幫我們做了太多的事,不方便我們觀察結果。關掉ARC的方法在Build Settings裡面搜尋Objective-C Automatic Reference Counting,把這一項置為NO。

 

int height = 10;
static int weight = 20;

int main(int argc, char * argv[]) {
    @autoreleasepool {

        int age = 10;

        void (^block)(void) = ^{

            NSLog(@"%d %d", height, age);
        };

        NSLog(@"%@
 %@
 %@
 %@", [block class], [[block class] superclass], [[[block class] superclass] superclass], [[[[block class] superclass] superclass] superclass]);

        return 0;
    }
}

 

上面的程式碼的列印結果是:

 

 __NSStackBlock__
 __NSStackBlock
 NSBlock
 NSObject

 

這說明上面定義的這個block的型別是NSStackBlock,並且它最終繼承自NSObject也說明Block的本質是OC物件。


Block有三種型別,分別是NSGlobalBlock,MallocBlock,NSStackBlock。


這三種型別的Block物件的儲存區域如下:

 

物件的儲存域
NSStackBlock
NSGlobalBlock 程式的資料區域(.data區)
NSMallocBlock

 

截獲了自動變數的Block是NSStackBlock型別,沒有截獲自動變數的Block則是NSGlobalStack型別,NSStackBlock型別的Block進行copy操作之後其型別變成了NSMallocBlock型別。

 

Block的型別 副本的配置儲存域 複製效果
NSStackBlock 從棧複製到堆
NSGlobalStack 程式的資料區域 什麼也不做
NSMallocBlock 取用計數增加

 

下麵我們一起分析一下NSStackBlock型別的Block進行copy操作後Block物件從棧複製到了堆有什麼道理,我們首先來看一段程式碼:

 

void (^block)(void);

void test() {

    int age = 10;

    block = ^{

        NSLog(@"age=%d", age);
    };
}

int main(int argc, char * argv[]) {
    @autoreleasepool {

        test();

        block();

        return 0;
    }
}

 

不出意外的話,列印結果應該是10,那麼結果是不是這樣呢?我們列印看一下:

 

age=-411258824

 

很奇怪,列印了一個這麼奇怪的數字。這是為什麼呢?


block使用了自動變數age,所以它是NSStackBlock型別的,因此block是存放在棧區,age是被捕獲作為結構體的成員變數,其值也是被儲存在棧區。所以當test這個函式呼叫完畢後,它棧上的東西就有可能被銷毀了,一旦銷毀了,age值就不確定是多少了。透過列印結果也可以看到,確實是影響到了block的執行。


如果我們對block執行copy操作,結果會不會不一樣呢?

 

void (^block)(void);

void test() {

    int age = 10;

    block = [^{

        NSLog(@"age=%d", age);
    } copy];
}

int main(int argc, char * argv[]) {
    @autoreleasepool {

        test();

        block();

        return 0;
    }
}

 

列印結果:

 

age=10

 

這個時候得出了正確的輸出。


因為對block進行copy操作後,block從棧區被覆制到了堆區,它的成員變數age也隨之被覆制到了堆區,這樣test函式執行完之後,它的棧區被銷毀並不影響block,因此能得出正確的輸出。

7、ARC環境下自動為Block進行copy操作的情況

6中講的最後一個例子:

 

void (^block)(void);

void test() {

    int age = 10;

    block = ^{

        NSLog(@"age=%d", age);
    };
}

int main(int argc, char * argv[]) {
    @autoreleasepool {

        test();

        block();

        return 0;
    }
}

 

這種使用方式其實非常常見,我們在使用的時候也沒有發現有什麼問題,那為什麼在MRC環境下就有問題呢?因為在ARC環境下編譯器為我們做了很多copy操作。其中有一個規則就是如果Block被強指標指著,那麼編譯器就會對其進行copy操作。我們看到這裡:

 

^{

        NSLog(@"age=%d", age);
    };

 

這個Block塊是被強指標指著,所以它會進行copy操作,由於其使用了自動變數,所以是棧區的Block。經過複製以後就到了堆區,這樣由於Block在堆區,所以就不受Block執行完成的影響,隨時可以獲取age的正確值。

 

總結一下ARC環境下自動進行copy操作的情況一共有以下幾種:

 

  • block作為函式傳回值時。

  • 將block賦值給__strong指標時。

  • block作為Cocoa API中方法名含有usingBlock的方法引數時。

  • GCD中的API。

 
block作為函式傳回值時

 

typedef void(^PDBlock)(void);

PDBlock test() {

    int age = 10;

    return ^{

        NSLog(@"age=%d", age);
    };


}

int main(int argc, char * argv[]) {
    @autoreleasepool {

        PDBlock block = test();
        block();

        return 0;
    }
}

 

test函式的傳回值是一個block,那這種情況的時候,在棧區的

 

^{

        NSLog(@"age=%d", age);
    };

 

這個block會被覆制到堆區

 

將block賦值給強指標時

 

7中第一個例子就是將block賦值給強指標時,進行了copy操作的情況。

 

block作為Cocoa API中方法名含有usingBlock的方法引數時

 

比如說遍歷陣列的函式:

 

NSArray *array = [[NSArray alloc] init];
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            NSLog(@"%d", idx);
        }];

 

enumerateObjectsUsingBlock:這個函式中的block會進行copy操作

 

GCD中的API

 

GCD中的很多API的引數都有block,這個時候都會對block進行一次copy操作,比如下麵這個dispatch_after函式:

 

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

            NSLog(@"wait");
        });

贊(0)

分享創造快樂