iOS底層原理總結 - 探尋block的本質(一)

面試題

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

首先對block有一個基本的認識c++

block本質上也是一個oc對象,他內部也有一個isa指針。block是封裝了函數調用以及函數調用環境的OC對象。面試

探尋block的本質

首先寫一個簡單的block數組

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        void(^block)(int ,int) = ^(int a, int b){
            NSLog(@"this is block,a = %d,b = %d",a,b);
            NSLog(@"this is block,age = %d",age);
        };
        block(3,5);
    }
    return 0;
}
複製代碼

使用命令行將代碼轉化爲c++查看其內部結構,與OC代碼進行比較sass

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

c++與oc代碼對比

上圖中將c++中block的聲明和定義分別與oc代碼中相對應顯示。將c++中block的聲明和調用分別取出來查看其內部實現。數據結構

定義block變量

// 定義block變量代碼
void(*block)(int ,int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
複製代碼

上述定義代碼中,能夠發現,block定義中調用了__main_block_impl_0函數,而且將__main_block_impl_0函數的地址賦值給了block。那麼咱們來看一下__main_block_impl_0函數內部結構。iphone

__main_block_imp_0結構體

__main_block_imp_0結構體

__main_block_imp_0結構體內有一個同名構造函數__main_block_imp_0,構造函數中對一些變量進行了賦值最終會返回一個結構體。函數

那麼也就是說最終將一個__main_block_imp_0結構體的地址賦值給了block變量post

__main_block_impl_0結構體內能夠發現__main_block_impl_0構造函數中傳入了四個參數。(void *)__main_block_func_0、&__main_block_desc_0_DATA、age、flags。其中flage有默認值,也就說flage參數在調用的時候能夠省略不傳。而最後的 age(_age)則表示傳入的_age參數會自動賦值給age成員,至關於age = _age。學習

接下來着重看一下前面三個參數分別表明什麼。

(void *)__main_block_func_0

__main_block_func_0

在__main_block_func_0函數中首先取出block中age的值,緊接着能夠看到兩個熟悉的NSLog,能夠發現這兩段代碼偏偏是咱們在block塊中寫下的代碼。 那麼__main_block_func_0函數中其實存儲着咱們block中寫下的代碼。而__main_block_impl_0函數中傳入的是(void *)__main_block_func_0,也就說將咱們寫在block塊中的代碼封裝成__main_block_func_0函數,並將__main_block_func_0函數的地址傳入了__main_block_impl_0的構造函數中保存在結構體內。

&__main_block_desc_0_DATA

&__main_block_desc_0_DATA

咱們能夠看到__main_block_desc_0中存儲着兩個參數,reserved和Block_size,而且reserved賦值爲0而Block_size則存儲着__main_block_impl_0的佔用空間大小。最終將__main_block_desc_0結構體的地址傳入__main_block_func_0中賦值給Desc。

age

age也就是咱們定義的局部變量。由於在block塊中使用到age局部變量,因此在block聲明的時候這裏纔會將age做爲參數傳入,也就說block會捕獲age,若是沒有在block中使用age,這裏將只會傳入(void *)__main_block_func_0,&__main_block_desc_0_DATA兩個參數。

這裏能夠根據源碼思考一下爲何當咱們在定義block以後修改局部變量age的值,在block調用的時候沒法生效。

int age = 10;
void(^block)(int ,int) = ^(int a, int b){
     NSLog(@"this is block,a = %d,b = %d",a,b);
     NSLog(@"this is block,age = %d",age);
};
     age = 20;
     block(3,5); 
     // log: this is block,a = 3,b = 5
     //      this is block,age = 10
複製代碼

由於block在定義的以後已經將age的值傳入存儲在__main_block_imp_0結構體中並在調用的時候將age從block中取出來使用,所以在block定義以後對局部變量進行改變是沒法被block捕獲的。

此時回過頭來查看__main_block_impl_0結構體

__main_block_impl_0結構體

首先咱們看一下__block_impl第一個變量就是__block_impl結構體。 來到__block_impl結構體內部

__block_impl結構體內部

咱們能夠發現__block_impl結構體內部就有一個isa指針。所以能夠證實block本質上就是一個oc對象。而在構造函數中將函數中傳入的值分別存儲在__main_block_impl_0結構體實例中,最終將結構體的地址賦值給block。

接着經過上面對__main_block_impl_0結構體構造函數三個參數的分析咱們能夠得出結論:

1. __block_impl結構體中isa指針存儲着&_NSConcreteStackBlock地址,能夠暫時理解爲其類對象地址,block就是_NSConcreteStackBlock類型的。

2. block代碼塊中的代碼被封裝成__main_block_func_0函數,FuncPtr則存儲着__main_block_func_0函數的地址。

3. Desc指向__main_block_desc_0結構體對象,其中存儲__main_block_impl_0結構體所佔用的內存。

調用block執行內部代碼

// 執行block內部的代碼
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);
複製代碼

