iOS底層原理 - 探尋block本質(zhì)

面試題引發(fā)的思考:

Q: block的原理是怎樣的?本質(zhì)是什么?

  • block本質(zhì)上也是一個OC對象,它內(nèi)部也有一個isa指針。
  • block是封裝了 函數(shù)調(diào)用 以及 函數(shù)調(diào)用環(huán)境 的OC對象。

1. 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);
        };
        age = 20;
        block(1, 2);
    }
    return 0;
}

使用命令行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m將代碼轉(zhuǎn)化成C++語言:

OC代碼轉(zhuǎn)化為C++

將C++中強制轉(zhuǎn)換代碼去掉,以便閱讀:

簡化后代碼

(1) 定義block

代碼顯示:

  • 定義block時調(diào)用了__main_block_impl_0函數(shù),并將__main_block_impl_0函數(shù)地址賦值給了block。
    即:block底層就是__main_block_impl_0結(jié)構(gòu)體

通過__main_block_impl_0找到__main_block_impl_0函數(shù):

block底層結(jié)構(gòu) - __main_block_impl_0函數(shù)

代碼顯示:

  • __main_block_imp_0結(jié)構(gòu)體的第一個成員是__block_impl結(jié)構(gòu)體變量,__block_impl結(jié)構(gòu)體的第一個成員是isa;
    即:__main_block_imp_0結(jié)構(gòu)體第一個成員是isa,也就是說block的底層就是一個OC對象。

  • __main_block_imp_0結(jié)構(gòu)體內(nèi)部有一個同名構(gòu)造函數(shù)__main_block_imp_0,會對相關(guān)變量賦值并返回一個__main_block_imp_0結(jié)構(gòu)體,然后將結(jié)構(gòu)體的地址賦值給block。

定義block時__main_block_impl_0函數(shù)傳入的3個參數(shù):
1> _main_block_func_0參數(shù):
_main_block_func_0

很明顯,其中的NSLog(...);即我們寫的NSLog(@"a = %d, b = %d, age = %d", a, b, age);語句;
即:_main_block_func_0函數(shù)把Block中要執(zhí)行的代碼封裝到其內(nèi)部。

2> &_main_block_desc_0_DATA參數(shù):
&_main_block_desc_0_DATA

__main_block_desc_0中包含兩個參數(shù):
a> reserved:賦值為0
b> Block_size:存儲__main_block_impl_0占用的空間大小。

3> age參數(shù):

age是我們定義的局部變量。block中使用age,所以block會在聲明的時候?qū)?code>age作為參數(shù)傳入,即block會捕獲age。

因為block在定義時將age值傳入存儲在__main_block_impl_0結(jié)構(gòu)體中,并在調(diào)動block時將age取出來使用,所以在block定義結(jié)束后對局部變量進行修改是無法被block捕獲的。所以文章開始的代碼中輸出的age值為10,而非20

下面再看__main_block_impl_0函數(shù):

block底層結(jié)構(gòu) - __main_block_impl_0函數(shù)

由以上分析可知:

  • __block_impl結(jié)構(gòu)體中isa指針存儲著_NSConcreteStackBlock地址;
  • _main_block_func_0函數(shù)把Block中要執(zhí)行的代碼封裝到其內(nèi)部,FuncPtr則存儲著__main_block_func_0函數(shù)的地址;
  • Desc指向__main_block_desc_0結(jié)構(gòu)體對象,其中存儲__main_block_impl_0結(jié)構(gòu)體所占用的內(nèi)存。

(2) 調(diào)用block

// 調(diào)用block內(nèi)部的代碼 
// 簡化版:block->FuncPtr(block, 1, 2);
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);

通過__main_block_impl_0函數(shù)結(jié)構(gòu)可知:

FuncPtr_main_block_impl_0第一個成員變量impl的成員變量,但是block調(diào)用時卻直接通過block找到FuncPtr進行調(diào)用,這是為什么呢?

原因在于:(__block_impl *)block將block強制轉(zhuǎn)化為__block_impl類型,而impl__main_block_impl_0結(jié)構(gòu)體的第一個成員,所以impl__main_block_impl_0的首地址是一樣的,因此指向_main_block_impl_0的首地址的指針也就可以被強制轉(zhuǎn)換為指向impl的首地址的指針,并找到FuncPtr。

FuncPtr則存儲著__main_block_func_0函數(shù)的地址,因此通過block->FuncPtr()獲取__main_block_func_0的地址,對其進行調(diào)用,進而執(zhí)行block中的代碼。并且_main_block_func_0函數(shù)第一個參數(shù)就是__main_block_impl_0類型的指針,也就是說將block傳入__main_block_func_0函數(shù)中,進而取出block捕獲的值。


