iOS源碼解析:Block的本質(zhì)<一>

Block在iOS開發(fā)中的用途非常廣往枷,今天我們就來一起探索一下Block的底層結(jié)構(gòu)嘿悬。

1. Block的底層結(jié)構(gòu)

下面是一個(gè)沒有參數(shù)和返回值的簡單的Block:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        void (^block)(void) = ^{
            
            NSLog(@"Hello World!");
        };
        
        block();
        
        return 0;
    }
}

為了探索Block的底層結(jié)構(gòu)嫁怀,我們將main.m文件轉(zhuǎn)化為C++的源碼溉愁、我們打開命令行维蒙。cd到包含main.m文件的文件夾错蝴,然后輸入:clang -rewrite-objc main.m,這個(gè)時(shí)候在該文件夾的目錄下會(huì)生成main.cpp文件摇幻。
這個(gè)文件非常長横侦,我們直接拉到文件的最下面,找到main函數(shù):

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));
         //調(diào)用block
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

        return 0;
    }
}

這第一行代碼是定義一個(gè)block變量绰姻,第二行代碼是調(diào)用block枉侧。這兩行代碼看起來非常復(fù)雜。但是我們可以去簡化一下狂芋,怎么簡化呢榨馁?

變量前面的()一般是做強(qiáng)制類型轉(zhuǎn)換的,比如在調(diào)用block這一行帜矾,block前面有一個(gè)()是(__block_impl *)翼虫,這就是進(jìn)行了一個(gè)強(qiáng)制類型轉(zhuǎn)換,將其轉(zhuǎn)換為一個(gè)_block_impl類型的結(jié)構(gòu)體指針屡萤,那像這樣的強(qiáng)制類型轉(zhuǎn)換非常妨礙我們理解代碼珍剑,我們可以暫時(shí)將這些強(qiáng)制類型轉(zhuǎn)換去掉,這樣可以幫助我們理解代碼死陆。

化簡后的代碼如下:

//定義block
void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
//調(diào)用block
block->FuncPtr(block);

這樣化簡后的代碼就要清爽多了招拙。我們一句一句的看,先看第一句:

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

這句代碼的意思好像就是調(diào)用_main_block_impl_0這個(gè)函數(shù)翔曲,給這個(gè)函數(shù)傳進(jìn)兩個(gè)參數(shù)_main_block_func_0&_main_block_desc_0_DATA,然后得到這個(gè)函數(shù)的返回值迫像,取函數(shù)返回值的地址,賦值給block這個(gè)指針瞳遍。
我們?cè)谏晕⑸弦稽c(diǎn)的位置可以找到_main_block_impl_0這個(gè)結(jié)構(gòu):

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
//構(gòu)造函數(shù)闻妓,類似于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這個(gè)結(jié)構(gòu)體的結(jié)構(gòu)我們可以command+f在main.cpp文件中搜索得到:

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

_main_block_desc_0結(jié)構(gòu)體的結(jié)構(gòu)在main.cpp文件的最下面可以找到:

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

這是一個(gè)C++的結(jié)構(gòu)體。而且在這個(gè)結(jié)構(gòu)體內(nèi)還包含一個(gè)函數(shù)掠械,這個(gè)函數(shù)的函數(shù)名和結(jié)構(gòu)體名稱一致由缆,這在C語言中是沒有的,這是C++特有的猾蒂。

在C++的結(jié)構(gòu)體包含的函數(shù)稱為結(jié)構(gòu)體的構(gòu)造函數(shù)均唉,它就相當(dāng)于是OC中的init方法,用來初始化結(jié)構(gòu)體肚菠。OC中的init方法返回的是對(duì)象本身舔箭,C++的結(jié)構(gòu)體中的構(gòu)造方法返回的也是結(jié)構(gòu)體對(duì)象。