經過上述代碼能夠發現調用block是經過block找到FunPtr直接調用,經過上面分析咱們知道block指向的是__main_block_impl_0類型結構體,可是咱們發現__main_block_impl_0結構體中並不直接就能夠找到FunPtr,而FunPtr是存儲在__block_impl中的,爲何block能夠直接調用__block_impl中的FunPtr呢?

從新查看上述源代碼能夠發現,(__block_impl *)block將block強制轉化爲__block_impl類型的,由於__block_impl是__main_block_impl_0結構體的第一個成員,至關於將__block_impl結構體的成員直接拿出來放在__main_block_impl_0中,那麼也就說明__block_impl的內存地址就是__main_block_impl_0結構體的內存地址開頭。因此能夠轉化成功。並找到FunPtr成員。

上面咱們知道,FunPtr中存儲着經過代碼塊封裝的函數地址,那麼調用此函數,也就是會執行代碼塊中的代碼。而且回頭查看__main_block_func_0函數,能夠發現第一個參數就是__main_block_impl_0類型的指針。也就是說將block傳入__main_block_func_0函數中,便於重中取出block捕獲的值。

如何驗證block的本質確實是__main_block_impl_0結構體類型。

經過代碼證實一下上述內容: 一樣使用以前的方法,咱們按照上面分析的block內部結構自定義結構體,並將block內部的結構體強制轉化爲自定義的結構體,轉化成功說明底層結構體確實如咱們以前分析的同樣。

struct __main_block_desc_0 { 
    size_t reserved;
    size_t Block_size;
};
struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};
// 模仿系統__main_block_impl_0結構體
struct __main_block_impl_0 { 
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int age;
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        void(^block)(int ,int) = ^(int a, int b){
            NSLog(@"this is block,a = %d,b = %d",a,b);
            NSLog(@"this is block,age = %d",age);
        };
// 將底層的結構體強制轉化爲咱們本身寫的結構體,經過咱們自定義的結構體探尋block底層結構體
        struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block;
        block(3,5);
    }
    return 0;
}
複製代碼

經過打斷點能夠看出咱們自定義的結構體能夠被賦值成功,以及裏面的值。

blockStruct

接下來斷點來到block代碼塊中,看一下堆棧信息中的函數調用地址。Debuf workflow -> always show Disassembly

Debuf workflow -> always show Disassembly

經過上圖能夠看到地址確實和FuncPtr中的代碼塊地址同樣。

總結

此時已經基本對block的底層結構有了基本的認識,上述代碼能夠經過一張圖展現其中各個結構體之間的關係。

圖示block結構體內部之間的關係

block底層的數據結構也能夠經過一張圖來展現

block底層的數據結構

block的變量捕獲

爲了保證block內部可以正常訪問外部的變量,block有一個變量捕獲機制。

局部變量

auto變量

上述代碼中咱們已經瞭解過block對age變量的捕獲。 auto自動變量,離開做用域就銷燬,一般局部變量前面自動添加auto關鍵字。自動變量會捕獲到block內部,也就是說block內部會專門新增長一個參數來存儲變量的值。 auto只存在於局部變量中,訪問方式爲值傳遞,經過上述對age參數的解釋咱們也能夠肯定確實是值傳遞。

static變量

static 修飾的變量爲指針傳遞,一樣會被block捕獲。