(3) 總結(jié)

通過以上分析,我們對block底層結(jié)構(gòu)有了基本的了解,由此可以分析出其中的結(jié)構(gòu)關(guān)系:

block結(jié)構(gòu)體內(nèi)部之間的關(guān)系

2. block的變量捕獲

為了保證block內(nèi)部能夠正常訪問外部的變量,block有個變量捕獲機制(capture)。

// 全局變量c, d
int c = 30;
static int d = 40;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 局部變量a, b
        // auto變量是聲明在函數(shù)內(nèi)部的變量,離開作用域就銷毀
        // int a = 10; 則a為auto變量,會自動在前面添加auto關(guān)鍵字
        auto int a = 10;
        static int b = 20;
        void (^block)(void) = ^{
            NSLog(@"a = %d, b = %d, c = %d, d = %d", a, b, c, d);
        };
        a = 1;
        b = 2;
        c = 3;
        d = 4;
        block();
    }
    return 0;
}

// 打印結(jié)果
Demo[1234:567890] a = 10, b = 2, c = 3, d = 4

OC代碼轉(zhuǎn)化為C++查看變量調(diào)用方式:

OC代碼轉(zhuǎn)化為C++

(1) 局部變量

1> auto變量:
  • auto變量是聲明在函數(shù)內(nèi)部的變量,離開作用域就銷毀,局部變量前面自動添加auto關(guān)鍵字。
  • auto變量會捕獲到block內(nèi)部,即block內(nèi)部會專門新增加一個參數(shù)來存儲變量的值。
  • auto變量只存在于局部變量中,訪問方式為值傳遞。
2> static變量:
  • static變量在變量的作用域結(jié)束時并不會被系統(tǒng)自動回收
  • static變量會捕獲到block內(nèi)部,即block內(nèi)部會專門新增加一個參數(shù)來存儲變量的值的地址。
  • static變量只存在于局部變量中,訪問方式為地址傳遞。

(2) 全局變量

全局變量在哪里都可以訪問,所以block不用捕獲全局變量,直接進行訪問。

(3) 總結(jié)

block的變量捕獲
  • block處理方式不同是由變量的聲明周期決定的;
  • 局部變量都會被block捕獲,auto變量值傳遞,static變量指針傳遞;
  • 全局變量不會被block捕獲,直接訪問。

Q: 那么以下情況block是否會捕獲變量呢?

#import "Person.h"

@implementation Person
- (void)test {
    void (^block)(void) = ^{
        NSLog(@"-------- %p", self);
    };
    block();
}
@end

OC代碼轉(zhuǎn)化為C++查看變量調(diào)用方式:

OC代碼轉(zhuǎn)化為C++

由圖可知:self會被block捕獲

因為OC方法會默認傳遞兩個參數(shù)self_cmd,兩者都是局部變量,與我們的結(jié)論:局部變量會被block捕獲符合。


3. block類型

前文可知:block的本質(zhì)就是一個OC對象,所以block有類型。

下面我們探尋一下block的類型,首先關(guān)閉ARC:

關(guān)閉ARC
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;
}

// 打印結(jié)果
Demo[1234:567890] __NSGlobalBlock__
Demo[1234:567890] __NSGlobalBlock
Demo[1234:567890] NSBlock
Demo[1234:567890] NSObject

由打印結(jié)果可知:
block最終繼承自NSBlock,而NSBlock繼承自NSObject,所以block的isa指針是來自于NSObject。也印證了block的本質(zhì)就是OC對象。


(1) block的類型及存放區(qū)域:

block的存放區(qū)域
  • block有三種類型:__NSGlobalBlock__、__NSMallocBlock____NSStackBlock__
  • 數(shù)據(jù)段中的__NSGlobalBlock__直到程序結(jié)束才會被回收;
  • 堆中的__NSMallocBlock__需要手動內(nèi)存管理;
  • 棧中的__NSStackBlock__作用域執(zhí)行完畢被回收。

(2) block類型總結(jié):

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1. __NSGlobalBlock__:沒有訪問auto變量
        void (^block1)(void) = ^{
            NSLog(@"block1");
        };
        NSLog(@"%@", [block1 class]);
        // 2. __NSStackBlock__:訪問了auto變量
        int age = 10;
        void (^block2)(void) = ^{
            NSLog(@"block2 - %d", age);
        };
        NSLog(@"%@", [block2 class]);
        // 3. __NSMallocBlock__:__NSStackBlock__調(diào)用了copy
        NSLog(@"%@", [[block2 copy] class]);
    }
    return 0;
}