那么我們就知道了,__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);返回的就是_main_block_impl_0這個(gè)結(jié)構(gòu)體對(duì)象层扶,然后取結(jié)構(gòu)體對(duì)象的地址賦值給block指針箫章。換句話說,block指向的就是初始化后的_main_block_impl_0結(jié)構(gòu)體對(duì)象镜会。
我們?cè)倏匆幌鲁跏蓟?code>_main_block_impl_0結(jié)構(gòu)體傳進(jìn)去的參數(shù):

  • 第一個(gè)參數(shù)是_main_block_func_0檬寂,這個(gè)參數(shù)的結(jié)構(gòu)在上面一點(diǎn)的位置也能找到:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {


  NSLog((NSString *)&__NSConstantStringImpl__var_folders_74_wk04zv690mz36wn0g18r5nxm0000gn_T_main_3b803f_mi_0);
 }

這個(gè)函數(shù)其實(shí)就是把我們Block中要執(zhí)行的代碼封裝到這個(gè)函數(shù)內(nèi)部了。我們可以看到這個(gè)函數(shù)內(nèi)部就一行代碼戳表,就是一個(gè)NSlog函數(shù)桶至,這也就是NSLog(@"Hello World!");這句代碼。
把這個(gè)函數(shù)指針傳給_main_block_impl_0的構(gòu)造函數(shù)的第一個(gè)參數(shù)匾旭,然后用這個(gè)函數(shù)指針去初始化_main_block_impl_0這個(gè)結(jié)構(gòu)體的第一個(gè)成員變量impl的成員變量FuncPtr镣屹。也就是說FuncPtr這個(gè)指針指向_main_block_func_0這個(gè)函數(shù)。

  • 第二個(gè)參數(shù)是&_main_block_desc_0_DATA价涝。
    我們看一下這個(gè)結(jié)構(gòu):
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

在結(jié)構(gòu)體的構(gòu)造函數(shù)中野瘦,0賦值給了reserved,sizeof(struct __main_block_impl_0)是賦值給了Block_size飒泻,可以看出這個(gè)結(jié)構(gòu)體存放的是_main_block_impl_0這個(gè)結(jié)構(gòu)體的信息。在_main_block_impl_0的構(gòu)造函數(shù)中我們可以看到吏廉,_main_block_desc_0這個(gè)結(jié)構(gòu)體的地址被賦值給了_main_block_impl_0的第二個(gè)成員變量Desc這個(gè)結(jié)構(gòu)體指針泞遗。也就是說Desc這個(gè)結(jié)構(gòu)體指針指向_main_block_desc_0_DATA這個(gè)結(jié)構(gòu)體。
那么我們總結(jié)一下:

EC36EEA6-C31A-46F5-91A1-B5684A788213.png

所以第一句定義block

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

總結(jié)起來就是:

  • 1.創(chuàng)建一個(gè)函數(shù)_main_block_func_0,這個(gè)函數(shù)
    的作用就是將我們block中要執(zhí)行的代碼封裝到函數(shù)內(nèi)部席覆,方便調(diào)用史辙。
  • 2.創(chuàng)建一個(gè)結(jié)構(gòu)體_main_block_desc_0,這個(gè)結(jié)構(gòu)體中主要包含_main_block_impl_0這個(gè)結(jié)構(gòu)體占用的存儲(chǔ)空間大小等信息。
  • 3.將1中創(chuàng)建的_main_block_func_0這個(gè)函數(shù)的地址佩伤,和2中創(chuàng)建的_main_block_desc_0這個(gè)結(jié)構(gòu)體的地址傳給_main_block_impl_0的構(gòu)造函數(shù)聊倔。
  • 4.利用_main_block_func_0初始化_main_block_impl_0結(jié)構(gòu)體的第一個(gè)成員變量impl的成員變量FuncPtr。這樣_main_bck_impl_0這個(gè)結(jié)構(gòu)體也就得到了block中那個(gè)代碼塊的地址生巡。
  • 5.利用_mian_block_desc_0_DATA去初始化_mian_block_impl_0的第二個(gè)成員變量Desc耙蔑。
    下面我們?cè)倏吹诙秸{(diào)用block:
block->FuncPtr(block);

我們知道,block實(shí)質(zhì)上就是指向_main_block_impl_0這個(gè)結(jié)構(gòu)體的指針孤荣,而FuncPtr是_main_block_impl_0的第第一個(gè)成員變量impl的成員變量甸陌,正常來講,block想要調(diào)用自己的成員變量的成員變量的成員變量盐股,應(yīng)該像下面這樣調(diào)用:

block->impl->FuncPtr

然而事實(shí)卻不是這樣钱豁,這是為什么呢?

原因就在于之前我們把所有的強(qiáng)制類型轉(zhuǎn)換給刪掉了疯汁,之前block前面的()是(__block_impl *)牲尺,為什么可以這樣強(qiáng)制轉(zhuǎn)換呢?因?yàn)閎lock指向的是_main_block_impl_0這個(gè)結(jié)構(gòu)體的首地址幌蚊,而_main_block_impl_0
的第一個(gè)成員變量是struct __block_impl impl;谤碳,所以impl和_main_block_impl_0的首地址是一樣的溃卡,因此指向_main_block_impl_0的首地址的指針也就可以被強(qiáng)制轉(zhuǎn)換為指向impl的首地址的指針。

