block底層實(shí)現(xiàn)原理

block的本質(zhì)

block在開(kāi)發(fā)中的使用頻率非常高.

block本質(zhì)上是一個(gè)OC對(duì)象,它內(nèi)部也有isa指針,這個(gè)對(duì)象封裝了函數(shù)調(diào)用地址以及函數(shù)調(diào)用環(huán)境(函數(shù)參數(shù)、返回值、捕獲的外部變量等)。當(dāng)我們定義一個(gè)block,在編譯后它的底層存儲(chǔ)結(jié)構(gòu)是怎樣的呢,以下面這個(gè)簡(jiǎn)單的block為例

int age = 20;
void (^block)(void) = ^ {
     NSLog(@"age == %d",age);
};

如果想探究它的底層實(shí)現(xiàn)的話,可以在命令行運(yùn)行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m將這個(gè)main.m文件轉(zhuǎn)成編譯后的c/c++文件,然后在這個(gè)文件搜索__main_block_impl_0就可以找到這個(gè)block的結(jié)構(gòu)體。整體結(jié)構(gòu)如下圖:

16f8481974bfbfae.png

  • impl->isa:就是isa指針,可見(jiàn)它就是一個(gè)OC對(duì)象。
  • impl->FuncPtr:是一個(gè)函數(shù)指針,也就是底層將block中要執(zhí)行的代碼封裝成了一個(gè)函數(shù),然后用這個(gè)指針指向那個(gè)函數(shù)。
  • Desc->Block_size:block占用的內(nèi)存大小。
  • age:捕獲的外部變量age,可見(jiàn)block會(huì)捕獲外部變量并將其存儲(chǔ)在block的底層結(jié)構(gòu)體中。

當(dāng)我們調(diào)用block()時(shí),實(shí)際上就是通過(guò)函數(shù)指針FuncPtr找到封裝的函數(shù)并將block的地址作為參數(shù)傳給這個(gè)函數(shù)進(jìn)行執(zhí)行,把block傳給函數(shù)是因?yàn)楹瘮?shù)執(zhí)行中需要用到的某些數(shù)據(jù)是存在block的結(jié)構(gòu)體中的(比如捕獲的外部變量)。如果定義的是帶參數(shù)的block,調(diào)用block時(shí)是將block地址和block的參數(shù)一起傳給封裝好的函數(shù)。

block的變量捕獲機(jī)制

block外部的變量是可以被block捕獲的,這樣就可以在block內(nèi)部使用外部的變量了。不同類型的變量的捕獲機(jī)制是不一樣的。下面我們來(lái)看一個(gè)示例:

int c = 1000; // 全局變量
static int d = 10000; // 靜態(tài)全局變量

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

        int a = 10; // 局部變量
        static int b = 100; // 靜態(tài)局部變量
        void (^block)(void) = ^{
             NSLog(@"a = %d",a);
             NSLog(@"b = %d",b);
             NSLog(@"c = %d",c);
             NSLog(@"d = %d",d);
         };
         a = 20;
         b = 200;
         c = 2000;
         d = 20000;
         block();
    }
    return 0;
}

//*打印結(jié)果*
a = 10
b = 200
c = 2000
d = 20000

c文件中的結(jié)構(gòu)體如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  int *b;
};

只有2個(gè)局部變量被捕獲了,而且2個(gè)局部變量的捕獲方式還不一樣。

  • 2.1 全局變量的捕獲
    不管是普通全局變量還是靜態(tài)全局變量,block都不會(huì)捕獲。因?yàn)槿肿兞吭谀睦锒伎梢栽L問(wèn),所以block內(nèi)部不捕獲也是可以直接訪問(wèn)全局變量的,所以外部更改全局變量的值時(shí),block內(nèi)部打印的就是最新更改的值。
  • 2.2 靜態(tài)局部變量的捕獲
    我們發(fā)現(xiàn)定義的靜態(tài)局部變量b被block捕獲后,在block結(jié)構(gòu)體里面是以int *b;的形式來(lái)存儲(chǔ)的,也就是說(shuō)block其實(shí)是捕獲的變量b的地址,block內(nèi)部是通過(guò)b的地址去獲取或修改b的值,所以block外部更改b的值會(huì)影響block里面獲取的b的值,block里面更改b的值也會(huì)影響block外面b的值。所以上面會(huì)打印b = 200。
  • 2.3 普通局部變量的捕獲
    普通局部變量就是在一個(gè)函數(shù)或代碼塊中定義的類似int a = 10;的變量,它其實(shí)是省略了auto關(guān)鍵字,等價(jià)于auto int a = 10,所以也叫auto變量。和靜態(tài)局部變量不同的是,普通局部變量被block捕獲后再block底層結(jié)構(gòu)體中是以int a;的形式存儲(chǔ),也就是說(shuō)block捕獲的其實(shí)是a的值(也就是10),并且在block內(nèi)部重新定義了一個(gè)變量來(lái)存儲(chǔ)這個(gè)值,這個(gè)時(shí)候block外部和里面的a其實(shí)是2個(gè)不同的變量,所以外面更改a的值不會(huì)影響block里面的a。所以打印的結(jié)果是a = 10。