接下來分別添加aotu修飾的局部變量和static修飾的局部變量,重看源碼來看一下他們之間的差異。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        auto int a = 10;
        static int b = 11;
        void(^block)(void) = ^{
            NSLog(@"hello, a = %d, b = %d", a,b);
        };
        a = 1;
        b = 2;
        block();
    }
    return 0;
}
// log : block本質[57465:18555229] hello, a = 10, b = 2
// block中a的值沒有被改變而b的值隨外部變化而變化。
複製代碼

從新生成c++代碼看一下內部結構中兩個參數的區別。

局部變量c++代碼

從上述源碼中能夠看出,a,b兩個變量都有捕獲到block內部。可是a傳入的是值,而b傳入的則是地址。

爲何兩種變量會有這種差別呢,由於自動變量可能會銷燬,block在執行的時候有可能自動變量已經被銷燬了,那麼此時若是再去訪問被銷燬的地址確定會發生壞內存訪問,所以對於自動變量必定是值傳遞而不多是指針傳遞了。而靜態變量不會被銷燬,因此徹底能夠傳遞地址。而由於傳遞的是值得地址,因此在block調用以前修改地址中保存的值,block中的地址是不會變得。因此值會隨之改變。

全局變量

咱們一樣以代碼的方式看一下block是否捕獲全局變量

int a = 10;
static int b = 11;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(void) = ^{
            NSLog(@"hello, a = %d, b = %d", a,b);
        };
        a = 1;
        b = 2;
        block();
    }
    return 0;
}
// log hello, a = 1, b = 2
複製代碼

一樣生成c++代碼查看全局變量調用方式

全局變量c++代碼

經過上述代碼能夠發現,__main_block_imp_0並無添加任何變量,所以block不須要捕獲全局變量,由於全局變量不管在哪裏均可以訪問。

局部變量由於跨函數訪問因此須要捕獲,全局變量在哪裏均可以訪問 ,因此不用捕獲。

最後以一張圖作一個總結

block的變量捕獲

總結:局部變量都會被block捕獲,自動變量是值捕獲,靜態變量爲地址捕獲。全局變量則不會被block捕獲

疑問:如下代碼中block是否會捕獲變量呢?

#import "Person.h"
@implementation Person
- (void)test
{
    void(^block)(void) = ^{
        NSLog(@"%@",self);
    };
    block();
}
- (instancetype)initWithName:(NSString *)name
{
    if (self = [super init]) {
        self.name = name;
    }
    return self;
}
+ (void) test2
{
    NSLog(@"類方法test2");
}
@end
複製代碼

一樣轉化爲c++代碼查看其內部結構

c++代碼

上圖中能夠發現,self一樣被block捕獲,接着咱們找到test方法能夠發現,test方法默認傳遞了兩個參數self和_cmd。而類方法test2也一樣默認傳遞了類對象self和方法選擇器_cmd。

對象方法和類方法對比

不論對象方法仍是類方法都會默認將self做爲參數傳遞給方法內部,既然是做爲參數傳入,那麼self確定是局部變量。上面講到局部變量確定會被block捕獲。

接着咱們來看一下若是在block中使用成員變量或者調用實例的屬性會有什麼不一樣的結果。

- (void)test
{
    void(^block)(void) = ^{
        NSLog(@"%@",self.name);
        NSLog(@"%@",_name);
    };
    block();
}
複製代碼

c++代碼

上圖中能夠發現,即便block中使用的是實例對象的屬性,block中捕獲的仍然是實例對象,並經過實例對象經過不一樣的方式去獲取使用到的屬性。

block的類型

block對象是什麼類型的,以前稍微提到過,經過源碼能夠知道block中的isa指針指向的是_NSConcreteStackBlock類對象地址。那麼block是否就是_NSConcreteStackBlock類型的呢?

咱們經過代碼用class方法或者isa指針查看具體類型。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // __NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
        void (^block)(void) = ^{
            NSLog(@"Hello");
        };
        
        NSLog(@"%@", [block class]);
        NSLog(@"%@", [[block class] superclass]);
        NSLog(@"%@", [[[block class] superclass] superclass]);
        NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
    }
    return 0;
}
複製代碼

打印內容

block的類型

從上述打印內容能夠看出block最終都是繼承自NSBlock類型,而NSBlock繼承於NSObjcet。那麼block其中的isa指針實際上是來自NSObject中的。這也更加印證了block的本質其實就是OC對象。