之前說過估蹄,F(xiàn)uncPtr這個(gè)指針在構(gòu)造函數(shù)中是被初始化為指向_mian_block_func_0這個(gè)函數(shù)的地址塑煎。因此通過block->FuncPtr調(diào)用也就獲取了_main_block_func_0這個(gè)函數(shù)的地址,然后對(duì)_main_block_func_0進(jìn)行調(diào)用臭蚁,也就是執(zhí)行block中的代碼了最铁。這中間block又被當(dāng)做參數(shù)傳進(jìn)了_main_block_func_0這個(gè)函數(shù)。

2.變量捕獲-auto變量

auto變量是聲明在函數(shù)內(nèi)部的變量垮兑,比如int a = 0;這句代碼聲明在函數(shù)內(nèi)部冷尉,那a就是auto變量,等價(jià)于auto int a = 0;auto變量時(shí)分配在棧區(qū)系枪,當(dāng)超出作用域時(shí)雀哨,其占用的內(nèi)存會(huì)被系統(tǒng)自動(dòng)銷毀并生成。下面看一段代碼:

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

這是一個(gè)很簡單的Block捕獲自動(dòng)變量的例子私爷,我們看一下打印結(jié)果:

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

自動(dòng)變量a的值明明已經(jīng)變?yōu)榱?0雾棺,為什么輸出結(jié)果還是10呢?我們把這段代碼轉(zhuǎn)化為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);

對(duì)比一下上面分析的沒有捕獲自動(dòng)變量的源代碼捌浩,我們發(fā)現(xiàn)這里_main_block_impl_0中傳入的參數(shù)多了一個(gè)a。然后我們往上翻看看_main_block_impl_0的結(jié)構(gòu):

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  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這個(gè)結(jié)構(gòu)體中我們發(fā)現(xiàn)多了一個(gè)int類型的成員變量a工秩,在結(jié)構(gòu)體的構(gòu)造函數(shù)中多了一個(gè)參數(shù)int _a尸饺,并且用這個(gè)int _a去初始化成員變量a。
所以在void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a);中傳入了自動(dòng)變量a用來初始化_main_block_impl_0的成員變量a助币。那這個(gè)時(shí)候_main_block_impl_0的成員變量a就被賦值為10了浪听。
由于上面這一步是值傳遞,所以當(dāng)執(zhí)行a = 20時(shí)眉菱,_main_block_impl_0結(jié)構(gòu)體的成員變量a的值是不會(huì)隨之改變的迹栓,仍然是10。
然后我們?cè)賮砜匆幌耞main_block_func_0的結(jié)構(gòu)有何變化:

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這個(gè)結(jié)構(gòu)體獲得其成員變量a的值迈螟。

3.變量捕獲-static變量

上面講的捕獲的是自動(dòng)變量,在函數(shù)內(nèi)部聲明的變量默認(rèn)為自動(dòng)變量尔崔,即默認(rèn)用auto修飾答毫。那么如果在函數(shù)內(nèi)部聲明的變量用static修飾,又會(huì)帶來哪些不同呢季春?static變量和auto變量的不同之處在于變量的內(nèi)存的回收時(shí)機(jī)洗搂。auto變量在其作用域結(jié)束時(shí)就會(huì)被系統(tǒng)自動(dòng)回收,而static變量在變量的作用域結(jié)束時(shí)并不會(huì)被系統(tǒng)自動(dòng)回收。
先看一段代碼:

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