// 打印結(jié)果
Demo[1234:567890] __NSGlobalBlock__
Demo[1234:567890] __NSStackBlock__
Demo[1234:567890] __NSMallocBlock__

輸出打印結(jié)果可知block的三種類型:

上述代碼轉(zhuǎn)化為C++去查看源碼發(fā)現(xiàn)三個block的isa指針全部都指向_NSConcreteStackBlock類型地址。原因是runtime會對其類型進行了轉(zhuǎn)變,所以以runtime運行時類型即打印出的類型為準。

總結(jié)可得:

block類型 內(nèi)存區(qū)域 環(huán)境 復(fù)制效果copy
NSGlobalBlock 數(shù)據(jù)段 沒有訪問auto變量 什么也不做,類型不變
NSStackBlock 訪問了auto變量 從棧復(fù)制到堆,類型改變?yōu)?code>__ NSMallocBlock__
NSMallocBlock __ NSStackBlock__調(diào)用copy 引用計數(shù)增加,類型不變

棧中的__NSStackBlock__訪問auto變量,作用域執(zhí)行完畢被回收,如果調(diào)用block時已經(jīng)銷毀其內(nèi)存,就會出現(xiàn)問題:

void (^block)(void);
void test() {
    // __NSStackBlock__:訪問了auto變量
    int age = 10;
    block = ^{
        NSLog(@"age = %d", age);
    };
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();
    }
    return 0;
}

// 打印結(jié)果
Demo[1234:567890] age = -272632568

由打印結(jié)果可知:
age沒有打印出正確結(jié)果,這是因為__NSStackBlock__類型的block存儲在棧中,test函數(shù)執(zhí)行完畢后,棧內(nèi)存中block所占用的內(nèi)存被系統(tǒng)回收。

我們可以通過copyNSStackBlock類型的block轉(zhuǎn)化為NSMallocBlock類型的block

void (^block)(void);
void test() {
    // __NSStackBlock__ 調(diào)用copy轉(zhuǎn)化為 __NSMallocBlock__
    int age = 10;
    block = [^{
        NSLog(@"age = %d", age);
    } copy];
    [block release];
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();
    }
    return 0;
}

// 打印結(jié)果
Demo[1234:567890] age = 10
  • MRC環(huán)境下,經(jīng)常使用copy將棧上的block拷貝到堆中,然后手動調(diào)用release操作將其銷毀即可;
  • ARC環(huán)境下,編譯器會自動將棧上的block進行copy操作,將block復(fù)制到堆上。

4. ARC幫我們做了什么?

在ARC環(huán)境下,編譯器會根據(jù)情況自動將棧上的block進行一次copy操作,將block復(fù)制到堆上:

  • block作為函數(shù)返回值時;
  • block賦值給__strong指針時;
  • block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時;
  • block作為GCD API的方法參數(shù)時。

(1) block作為函數(shù)返回值時:

typedef void(^MyBlock)(void);
MyBlock myblock() {
    int age = 10;
    return ^{
        NSLog(@"-------- %d", age);
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // block作為函數(shù)返回值
        MyBlock block = myblock();
        block();

        // MRC打?。篲_NSStackBlock__  (訪問了auto變量)
        // ARC打?。篲_NSMallocBlock__
        NSLog(@"%@", [block class]);
    }
    return 0;
}

block訪問auto變量時,block的類型為__NSStackBlock__;
ARC將棧上的block進行一次copy操作,將block復(fù)制到堆上,并在適當?shù)牡胤竭M行release操作,所以ARC打印block為__NSMallocBlock__類型。

(2) 將block賦值給__strong指針時:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // block內(nèi)沒有訪問auto變量
        MyBlock block = ^{
            NSLog(@"block---------");
        };
        NSLog(@"%@", [block class]);

        // block內(nèi)訪問了auto變量,但沒有賦值給__strong指針
        int age = 10;
        NSLog(@"%@", [^{
            NSLog(@"block1--------- %d", age);
        } class]);

        // block賦值給__strong指針
        MyBlock block2 = ^{
            NSLog(@"block2--------- %d", age);
        };
        NSLog(@"%@", [block2 class]);
    }
    return 0;
}
打印結(jié)果

由打印結(jié)果可知:
將block賦值給__strong指針時,RAC會自動進行一次copy操作。

(3) block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時:

例如:遍歷數(shù)組的block方法,將block作為參數(shù)

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

        }];

(4) block作為GCD API的方法參數(shù)時:

例如:GCD的一次性函數(shù)或延遲執(zhí)行的函數(shù)

        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(), ^{

        });

MRC下block屬性的建議寫法:
@property (copy, nonatomic) void (^block)(void);

ARC下block屬性的建議寫法:
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容