block的3種類型

block有3中類型

__NSGlobalBlock__ ( _NSConcreteGlobalBlock )
__NSStackBlock__ ( _NSConcreteStackBlock )
__NSMallocBlock__ ( _NSConcreteMallocBlock )
複製代碼

經過代碼查看一下block在什麼狀況下其類型會各不相同

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1. 內部沒有調用外部變量的block
        void (^block1)(void) = ^{
            NSLog(@"Hello");
        };
        // 2. 內部調用外部變量的block
        int a = 10;
        void (^block2)(void) = ^{
            NSLog(@"Hello - %d",a);
        };
       // 3. 直接調用的block的class
        NSLog(@"%@ %@ %@", [block1 class], [block2 class], [^{
            NSLog(@"%d",a);
        } class]);
    }
    return 0;
}
複製代碼

經過打印內容確實能夠發現block的三種類型

block的三種類型

可是咱們上面提到過,上述代碼轉化爲c++代碼查看源碼時卻發現block的類型與打印出來的類型不同,c++源碼中三個block的isa指針所有都指向_NSConcreteStackBlock類型地址。

咱們能夠猜想runtime運行時過程當中也許對類型進行了轉變。最終類型固然以runtime運行時類型也就是咱們打印出的類型爲準。

block在內存中的存儲

經過下面一張圖看一下不一樣block的存放區域

不一樣類型block的存放區域

上圖中能夠發現,根據block的類型不一樣,block存放在不一樣的區域中。 數據段中的__NSGlobalBlock__直到程序結束纔會被回收,不過咱們不多使用到__NSGlobalBlock__類型的block,由於這樣使用block並無什麼意義。

__NSStackBlock__類型的block存放在棧中,咱們知道棧中的內存由系統自動分配和釋放,做用域執行完畢以後就會被當即釋放,而在相同的做用域中定義block而且調用block彷佛也畫蛇添足。

__NSMallocBlock__是在平時編碼過程當中最常使用到的。存放在堆中須要咱們本身進行內存管理。

block是如何定義其類型

block是如何定義其類型,依據什麼來爲block定義不一樣的類型並分配在不一樣的空間呢?首先看下面一張圖

block是如何定義其類型

接着咱們使用代碼驗證上述問題,首先關閉ARC回到MRC環境下,由於ARC會幫助咱們作不少事情,可能會影響咱們的觀察。

// MRC環境!!!
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // Global:沒有訪問auto變量:__NSGlobalBlock__
        void (^block1)(void) = ^{
            NSLog(@"block1---------");
        };   
        // Stack:訪問了auto變量: __NSStackBlock__
        int a = 10;
        void (^block2)(void) = ^{
            NSLog(@"block2---------%d", a);
        };
        NSLog(@"%@ %@", [block1 class], [block2 class]);
        // __NSStackBlock__調用copy : __NSMallocBlock__
        NSLog(@"%@", [[block2 copy] class]);
    }
    return 0;
}
複製代碼

查看打印內容

block類型

經過打印的內容能夠發現正如上圖中所示。 沒有訪問auto變量的block是__NSGlobalBlock__類型的,存放在數據段中。 訪問了auto變量的block是__NSStackBlock__類型的,存放在棧中。 __NSStackBlock__類型的block調用copy成爲__NSMallocBlock__類型並被複制存放在堆中。

上面提到過__NSGlobalBlock__類型的咱們不多使用到,由於若是不須要訪問外界的變量,直接經過函數實現就能夠了,不須要使用block。

可是__NSStackBlock__訪問了aotu變量,而且是存放在棧中的,上面提到過,棧中的代碼在做用域結束以後內存就會被銷燬,那麼咱們頗有可能block內存銷燬以後纔去調用他,那樣就會發生問題,經過下面代碼能夠證明這個問題。

void (^block)(void);
void test()
{
    // __NSStackBlock__
    int a = 10;
    block = ^{
        NSLog(@"block---------%d", a);
    };
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();
    }
    return 0;
}
複製代碼

此時查看打印內容

打印內容