為什么普通局部變量要捕獲值,跟靜態(tài)局部變量一樣捕獲地址不行嗎?
是的,不行。因?yàn)槠胀ň植孔兞縜在出了大括號(hào)后就會(huì)被釋放掉了,這個(gè)時(shí)候如果我們?cè)诖罄ㄌ?hào)外面調(diào)用這個(gè)block,block內(nèi)部通過(guò)a的指針去訪問(wèn)a的值就會(huì)拋出異常,因?yàn)閍已經(jīng)被釋放了。而靜態(tài)局部變量的生命周期是和整個(gè)程序的生命周期是一樣的,也就是說(shuō)在整個(gè)程序運(yùn)行過(guò)程中都不會(huì)釋放b,所以不會(huì)出現(xiàn)這種情況。
那有人又有疑問(wèn)了,既然靜態(tài)局部變量一直都不會(huì)被釋放,那block為什么還要捕獲它,直接拿來(lái)用不就可以了嗎?這是因?yàn)殪o態(tài)局部變量作用域只限制在這個(gè)大括號(hào)類,出了這個(gè)大括號(hào),雖然它還存在,但是外面無(wú)法訪問(wèn)它。而前面已經(jīng)介紹過(guò),block里面的代碼在底層是被封裝成了一個(gè)函數(shù),那這個(gè)函數(shù)肯定是在b所在的大括號(hào)外面,所以這個(gè)函數(shù)是無(wú)法直接訪問(wèn)到b的,所以block必須將其捕獲。

block捕獲變量小結(jié)

  • 全局變量--不會(huì)捕獲,是直接訪問(wèn)。
  • 靜態(tài)局部變量--是捕獲變量地址。
  • 普通局部變量--是捕獲變量的值。

block的3種類型

  • NSGlobalBlock ( _NSConcreteGlobalBlock )
  • NSStackBlock ( _NSConcreteStackBlock )
  • NSMallocBlock ( _NSConcreteMallocBlock )

通過(guò)一個(gè)簡(jiǎn)單的代碼查看一下block在什么情況下其類型會(huì)各不相同

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1. 內(nèi)部沒(méi)有調(diào)用外部變量的block
        void (^block1)(void) = ^{  __NSGlobalBlock__
            NSLog(@"Hello");
        };
        // 2. 內(nèi)部調(diào)用外部變量的block
        int a = 10;
        void (^block2)(void) = ^{  __NSStackBlock__
            NSLog(@"Hello - %d",a);
        };
       // 3. 直接調(diào)用的block的class 
        NSLog(@"%@ %@ %@", [block1 class], [block2 class], [^{  __NSMallocBlock__
            NSLog(@"%d",a);
        } class]);
    }
    return 0;
}
block是如何定義其類型

在MRC的環(huán)境下,可以得到下圖:


1637de34b6966052.png

沒(méi)有訪問(wèn)auto變量的block是NSGlobalBlock類型的,存放在數(shù)據(jù)段中。
訪問(wèn)了auto變量的block是NSStackBlock類型的,存放在棧中。
NSStackBlock類型的block調(diào)用copy成為NSMallocBlock類型并被復(fù)制存放在堆中。

block在內(nèi)存中的存儲(chǔ)

通過(guò)下面一張圖看一下不同block的存放區(qū)域

1637de34c0579805.png

上圖中可以發(fā)現(xiàn),根據(jù)block的類型不同,block存放在不同的區(qū)域中。
數(shù)據(jù)段中的NSGlobalBlock直到程序結(jié)束才會(huì)被回收,不過(guò)我們很少使用到NSGlobalBlock類型的block,因?yàn)檫@樣使用block并沒(méi)有什么意義。
NSStackBlock類型的block存放在棧中,我們知道棧中的內(nèi)存由系統(tǒng)自動(dòng)分配和釋放,作用域執(zhí)行完畢之后就會(huì)被立即釋放,而在相同的作用域中定義block并且調(diào)用block似乎也多此一舉。
NSMallocBlock是在平時(shí)編碼過(guò)程中最常使用到的。存放在堆中需要我們自己進(jìn)行內(nèi)存管理。

各類型block調(diào)用copy
1637de351bcee494.png