我們看一下打印結(jié)果:

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

結(jié)果是20耘拇,這個(gè)和2中的打印結(jié)果不一樣撵颊,為什么局部變量從auto變成了static結(jié)果會(huì)不一樣呢?我們還是從源碼來分析:

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這個(gè)變量的地址值。那么這個(gè)&a是賦值給誰了呢嘉涌?我們上翻找到_main_block_impl_0的結(jié)構(gòu):

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

這里我們可以看到結(jié)構(gòu)體多了一個(gè)指針類型的成員變量int *a,然后在構(gòu)造函數(shù)中妻熊,將傳遞過來的&a,賦值給這個(gè)指針變量仑最。也就是說扔役,在_main_block_impl_0這個(gè)結(jié)構(gòu)體中多了一個(gè)成員變量,這個(gè)成員變量是指針警医,指向a這個(gè)變量亿胸。所以當(dāng)a變量的值發(fā)生變化時(shí),能夠得到最新的值预皇。

4.變量捕獲-全局變量

2和3分析了兩種類型的局部變量侈玄,auto局部變量和static局部變量。這一部分則分析全局變量吟温。全局變量會(huì)不會(huì)像局部變量一樣被block所捕獲呢拗馒?我們還是看一下實(shí)例:

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;
    }
}

打印結(jié)果:

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這兩個(gè)全局變量沒有作為參數(shù)傳入_main_block_impl_0中去溯街。然后我們?cè)俨榭匆幌耞main_block_impl_0的結(jié)構(gòu):

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;
  }
};

可以看到,_main_block_impl_0中并沒有增加成員變量洋丐。然后我們?cè)倏確main_block_func_0的結(jié)構(gòu):

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);
        }

可以看到呈昔,這個(gè)地方在調(diào)用的時(shí)候是直接調(diào)用的全局變量height和weight。
所以我們可以得出結(jié)論友绝,block并不會(huì)不會(huì)全局變量堤尾。
總結(jié):

變量類型 是否捕獲到block內(nèi)部 訪問方式
局部變量auto 值傳遞
局部變量static 指針傳遞
全局變量 直接訪問
思考 為什么對(duì)于不同類型的變量,block的處理方式不同呢迁客?

這是由變量的生命周期決定的郭宝。對(duì)于自動(dòng)變量,當(dāng)作用域結(jié)束時(shí)掷漱,會(huì)被系統(tǒng)自動(dòng)回收粘室,而block很可能是在超出自動(dòng)變量作用域的時(shí)候去執(zhí)行,如果之前沒有捕獲自動(dòng)變量卜范,那么后面執(zhí)行的時(shí)候衔统,自動(dòng)變量已經(jīng)被回收了,得不到正確的值。對(duì)于static局部變量锦爵,它的生命周期不會(huì)因?yàn)樽饔糜蚪Y(jié)束而結(jié)束舱殿,所以block只需要捕獲這個(gè)變量的地址,在執(zhí)行的時(shí)候通過這個(gè)地址去獲取變量的值险掀,這樣可以獲得變量的最新的值沪袭。gao'mi而對(duì)于全局變量,在任何位置都可以直接讀取變量的值樟氢。

5.變量捕獲-self變量

看下面一段代碼:

@implementation Person

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

@end

這個(gè)Person類中只有一個(gè)東西冈绊,就是test這個(gè)函數(shù),那么這個(gè)block有沒有捕獲self變量呢嗡害?
要搞清這個(gè)問題焚碌,我們只需要知道搞清楚這里self變量是局部變量還是全局變量,如果是局部變量霸妹,那么是一定會(huì)捕獲的十电,而如果是全局變量,則一定不會(huì)被捕獲叹螟。
我們把這個(gè)Person.m文件轉(zhuǎn)化為c++的源碼鹃骂,然后找到test函數(shù)在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, self, 570425344));
}

