iOS Block的變量捕獲機(jī)制

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

先看幾段代碼:

執(zhí)行下面的代碼會(huì)輸出什么?

int main(int argc, const char * argv[]) { 
    void(^block)(int,int) = ^(int a, int b){
        NSLog(@"a = %d, b = %d",a,b);
    };
    block(10,20);
    return 0;
}

會(huì)輸出 a = 10, b = 20

執(zhí)行下面的代碼會(huì)輸出什么?

int main(int argc, const char * argv[]) { 
    int age = 10;
    void (^block)(void) = ^{
        NSLog(@"age = %d",age);
    };
    age = 20;
    block();
    return 0;
}

會(huì)輸出age = 10,但是age明明已經(jīng)重新賦值成20了,為什么執(zhí)行block age的值還是10 呢?

我們將代碼通過(guò)clang -rewrite-objc main.m命令將文件轉(zhuǎn)換為cpp格式的文件,可以看到block的底層結(jié)構(gòu),可以看到上面這兩種block的底層結(jié)構(gòu)有什么區(qū)別:
第一種block:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __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

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

可以看到第二種block的底層結(jié)構(gòu)中,多了一個(gè)int類型 名字為age的變量,為什么會(huì)多一個(gè)這樣的變量呢?
因?yàn)閎lock為了保證在其內(nèi)部能夠正常訪問(wèn)外部的變量,block有一個(gè)變量捕獲機(jī)制 capture,在創(chuàng)建block的時(shí)候,age=10, age這個(gè)值已經(jīng)存儲(chǔ)到block內(nèi)部了,所以即使age后來(lái)被重新賦值,運(yùn)行block時(shí)打印結(jié)果依然是age = 10,第一種block內(nèi)部沒有訪問(wèn)外界的變量,所以它的底層結(jié)構(gòu)不會(huì)發(fā)生變化

此時(shí) 我們把a(bǔ)ge改成一個(gè)靜態(tài)變量,作用域不變,就像這樣:

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

運(yùn)行程序后會(huì)看到此時(shí)的打印結(jié)果為 age = 20,block運(yùn)行后得出的值會(huì)隨著age的改變而改變, 那么block是不是就沒有捕獲這個(gè)靜態(tài)變量呢?

我們同樣可以看一下這個(gè)block的底層結(jié)構(gòu):

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

可以看到block的底層結(jié)構(gòu)中,依然會(huì)增加一個(gè) *age 的變量,說(shuō)明這種情況下block依然捕獲了靜態(tài)類型的age變量,與第二種block不同的是,第二種block相當(dāng)于在block內(nèi)部新建了一個(gè)int類型的變量來(lái)保存外部的那個(gè)age的值,而在這個(gè)block內(nèi)部 相當(dāng)于保存了外部age這個(gè)變量的內(nèi)存地址,block內(nèi)部的age與外部的age是同一個(gè)地址,所以當(dāng)外部的age值改變時(shí),block內(nèi)部的age值也會(huì)改變

那如果age是一個(gè)全局變量 而不是一個(gè)局部變量呢?像這樣:

int age = 10;//全局變量
static int height = 60;//靜態(tài)全局變量

int main(int argc, const char * argv[]) {
    
    void (^block)(void) = ^{
        NSLog(@"age = %d, height = %d",age,height);
    };
    age = 20;
    height = 120;
    block();
    
    return 0;
}

此時(shí)程序的運(yùn)行結(jié)果為 age = 20,height = 120, 同樣我們查看block的底層數(shù)據(jù) 發(fā)現(xiàn) block并沒有捕獲這兩個(gè)全局變量

int age = 10;
static int height = 60;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __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;
  }
};

剛才用到的都是基礎(chǔ)類型的變量,如果我們用對(duì)象類型的變量呢?來(lái)試一試:

NSNumber *number1;
static NSNumber *number2;