所以在平時(shí)開(kāi)發(fā)過(guò)程中MRC環(huán)境下經(jīng)常需要使用copy來(lái)保存block,將棧上的block拷貝到堆中,即使棧上的block被銷毀,堆上的block也不會(huì)被銷毀,需要我們自己調(diào)用release操作來(lái)銷毀。而在ARC環(huán)境下系統(tǒng)會(huì)自動(dòng)調(diào)用copy操作,使block不會(huì)被銷毀。

ARC環(huán)境下的block

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

什么情況下ARC會(huì)自動(dòng)將block進(jìn)行一次copy操作? 以下代碼都在ARC環(huán)境下執(zhí)行。

  1. block作為函數(shù)返回值時(shí)
typedef void (^Block)(void);
Block myblock()
{
    int a = 10;
    // 上文提到過(guò),block中訪問(wèn)了auto變量,此時(shí)block類型應(yīng)為_(kāi)_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;
}
  1. 將block賦值給__strong指針時(shí)
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // block內(nèi)沒(méi)有訪問(wèn)auto變量
        Block block = ^{
            NSLog(@"block---------");
        };
        NSLog(@"%@",[block class]);
        int a = 10;
        // block內(nèi)訪問(wèn)了auto變量,但沒(méi)有賦值給__strong指針
        NSLog(@"%@",[^{
            NSLog(@"block1---------%d", a);
        } class]);
        // block賦值給__strong指針
        Block block2 = ^{
          NSLog(@"block2---------%d", a);
        };
        NSLog(@"%@",[block1 class]);
    }
    return 0;
}
  1. block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時(shí)
NSArray *array = @[];
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            
}];

  1. block作為GCD API的方法參數(shù)時(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(), ^{
            
});

block對(duì)對(duì)象型的局部變量的捕獲

block對(duì)對(duì)象類型和對(duì)基本數(shù)據(jù)類型變量的捕獲是不一樣的,對(duì)象類型的變量涉及到強(qiáng)引用和弱引用的問(wèn)題,強(qiáng)引用和弱引用在block底層是怎么處理的呢?
如果block是在棧上,不管捕獲的對(duì)象時(shí)強(qiáng)指針還是弱指針,block內(nèi)部都不會(huì)對(duì)這個(gè)對(duì)象產(chǎn)生強(qiáng)引用。所以我們主要來(lái)看下block在堆上的情況。
首先來(lái)看下強(qiáng)引用的對(duì)象被block捕獲后在底層結(jié)構(gòu)體中是如何存儲(chǔ)的。

// OC代碼
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person *person = [[Person alloc] init];
        person.age = 20;

        void (^block)(void) = ^{
            NSLog(@"age--- %ld",person.age);
         };
        block();

    }
    return 0;
}

// 底層結(jié)構(gòu)體
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__strong person;
};

可以看到和基本數(shù)據(jù)類型不同的是,person對(duì)象被block捕獲后,在結(jié)構(gòu)體中多了一個(gè)修飾關(guān)鍵字__strong

我們?cè)賮?lái)看下弱引用對(duì)象被捕獲后是什么樣的:

// OC代碼
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person *person = [[Person alloc] init];
        person.age = 20;

        __weak Person *weakPerson = person;
        void (^block)(void) = ^{
            NSLog(@"age--- %ld",weakPerson.age);
         };
        block();

    }
    return 0;
}

// 底層block
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__weak weakPerson;
};

可見(jiàn)此時(shí)block中weakPerson的關(guān)鍵字變成了__weak
在block中修飾被捕獲的對(duì)象類型變量的關(guān)鍵字除了__strong__weak外還有一個(gè)__unsafe_unretained。那這結(jié)果關(guān)鍵字起什么作用呢?
當(dāng)block被拷貝到堆上時(shí)是調(diào)用的copy函數(shù),copy函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_assign函數(shù),_Block_object_assign函數(shù)就會(huì)根據(jù)這3個(gè)關(guān)鍵字來(lái)進(jìn)行操作。

  • 如果關(guān)鍵字是__strong,那block內(nèi)部就會(huì)對(duì)這個(gè)對(duì)象進(jìn)行一次retain操作,引用計(jì)數(shù)+1,也就是block會(huì)強(qiáng)引用這個(gè)對(duì)象。也正是這個(gè)原因,導(dǎo)致在使用block時(shí)很容易造成循環(huán)引用。
  • 如果關(guān)鍵字是__weak__unsafe_unretained,那block對(duì)這個(gè)對(duì)象是弱引用,不會(huì)造成循環(huán)引用。所以我們通常在block外面定義一個(gè)__weak__unsafe_unretained修飾的弱指針指向?qū)ο螅缓笤赽lock內(nèi)部使用這個(gè)弱指針來(lái)解決循環(huán)引用的問(wèn)題。