我們可以看到,本來Person.m中罢绽,這個(gè)test函數(shù)我是沒有傳任何參數(shù)的畏线,但是轉(zhuǎn)化為c++的代碼后,這里傳入了兩個(gè)參數(shù)良价,一個(gè)是self參數(shù)寝殴,一個(gè)是_cmd。self很常見明垢,_cmd表示test函數(shù)本身蚣常。所以我們就很清楚了,self是作為參數(shù)傳進(jìn)來痊银,也就是局部變量抵蚊,那么block應(yīng)該是捕獲了self變量,事實(shí)是不是這樣呢溯革?我們只需要查看一下_Person_test_block_impl_0的結(jié)構(gòu)就可以知道了贞绳。
_Person_test_block_impl_0的結(jié)構(gòu):

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 *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到,self確實(shí)是作為成員變量被捕獲了致稀。

6.Block的類型

前面已經(jīng)說過了冈闭,Block的本質(zhì)就是一個(gè)OC對(duì)象,既然它是OC對(duì)象抖单,那么它就有類型拒秘。
在搞清楚Block的類型之前号显,先把ARC關(guān)掉,因?yàn)锳RC幫我們做了太多的事躺酒,不方便我們觀察結(jié)果押蚤。關(guān)掉ARC的方法在Build Settings里面搜索Objective-C Automatic Reference Counting,把這一項(xiàng)置為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(@"%@\n %@\n %@\n %@", [block class], [[block class] superclass], [[[block class] superclass] superclass], [[[[block class] superclass] superclass] superclass]);
        
        return 0;
    }
}

上面的代碼的打印結(jié)果是:

 __NSStackBlock__
 __NSStackBlock
 NSBlock
 NSObject

這說明上面定義的這個(gè)block的類型是NSStackBlock揽碘,并且它最終繼承自NSObject也說明Block的本質(zhì)是OC對(duì)象。
Block有三種類型园匹,分別是NSGlobalBlock,MallocBlock,NSStackBlock雳刺。
這三種類型的Block對(duì)象的存儲(chǔ)區(qū)域如下:

對(duì)象的存儲(chǔ)域
NSStackBlock
NSGlobalBlock 程序的數(shù)據(jù)區(qū)域(.data區(qū))
NSMallocBlock

截獲了自動(dòng)變量的Block是NSStackBlock類型,沒有截獲自動(dòng)變量的Block則是NSGlobalStack類型,NSStackBlock類型的Block進(jìn)行copy操作之后其類型變成了NSMallocBlock類型裸违。

Block的類型 副本的配置存儲(chǔ)域 復(fù)制效果
NSStackBlock 從棧復(fù)制到堆
NSGlobalStack 程序的數(shù)據(jù)區(qū)域 什么也不做
NSMallocBlock 引用計(jì)數(shù)增加

下面我們一起分析一下NSStackBlock類型的Block進(jìn)行copy操作后Block對(duì)象從棧復(fù)制到了堆有什么道理掖桦,我們首先來看一段代碼:

void (^block)(void);

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

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

不出意外的話,打印結(jié)果應(yīng)該是10供汛,那么結(jié)果是不是這樣呢枪汪?我們打印看一下:

age=-411258824

很奇怪,打印了一個(gè)這么奇怪的數(shù)字怔昨。這是為什么呢雀久?
block使用了自動(dòng)變量age,所以它是NSStackBlock類型的趁舀,因此block是存放在棧區(qū)赖捌,age是被捕獲作為結(jié)構(gòu)體的成員變量,其值也是被保存在棧區(qū)矮烹。所以當(dāng)test這個(gè)函數(shù)調(diào)用完畢后越庇,它棧上的東西就有可能被銷毀了,一旦銷毀了奉狈,age值就不確定是多少了悦荒。通過打印結(jié)果也可以看到,確實(shí)是影響到了block的執(zhí)行嘹吨。
如果我們對(duì)block執(zhí)行copy操作,結(jié)果會(huì)不會(huì)不一樣呢境氢?

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;
    }
}

打印結(jié)果:

age=10

這個(gè)時(shí)候得出了正確的輸出蟀拷。
因?yàn)閷?duì)block進(jìn)行copy操作后,block從棧區(qū)被復(fù)制到了堆區(qū)萍聊,它的成員變量age也隨之被復(fù)制到了堆區(qū)问芬,這樣test函數(shù)執(zhí)行完之后,它的棧區(qū)被銷毀并不影響block寿桨,因此能得出正確的輸出此衅。