int main(int argc, const char * argv[]) {
    
    NSNumber *number3;
    static NSNumber *number4;
    
    number1 = @1;
    number2 = @2;
    number3 = @3;
    number4 = @4;
    

    void (^block)(void) = ^{
        NSLog(@"number1 = %@ number1Pointer = %p",number1,&number1);
        NSLog(@"number2 = %@ number2Pointer = %p",number2,&number2);
        NSLog(@"number3 = %@ number3Pointer = %p",number3,&number3);
        NSLog(@"number4 = %@ number4Pointer = %p",number4,&number4);
    };
    
    number1 = @10;
    number2 = @20;
    number3 = @30;
    number4 = @40;
    
    NSLog(@"number1 = %@ number1Pointer = %p",number1,&number1);
    NSLog(@"number2 = %@ number2Pointer = %p",number2,&number2);
    NSLog(@"number3 = %@ number3Pointer = %p",number3,&number3);
    NSLog(@"number4 = %@ number4Pointer = %p",number4,&number4);

    
    block();
    
    return 0;
}

運(yùn)行程序 得到的結(jié)果是:


屏幕快照 2019-06-16 上午9.49.34.png

可以看到 只有number3 的值和內(nèi)存地址都發(fā)生了變化,其余的都沒有變化,那其他三個(gè)是不是都沒有被block捕獲呢?我們還是通過(guò)這個(gè)block的底層結(jié)構(gòu)來(lái)看一下:

NSNumber *number1;
static NSNumber *number2;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSNumber *number3;
  NSNumber **number4;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSNumber *_number3, NSNumber **_number4, int flags=0) : number3(_number3), number4(_number4) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

首先可以確定的是 number1和number2是沒有被block捕獲的,因?yàn)镹SNumber是對(duì)象類型本身就是一個(gè)指針,所以number3 是被block捕獲了,從前后兩次打印出來(lái)的number3的數(shù)據(jù)可以看出來(lái)兩個(gè)number3的地址是不同的,block內(nèi)部相當(dāng)于新建了一個(gè)NSNumber類型的變量來(lái)保存外部的number3,而number4 在block內(nèi)部是一個(gè)雙指針,也就是block內(nèi)部保存了這個(gè)number4內(nèi)存地址的指針,所以兩個(gè)number4前后的值和地址是一樣的。

可以看到無(wú)論是基礎(chǔ)類型或者對(duì)象類型,block對(duì)于變量的捕獲機(jī)制基本是相同的:

  • 局部變量
    • auto變量 會(huì)被捕獲,訪問(wèn)方式是值傳遞 (block內(nèi)部會(huì)專門新增一個(gè)成員來(lái)存儲(chǔ)auto變量的值,block運(yùn)行時(shí)會(huì)訪問(wèn)這個(gè)新增的成員)
    • static變量 會(huì)被捕獲,訪問(wèn)方式是指針傳遞(問(wèn)題:這種方式到底算不算捕獲?)
  • 全局變量 不會(huì)捕獲,會(huì)直接去訪問(wèn)

可以看到只要是在block內(nèi)部訪問(wèn)局部變量,那么block就會(huì)捕獲這個(gè)變量,區(qū)別在于如果是自動(dòng)變量是捕獲它的值,而靜態(tài)變量是捕獲它的指針,如果block內(nèi)部訪問(wèn)的是全局變量,block就不會(huì)捕獲這個(gè)變量(無(wú)論是靜態(tài)還是非靜態(tài)全局變量)

那么block為什么要采用這種做法呢?為什么局部變量就需要捕獲,全局變量就不用?

我們來(lái)看另一段代碼:

void (^block)(void);
void test(){
    int age = 10;
    static int height = 60;
    block = ^{
        NSLog(@"age = %d, height = %d",age,height);
    };
}

int main(int argc, const char * argv[]) {
    test();
    block();
    
    return 0;
}

很明顯 test()方法執(zhí)行完畢之后,它方法內(nèi)部的變量age和height就出了作用域了,在作用域之外就無(wú)法訪問(wèn),然后執(zhí)行block()方法,而block()方法內(nèi)部又用到了age和height,但是此時(shí)這兩個(gè)變量已經(jīng)不能訪問(wèn)了(auto變量已經(jīng)銷毀,自然無(wú)法訪問(wèn),static局部變量雖然不會(huì)銷毀,但已經(jīng)出了作用域,也不能訪問(wèn)),如果要保證正常的訪問(wèn),就相當(dāng)于要達(dá)到跨函數(shù)訪問(wèn)變量這種效果,所以block就會(huì)采用捕獲局部變量這種方式來(lái)保證程序正常運(yùn)行。