block從堆上移除時(shí),則會(huì)調(diào)用block內(nèi)部的dispose函數(shù),dispose函數(shù)內(nèi)部調(diào)用_Block_object_dispose函數(shù)會(huì)自動(dòng)釋放強(qiáng)引用的變量。

__block修飾符的作用

下面這段代碼:

- (void)test{
    int age = 10;
    void (^block)(void) = ^{
        age = 20;
    };
}

編譯器會(huì)直接報(bào)錯(cuò)。
因?yàn)?code>age是一個(gè)局部變量,它的作用域和生命周期就僅限在是test方法里面,而前面也介紹過(guò)了,block底層會(huì)將大括號(hào)中的代碼封裝成一個(gè)函數(shù),也就相當(dāng)于現(xiàn)在是要在另外一個(gè)函數(shù)中訪問(wèn)test方法中的局部變量,這樣肯定是不行的,所以會(huì)報(bào)錯(cuò)。
如果我想在block里面更改age的值要怎么做呢?我們可以將age定義成靜態(tài)局部變量static int age = 10;。雖然靜態(tài)局部變量的作用域也是在test方法里面,但是它的生命周期是和程序一樣的,而且block捕獲靜態(tài)局部變量實(shí)際是捕獲的age的地址,所以block里面也是通過(guò)age的地址去更改age的值,所以是沒(méi)有問(wèn)題的。
但我們并不推薦這樣做,因?yàn)殪o態(tài)局部變量在程序運(yùn)行過(guò)程中是不會(huì)被釋放的,所以還是要盡量少用。那還有什么別的方法來(lái)實(shí)現(xiàn)這個(gè)需求呢?這就是我們要講的__block關(guān)鍵字。

- (void)test1{
    __block int age = 10;
    void (^block)(void) = ^{
        age = 20;
    };
    block();
    NSLog(@"%d",age);
}

當(dāng)我們用__block關(guān)鍵字修飾后,底層到底做了什么讓我們能在block里面訪問(wèn)age呢?下面我們來(lái)看下上面代碼轉(zhuǎn)成c++代碼后block的存儲(chǔ)結(jié)構(gòu)是什么樣的。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; // by ref
};

struct __Block_byref_age_0 {
  void *__isa; // isa指針
__Block_byref_age_0 *__forwarding; // 如果這block是在堆上那么這個(gè)指針就是指向它自己,如果這個(gè)block是在棧上,那這個(gè)指針是指向它拷貝到堆上后的那個(gè)block
 int __flags;
 int __size; // 結(jié)構(gòu)體大小
 int age; // 真正捕獲到的age
};

我們可以看到,age用__block修飾后,在block的結(jié)構(gòu)體中變成了__Block_byref_age_0 *age;,而__Block_byref_age_0是個(gè)結(jié)構(gòu)體,里面有個(gè)成員int age;,這個(gè)才是真正捕獲到的外部變量age,實(shí)際上外部的age的地址也是指向這里的,所以不管是外面還是block里面,修改age時(shí)其實(shí)都是通過(guò)地址找到這里來(lái)修改的。

所以age用__block修飾后它就不再是一個(gè)test1方法內(nèi)部的局部變量了,而是被包裝成了一個(gè)對(duì)象,age就被存儲(chǔ)在這個(gè)對(duì)象中。之所以說(shuō)是包裝成一個(gè)對(duì)象,是因?yàn)?code>__Block_byref_age_0這個(gè)結(jié)構(gòu)體的第一個(gè)成員就是isa指針。

__block修飾變量的內(nèi)存管理

__block不管是修飾基礎(chǔ)數(shù)據(jù)類型還是修飾對(duì)象數(shù)據(jù)類型,底層都是將它包裝成一個(gè)對(duì)象,然后block結(jié)構(gòu)體中有個(gè)指針指向這個(gè)對(duì)象。既然是一個(gè)對(duì)象,那block內(nèi)部如何對(duì)它進(jìn)行內(nèi)存管理呢?

  • 當(dāng)block在棧上時(shí),block內(nèi)部并不會(huì)對(duì)這個(gè)對(duì)象產(chǎn)生強(qiáng)引用。
  • 當(dāng)block調(diào)用copy函數(shù)從棧拷貝到堆中時(shí),它同時(shí)會(huì)將這個(gè)對(duì)象也拷貝到堆上,并對(duì)這個(gè)對(duì)象產(chǎn)生強(qiáng)引用。
  • 當(dāng)block從堆中移除時(shí),會(huì)調(diào)用block內(nèi)部的dispose函數(shù),dispose函數(shù)內(nèi)部又會(huì)調(diào)用_Block_object_dispose函數(shù)來(lái)釋放這個(gè)對(duì)象。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容