7.ARC環(huán)境下自動(dòng)為Block進(jìn)行copy操作的情況

6中講的最后一個(gè)例子:

void (^block)(void);

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

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

這種使用方式其實(shí)非常常見强戴,我們?cè)谑褂玫臅r(shí)候也沒有發(fā)現(xiàn)有什么問題,那為什么在MRC環(huán)境下就有問題呢慈缔?因?yàn)樵贏RC環(huán)境下編譯器為我們做了很多copy操作铺罢。其中有一個(gè)規(guī)則就是如果Block被強(qiáng)指針指著价匠,那么編譯器就會(huì)對(duì)其進(jìn)行copy操作。我們看到這里:

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

這個(gè)Block塊是被強(qiáng)指針指著道媚,所以它會(huì)進(jìn)行copy操作,由于其使用了自動(dòng)變量翘县,所以是棧區(qū)的Block最域。經(jīng)過復(fù)制以后就到了堆區(qū),這樣由于Block在堆區(qū)锈麸,所以就不受Block執(zhí)行完成的影響镀脂,隨時(shí)可以獲取age的正確值。

總結(jié)一下ARC環(huán)境下自動(dòng)進(jìn)行copy操作的情況一共有以下幾種:
  • block作為函數(shù)返回值時(shí)忘伞。
  • 將block賦值給__strong指針時(shí)薄翅。
  • block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時(shí)。
  • GCD中的API虑省。
block作為函數(shù)返回值時(shí)
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函數(shù)的返回值是一個(gè)block匿刮,那這種情況的時(shí)候,在棧區(qū)的

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

這個(gè)block會(huì)被復(fù)制到堆區(qū)

將block賦值給強(qiáng)指針時(shí)

7中第一個(gè)例子就是將block賦值給強(qiáng)指針時(shí)探颈,進(jìn)行了copy操作的情況熟丸。

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

比如說遍歷數(shù)組的函數(shù):

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

enumerateObjectsUsingBlock:這個(gè)函數(shù)中的block會(huì)進(jìn)行copy操作

GCD中的API

GCD中的很多API的參數(shù)都有block,這個(gè)時(shí)候都會(huì)對(duì)block進(jìn)行一次copy操作伪节,比如下面這個(gè)dispatch_after函數(shù):

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
            NSLog(@"wait");
        });
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末光羞,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子怀大,更是在濱河造成了極大的恐慌纱兑,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件化借,死亡現(xiàn)場離奇詭異潜慎,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)蓖康,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門铐炫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蒜焊,你說我怎么就攤上這事倒信。” “怎么了泳梆?”我有些...
    開封第一講書人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵鳖悠,是天一觀的道長榜掌。 經(jīng)常有香客問我,道長乘综,這世上最難降的妖魔是什么憎账? 我笑而不...
    開封第一講書人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮瘾带,結(jié)果婚禮上鼠哥,老公的妹妹穿的比我還像新娘。我一直安慰自己看政,他們只是感情好朴恳,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著允蚣,像睡著了一般于颖。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上嚷兔,一...
    開封第一講書人閱讀 51,737評(píng)論 1 305
  • 那天森渐,我揣著相機(jī)與錄音,去河邊找鬼冒晰。 笑死同衣,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的壶运。 我是一名探鬼主播耐齐,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼蒋情!你這毒婦竟也來了埠况?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤棵癣,失蹤者是張志新(化名)和其女友劉穎辕翰,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體狈谊,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡喜命,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了河劝。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片壁榕。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖丧裁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情含衔,我是刑警寧澤煎娇,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布二庵,位于F島的核電站,受9級(jí)特大地震影響缓呛,放射性物質(zhì)發(fā)生泄漏催享。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一哟绊、第九天 我趴在偏房一處隱蔽的房頂上張望因妙。 院中可真熱鬧,春花似錦票髓、人聲如沸攀涵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽以故。三九已至,卻和暖如春裆操,著一層夾襖步出監(jiān)牢的瞬間怒详,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來泰國打工踪区, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留昆烁,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓缎岗,卻偏偏與公主長得像静尼,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子密强,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

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