能夠發現a的值變爲了避免可控的一個數字。爲何會發生這種狀況呢?由於上述代碼中建立的block是__NSStackBlock__類型的,所以block是存儲在棧中的,那麼當test函數執行完畢以後,棧內存中block所佔用的內存已經被系統回收,所以就有可能出現亂得數據。查看其c++代碼能夠更清楚的理解。

c++代碼

爲了不這種狀況發生,能夠經過copy將__NSStackBlock__類型的block轉化爲__NSMallocBlock__類型的block,將block存儲在堆中,如下是修改後的代碼。

void (^block)(void);
void test()
{
    // __NSStackBlock__ 調用copy 轉化爲__NSMallocBlock__
    int age = 10;
    block = [^{
        NSLog(@"block---------%d", age);
    } copy];
    [block release];
}
複製代碼

此時在打印就會發現數據正確

打印內容

那麼其餘類型的block調用copy會改變block類型嗎?下面表格已經展現的很清晰了。

不一樣類型調用copy效果

因此在平時開發過程當中MRC環境下常常須要使用copy來保存block,將棧上的block拷貝到堆中,即便棧上的block被銷燬,堆上的block也不會被銷燬,須要咱們本身調用release操做來銷燬。而在ARC環境下系統會自動調用copy操做,使block不會被銷燬。

ARC幫咱們作了什麼

在ARC環境下,編譯器會根據狀況自動將棧上的block進行一次copy操做,將block複製到堆上。

什麼狀況下ARC會自動將block進行一次copy操做? 如下代碼都在RAC環境下執行。

1. block做爲函數返回值時

typedef void (^Block)(void);
Block myblock()
{
    int a = 10;
    // 上文提到過,block中訪問了auto變量,此時block類型應爲__NSStackBlock__
    Block block = ^{
        NSLog(@"---------%d", a);
    };
    return block;
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block = myblock();
        block();
       // 打印block類型爲 __NSMallocBlock__
        NSLog(@"%@",[block class]);
    }
    return 0;
}
複製代碼

看一下打印的內容

打印內容

上文提到過,若是在block中訪問了auto變量時,block的類型爲__NSStackBlock__,上面打印內容發現blcok爲__NSMallocBlock__類型的,而且能夠正常打印出a的值,說明block內存並無被銷燬。

上面提到過,block進行copy操做會轉化爲__NSMallocBlock__類型,來說block複製到堆中,那麼說明RAC在 block做爲函數返回值時會自動幫助咱們對block進行copy操做,以保存block,並在適當的地方進行release操做。

2. 將block賦值給__strong指針時

block被強指針引用時,RAC也會自動對block進行一次copy操做。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // block內沒有訪問auto變量
        Block block = ^{
            NSLog(@"block---------");
        };
        NSLog(@"%@",[block class]);
        int a = 10;
        // block內訪問了auto變量,但沒有賦值給__strong指針
        NSLog(@"%@",[^{
            NSLog(@"block1---------%d", a);
        } class]);
        // block賦值給__strong指針
        Block block2 = ^{
          NSLog(@"block2---------%d", a);
        };
        NSLog(@"%@",[block1 class]);
    }
    return 0;
}
複製代碼

查看打印內容能夠看出,當block被賦值給__strong指針時,RAC會自動進行一次copy操做。

打印內容

3. block做爲Cocoa API中方法名含有usingBlock的方法參數時

例如:遍歷數組的block方法,將block做爲參數的時候。

NSArray *array = @[];
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            
}];
複製代碼

4. block做爲GCD API的方法參數時

例如:GDC的一次性函數或延遲執行的函數,執行完block操做以後系統纔會對block進行release操做。

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
            
});        
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
});
複製代碼

block聲明寫法

經過上面對MRC及ARC環境下block的不一樣類型的分析,總結出不一樣環境下block屬性建議寫法。

MRC下block屬性的建議寫法

@property (copy, nonatomic) void (^block)(void);

ARC下block屬性的建議寫法

@property (strong, nonatomic) void (^block)(void); @property (copy, nonatomic) void (^block)(void);

底層原理文章專欄

底層原理文章專欄


文中若是有不對的地方歡迎指出。我是xx_cc,一隻長大好久但尚未二夠的傢伙。須要視頻一塊兒探討學習的coder能夠加我Q:2336684744

相關文章
相關標籤/搜索