為什么auto變量是值傳遞?static變量是指針傳遞?

因?yàn)閍uto類型的局部變量 出了自己的作用域就被銷毀了,這個(gè)變量就不存在了,它原來(lái)所占的內(nèi)存就變成了垃圾內(nèi)存了,不可以再訪問(wèn),所以針對(duì)這種變量就需要在創(chuàng)建block的時(shí)候馬上保存到block內(nèi)部,否則在運(yùn)行block的時(shí)候這個(gè)變量就可能沒了,所以在block創(chuàng)建之后再怎么改變這個(gè)變量的值,運(yùn)行block的時(shí)候依然是之前的值 。

而static局部變量雖然出了作用域也不能訪問(wèn),但它的內(nèi)存是一直存在的,不會(huì)銷毀,所以block只需要在運(yùn)行的時(shí)候能訪問(wèn)到它就可以,所以針對(duì)這種變量block采用的是指針傳遞,block內(nèi)部只要保存這個(gè)變量的內(nèi)存地址就可以保證在block運(yùn)行的時(shí)候訪問(wèn)到這個(gè)變量,而正因?yàn)槭侵羔槀鬟f,多以block在運(yùn)行的時(shí)候總能夠訪問(wèn)到這個(gè)變量最新的值。

看到這里,我們也很容易明白為什么全局變量不用捕獲,因?yàn)槿肿兞考炔粫?huì)被銷毀,也可以隨處訪問(wèn),所以block根本不用去捕獲它也可能隨時(shí)隨地訪問(wèn)到它的值。

注意:
在一個(gè)類中的block的實(shí)現(xiàn)中用到了self,那這個(gè)block會(huì)捕獲self(其實(shí)也就是這個(gè)類的實(shí)例對(duì)象),因?yàn)閟elf是一個(gè)局部變量,通過(guò)類中的方法底層實(shí)現(xiàn)可以看到,每個(gè)方法的前兩個(gè)參數(shù)都是self和方法名,那么self也就是一個(gè)參數(shù),肯定是一個(gè)局部變量

如果block的實(shí)現(xiàn)中用到了這個(gè)類的某個(gè)屬性(比如_name)那block也是會(huì)捕獲的,因?yàn)開name相當(dāng)于self->name,此時(shí)block會(huì)直接捕獲self,而不是單單捕獲name這個(gè)屬性,同樣對(duì)于self.name這樣的屬性,block也會(huì)捕獲self

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

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

  • 參考篇:iOS-Block淺談 前言:本文簡(jiǎn)述Block本質(zhì),如有錯(cuò)誤請(qǐng)留言指正。 第一部分:Block本質(zhì) Q:...
    夢(mèng)蕊dream閱讀 61,910評(píng)論 41 323
  • 第一部分:Block本質(zhì) Q:什么是Block,Block的本質(zhì)是什么? block本質(zhì)上也是一個(gè)OC對(duì)象,它內(nèi)部...
    sheldon_龍閱讀 607評(píng)論 0 0
  • 一. 查看block內(nèi)部實(shí)現(xiàn) 1.編寫block代碼void (^DemoBlock)(int, int) = ^...
    李永開閱讀 385評(píng)論 0 0
  • 前言:Block 是開發(fā)過(guò)程中常用便捷的回調(diào)方式,本文簡(jiǎn)單介紹 Block 一、Block 簡(jiǎn)介 Block 對(duì)象...
    夢(mèng)蕊dream閱讀 4,859評(píng)論 5 26
  • Block概要 Block:帶有自動(dòng)變量的匿名函數(shù)。 匿名函數(shù):沒有函數(shù)名的函數(shù),一對(duì){}包裹的內(nèi)容是匿名函數(shù)的作...
    zweic閱讀 548評(píng)論 0 2

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