探尋block的本質(zhì)

轉(zhuǎn)自:
探尋block的本質(zhì)
拓展:
探尋OC對(duì)象的本質(zhì)
iOS底層原理總結(jié) - 關(guān)聯(lián)對(duì)象實(shí)現(xiàn)原理
iOS底層原理總結(jié) - Category的本質(zhì)
iOS底層原理總結(jié) - RunLoop
iOS底層原理總結(jié) - 探尋KVO本質(zhì)
iOS底層原理總結(jié) - 探尋Class的本質(zhì)

面試題
1械巡、block的原理是怎樣的整葡?本質(zhì)是什么臼予?
2泻红、__block的作用是什么雇逞?有什么使用注意點(diǎn)麻裁?
3嗤朴、block的屬性修飾詞為什么是copy剥懒?使用block有哪些使用注意须肆?
4匿乃、block在修改NSMutableArray,需不需要添加__block休吠?

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

探尋block的本質(zhì)

首先寫(xiě)一個(gè)簡(jiǎn)單的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;
}

使用命令行將代碼轉(zhuǎn)化為c++查看其內(nèi)部結(jié)構(gòu)阳懂,與OC代碼進(jìn)行比較

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

C++代碼和OC比較.png

上圖中將c++中block的聲明和定義分別與oc代碼中相對(duì)應(yīng)顯示。將c++中block的聲明和調(diào)用分別取出來(lái)查看其內(nèi)部實(shí)現(xiàn)柜思。

定義block變量

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

上述定義代碼中岩调,可以發(fā)現(xiàn),block定義中調(diào)用了__main_block_impl_0函數(shù)赡盘,并且將__main_block_impl_0函數(shù)的地址賦值給了block号枕。那么我們來(lái)看一下__main_block_impl_0函數(shù)內(nèi)部結(jié)構(gòu)。

__main_block_imp_0結(jié)構(gòu)體

__main_block_imp_0結(jié)構(gòu)體.png

__main_block_imp_0結(jié)構(gòu)體內(nèi)有一個(gè)同名構(gòu)造函數(shù)__main_block_imp_0陨享,構(gòu)造函數(shù)中對(duì)一些變量進(jìn)行了賦值最終會(huì)返回一個(gè)結(jié)構(gòu)體葱淳。

那么也就是說(shuō)最終將一個(gè)__main_block_imp_0結(jié)構(gòu)體的地址賦值給了block變量

__main_block_impl_0結(jié)構(gòu)體內(nèi)可以發(fā)現(xiàn)__main_block_impl_0構(gòu)造函數(shù)中傳入了四個(gè)參數(shù)。(void *)__main_block_func_0抛姑、&__main_block_desc_0_DATA赞厕、age、flags定硝。其中flage有默認(rèn)值皿桑,也就說(shuō)flage參數(shù)在調(diào)用的時(shí)候可以省略不傳。而最后的 age(_age)則表示傳入的_age參數(shù)會(huì)自動(dòng)賦值給age成員蔬啡,相當(dāng)于age = _age诲侮。

接下來(lái)著重看一下前面三個(gè)參數(shù)分別代表什么。

1箱蟆、(void *)__main_block_func_0

(void *)__main_block_func_0.png

在__main_block_func_0函數(shù)中首先取出block中age的值沟绪,緊接著可以看到兩個(gè)熟悉的NSLog,可以發(fā)現(xiàn)這兩段代碼恰恰是我們?cè)赽lock塊中寫(xiě)下的代碼空猜。
那么__main_block_func_0函數(shù)中其實(shí)存儲(chǔ)著我們block中寫(xiě)下的代碼近零。

而__main_block_impl_0函數(shù)中傳入的是(void *)__main_block_func_0诺核,也就說(shuō)將我們寫(xiě)在block塊中的代碼封裝成__main_block_func_0函數(shù),并將__main_block_func_0函數(shù)的地址傳入了__main_block_impl_0的構(gòu)造函數(shù)中保存在結(jié)構(gòu)體內(nèi)久信。

2窖杀、&__main_block_desc_0_DATA

&__main_block_desc_0_DATA.png

我們可以看到__main_block_desc_0中存儲(chǔ)著兩個(gè)參數(shù),reserved和Block_size裙士,并且reserved賦值為0而
Block_size則存儲(chǔ)著__main_block_impl_0的占用空間大小入客。
最終將__main_block_desc_0結(jié)構(gòu)體的地址傳入__main_block_func_0中賦值給Desc。

age

age也就是我們定義的局部變量腿椎。因?yàn)樵赽lock塊中使用到age局部變量桌硫,所以在block聲明的時(shí)候這里才會(huì)將age作為參數(shù)傳入,也就說(shuō)block會(huì)捕獲age啃炸,如果沒(méi)有在block中使用age铆隘,這里將只會(huì)傳入(void *)__main_block_func_0,&__main_block_desc_0_DATA兩個(gè)參數(shù)南用。

這里可以根據(jù)源碼思考一下為什么當(dāng)我們?cè)诙xblock之后修改局部變量age的值膀钠,在block調(diào)用的時(shí)候無(wú)法生效。

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

因?yàn)閎lock在定義的之后已經(jīng)將age的值傳入存儲(chǔ)在__main_block_imp_0結(jié)構(gòu)體中并在調(diào)用的時(shí)候?qū)ge從block中取出來(lái)使用裹虫,因此在block定義之后對(duì)局部變量進(jìn)行改變是無(wú)法被block捕獲的肿嘲。

此時(shí)回過(guò)頭來(lái)查看__main_block_impl_0結(jié)構(gòu)體

__main_block_impl_0結(jié)構(gòu)體.png

首先我們看一下__block_impl第一個(gè)變量就是__block_impl結(jié)構(gòu)體。 來(lái)到__block_impl結(jié)構(gòu)體內(nèi)部

__block_impl結(jié)構(gòu)體.png

我們可以發(fā)現(xiàn)__block_impl結(jié)構(gòu)體內(nèi)部就有一個(gè)isa指針筑公。因此可以證明block本質(zhì)上就是一個(gè)oc對(duì)象雳窟。而在構(gòu)造函數(shù)中將函數(shù)中傳入的值分別存儲(chǔ)在__main_block_impl_0結(jié)構(gòu)體實(shí)例中,最終將結(jié)構(gòu)體的地址賦值給block匣屡。

接著通過(guò)上面對(duì)__main_block_impl_0結(jié)構(gòu)體構(gòu)造函數(shù)三個(gè)參數(shù)的分析我們可以得出結(jié)論:

1. __block_impl結(jié)構(gòu)體中isa指針存儲(chǔ)著&_NSConcreteStackBlock地址封救,可以暫時(shí)理解為其類對(duì)象地址,block就是_NSConcreteStackBlock類型的捣作。

2. block代碼塊中的代碼被封裝成__main_block_func_0函數(shù),F(xiàn)uncPtr則存儲(chǔ)著__main_block_func_0函數(shù)的地址虾宇。

3. Desc指向__main_block_desc_0結(jié)構(gòu)體對(duì)象嘱朽,其中存儲(chǔ)__main_block_impl_0結(jié)構(gòu)體所占用的內(nèi)存怔接。

調(diào)用block執(zhí)行內(nèi)部代碼

// 執(zhí)行block內(nèi)部的代碼
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);

通過(guò)上述代碼可以發(fā)現(xiàn)調(diào)用block是通過(guò)block找到FunPtr直接調(diào)用搪泳,通過(guò)上面分析我們知道block指向的是__main_block_impl_0類型結(jié)構(gòu)體,但是我們發(fā)現(xiàn)__main_block_impl_0結(jié)構(gòu)體中并不直接就可以找到FunPtr扼脐,而FunPtr是存儲(chǔ)在__block_impl中的岸军,為什么block可以直接調(diào)用__block_impl中的FunPtr呢奋刽?

重新查看上述源代碼可以發(fā)現(xiàn),(__block_impl *)block將block強(qiáng)制轉(zhuǎn)化為_(kāi)_block_impl類型的艰赞,因?yàn)開(kāi)_block_impl是__main_block_impl_0結(jié)構(gòu)體的第一個(gè)成員佣谐,相當(dāng)于將__block_impl結(jié)構(gòu)體的成員直接拿出來(lái)放在__main_block_impl_0中,那么也就說(shuō)明__block_impl的內(nèi)存地址就是__main_block_impl_0結(jié)構(gòu)體的內(nèi)存地址開(kāi)頭方妖。所以可以轉(zhuǎn)化成功狭魂。并找到FunPtr成員。

上面我們知道党觅,F(xiàn)unPtr中存儲(chǔ)著通過(guò)代碼塊封裝的函數(shù)地址雌澄,那么調(diào)用此函數(shù),也就是會(huì)執(zhí)行代碼塊中的代碼杯瞻。并且回頭查看__main_block_func_0函數(shù)镐牺,可以發(fā)現(xiàn)第一個(gè)參數(shù)就是__main_block_impl_0類型的指針。也就是說(shuō)將block傳入__main_block_func_0函數(shù)中魁莉,便于重中取出block捕獲的值睬涧。

如何驗(yàn)證block的本質(zhì)確實(shí)是__main_block_impl_0結(jié)構(gòu)體類型。

通過(guò)代碼證明一下上述內(nèi)容: 同樣使用之前的方法沛厨,我們按照上面分析的block內(nèi)部結(jié)構(gòu)自定義結(jié)構(gòu)體宙地,并將block內(nèi)部的結(jié)構(gòu)體強(qiáng)制轉(zhuǎn)化為自定義的結(jié)構(gòu)體,轉(zhuǎn)化成功說(shuō)明底層結(jié)構(gòu)體確實(shí)如我們之前分析的一樣宅粥。

struct __main_block_desc_0 { 
    size_t reserved;
    size_t Block_size;
};
struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};
// 模仿系統(tǒng)__main_block_impl_0結(jié)構(gòu)體
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);
        };
// 將底層的結(jié)構(gòu)體強(qiáng)制轉(zhuǎn)化為我們自己寫(xiě)的結(jié)構(gòu)體,通過(guò)我們自定義的結(jié)構(gòu)體探尋block底層結(jié)構(gòu)體
        struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block;
        block(3,5);
    }
    return 0;
}

通過(guò)打斷點(diǎn)可以看出我們自定義的結(jié)構(gòu)體可以被賦值成功,以及里面的值钞诡。

賦值成功.png

接下來(lái)斷點(diǎn)來(lái)到block代碼塊中,看一下堆棧信息中的函數(shù)調(diào)用地址朵诫。Debuf workflow -> always show Disassembly

函數(shù)調(diào)用地址.png

通過(guò)上圖可以看到地址確實(shí)和FuncPtr中的代碼塊地址一樣废累。

總結(jié)
此時(shí)已經(jīng)基本對(duì)block的底層結(jié)構(gòu)有了基本的認(rèn)識(shí)邑滨,上述代碼可以通過(guò)一張圖展示其中各個(gè)結(jié)構(gòu)體之間的關(guān)系。

各個(gè)結(jié)構(gòu)體之間的關(guān)系.png

block底層的數(shù)據(jù)結(jié)構(gòu)也可以通過(guò)一張圖來(lái)展示

block底層的數(shù)據(jù)結(jié)構(gòu).png

block的變量捕獲
為了保證block內(nèi)部能夠正常訪問(wèn)外部的變量,block有一個(gè)變量捕獲機(jī)制耳峦。

局部變量
auto變量
上述代碼中我們已經(jīng)了解過(guò)block對(duì)age變量的捕獲蹲坷。
auto自動(dòng)變量疙咸,離開(kāi)作用域就銷毀题山,通常局部變量前面自動(dòng)添加auto關(guān)鍵字焰络。自動(dòng)變量會(huì)捕獲到block內(nèi)部囱稽,也就是說(shuō)block內(nèi)部會(huì)專門新增加一個(gè)參數(shù)來(lái)存儲(chǔ)變量的值各拷。
auto只存在于局部變量中规哲,訪問(wèn)方式為值傳遞痘番,通過(guò)上述對(duì)age參數(shù)的解釋我們也可以確定確實(shí)是值傳遞良漱。

static變量
static 修飾的變量為指針傳遞返帕,同樣會(huì)被block捕獲梧油。
接下來(lái)分別添加aotu修飾的局部變量和static修飾的局部變量,重看源碼來(lái)看一下他們之間的差別。

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本質(zhì)[57465:18555229] hello, a = 10, b = 2
// block中a的值沒(méi)有被改變而b的值隨外部變化而變化漏益。

重新生成c++代碼看一下內(nèi)部結(jié)構(gòu)中兩個(gè)參數(shù)的區(qū)別夸盟。

兩個(gè)參數(shù)的區(qū)別.png

從上述源碼中可以看出,a,b兩個(gè)變量都有捕獲到block內(nèi)部硼莽。但是a傳入的是值,而b傳入的則是地址休傍。

為什么兩種變量會(huì)有這種差異呢凫岖,因?yàn)樽詣?dòng)變量可能會(huì)銷毀甥雕,block在執(zhí)行的時(shí)候有可能自動(dòng)變量已經(jīng)被銷毀了挟阻,那么此時(shí)如果再去訪問(wèn)被銷毀的地址肯定會(huì)發(fā)生壞內(nèi)存訪問(wèn)附鸽,因此對(duì)于自動(dòng)變量一定是值傳遞而不可能是指針傳遞了。而靜態(tài)變量不會(huì)被銷毀击你,所以完全可以傳遞地址丁侄。而因?yàn)閭鬟f的是值得地址,所以在block調(diào)用之前修改地址中保存的值拙吉,block中的地址是不會(huì)變得筷黔。所以值會(huì)隨之改變。

全局變量
我們同樣以代碼的方式看一下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++代碼查看全局變量調(diào)用方式

全局變量調(diào)用方式.png

通過(guò)上述代碼可以發(fā)現(xiàn)订歪,__main_block_imp_0并沒(méi)有添加任何變量,因此block不需要捕獲全局變量眼虱,因?yàn)槿肿兞繜o(wú)論在哪里都可以訪問(wèn)。

局部變量因?yàn)榭绾瘮?shù)訪問(wèn)所以需要捕獲胆筒,全局變量在哪里都可以訪問(wèn) 仆救,所以不用捕獲。

最后以一張圖做一個(gè)總結(jié)

總結(jié).png

總結(jié):局部變量都會(huì)被block捕獲顿痪,自動(dòng)變量是值捕獲,靜態(tài)變量為地址捕獲揩悄。全局變量則不會(huì)被block捕獲

疑問(wèn):以下代碼中block是否會(huì)捕獲變量呢删性?

#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

同樣轉(zhuǎn)化為c++代碼查看其內(nèi)部結(jié)構(gòu)

內(nèi)部結(jié)構(gòu).png

上圖中可以發(fā)現(xiàn),self同樣被block捕獲,接著我們找到test方法可以發(fā)現(xiàn)晰韵,test方法默認(rèn)傳遞了兩個(gè)參數(shù)self和_cmd栏尚。而類方法test2也同樣默認(rèn)傳遞了類對(duì)象self和方法選擇器_cmd。

test2.png

不論對(duì)象方法還是類方法都會(huì)默認(rèn)將self作為參數(shù)傳遞給方法內(nèi)部纵菌,既然是作為參數(shù)傳入,那么self肯定是局部變量序苏。上面講到局部變量肯定會(huì)被block捕獲。

接著我們來(lái)看一下如果在block中使用成員變量或者調(diào)用實(shí)例的屬性會(huì)有什么不同的結(jié)果匈睁。

- (void)test
{
    void(^block)(void) = ^{
        NSLog(@"%@",self.name);
        NSLog(@"%@",_name);
    };
    block();
}
block中使用成員變量或者調(diào)用實(shí)例的屬性.png

上圖中可以發(fā)現(xiàn),即使block中使用的是實(shí)例對(duì)象的屬性佛点,block中捕獲的仍然是實(shí)例對(duì)象,并通過(guò)實(shí)例對(duì)象通過(guò)不同的方式去獲取使用到的屬性演闭。

block的類型

block對(duì)象是什么類型的窝革,之前稍微提到過(guò)虐译,通過(guò)源碼可以知道block中的isa指針指向的是_NSConcreteStackBlock類對(duì)象地址锣枝。那么block是否就是_NSConcreteStackBlock類型的呢供鸠?

我們通過(guò)代碼用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;
}

打印內(nèi)容

block本質(zhì).png

從上述打印內(nèi)容可以看出block最終都是繼承自NSBlock類型泡一,而NSBlock繼承于NSObjcet涵但。那么block其中的isa指針其實(shí)是來(lái)自NSObject中的瞳脓。這也更加印證了block的本質(zhì)其實(shí)就是OC對(duì)象。

block的3種類型

__NSGlobalBlock__ ( _NSConcreteGlobalBlock )
__NSStackBlock__ ( _NSConcreteStackBlock )
__NSMallocBlock__ ( _NSConcreteMallocBlock )

通過(guò)代碼查看一下block在什么情況下其類型會(huì)各不相同

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

通過(guò)打印內(nèi)容確實(shí)可以發(fā)現(xiàn)block的三種類型

block的三種類型.png

但是我們上面提到過(guò)澈侠,上述代碼轉(zhuǎn)化為c++代碼查看源碼時(shí)卻發(fā)現(xiàn)block的類型與打印出來(lái)的類型不一樣劫侧,c++源碼中三個(gè)block的isa指針全部都指向_NSConcreteStackBlock類型地址。

我們可以猜測(cè)runtime運(yùn)行時(shí)過(guò)程中也許對(duì)類型進(jìn)行了轉(zhuǎn)變哨啃。最終類型當(dāng)然以runtime運(yùn)行時(shí)類型也就是我們打印出的類型為準(zhǔn)烧栋。

block在內(nèi)存中的存儲(chǔ)
通過(guò)下面一張圖看一下不同block的存放區(qū)域

應(yīng)用程序的內(nèi)存分配.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是如何定義其類型

block是如何定義其類型,依據(jù)什么來(lái)為block定義不同的類型并分配在不同的空間呢较性?首先看下面一張圖

block類型.png

接著我們使用代碼驗(yàn)證上述問(wèn)題,首先關(guān)閉ARC回到MRC環(huán)境下秀姐,因?yàn)锳RC會(huì)幫助我們做很多事情,可能會(huì)影響我們的觀察扫步。

// MRC環(huán)境T?扌浮古涧!
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // Global:沒(méi)有訪問(wèn)auto變量:__NSGlobalBlock__
        void (^block1)(void) = ^{
            NSLog(@"block1---------");
        };   
        // Stack:訪問(wèn)了auto變量: __NSStackBlock__
        int a = 10;
        void (^block2)(void) = ^{
            NSLog(@"block2---------%d", a);
        };
        NSLog(@"%@ %@", [block1 class], [block2 class]);
        // __NSStackBlock__調(diào)用copy : __NSMallocBlock__
        NSLog(@"%@", [[block2 copy] class]);
    }
    return 0;
}

查看打印內(nèi)容

block本質(zhì).png
通過(guò)打印的內(nèi)容可以發(fā)現(xiàn)正如上圖中所示奖亚。

沒(méi)有訪問(wèn)auto變量的block是__NSGlobalBlock__類型的咏尝,存放在數(shù)據(jù)段中舵揭。
訪問(wèn)了auto變量的block是__NSStackBlock__類型的据忘,存放在棧中。
__NSStackBlock__類型的block調(diào)用copy成為_(kāi)_NSMallocBlock__類型并被復(fù)制存放在堆中。

上面提到過(guò)__NSGlobalBlock__類型的我們很少使用到,因?yàn)槿绻恍枰L問(wèn)外界的變量,
直接通過(guò)函數(shù)實(shí)現(xiàn)就可以了,不需要使用block。

但是__NSStackBlock__訪問(wèn)了aotu變量逼蒙,并且是存放在棧中的,上面提到過(guò)微猖,
棧中的代碼在作用域結(jié)束之后內(nèi)存就會(huì)被銷毀谈息,那么我們很有可能block內(nèi)存銷毀之后才去調(diào)用他,
那樣就會(huì)發(fā)生問(wèn)題凛剥,通過(guò)下面代碼可以證實(shí)這個(gè)問(wèn)題侠仇。

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

此時(shí)查看打印內(nèi)容

block本質(zhì).png
可以發(fā)現(xiàn)a的值變?yōu)榱瞬豢煽氐囊粋€(gè)數(shù)字。為什么會(huì)發(fā)生這種情況呢犁珠?
因?yàn)樯鲜龃a中創(chuàng)建的block是__NSStackBlock__類型的逻炊,因此block是存儲(chǔ)在棧中的,
那么當(dāng)test函數(shù)執(zhí)行完畢之后犁享,棧內(nèi)存中block所占用的內(nèi)存已經(jīng)被系統(tǒng)回收余素,
因此就有可能出現(xiàn)亂得數(shù)據(jù)。查看其c++代碼可以更清楚的理解饼疙。
c++代碼.png
為了避免這種情況發(fā)生溺森,可以通過(guò)copy將__NSStackBlock__類型的block轉(zhuǎn)化為_(kāi)_NSMallocBlock__類型的block慕爬,
將block存儲(chǔ)在堆中窑眯,以下是修改后的代碼屏积。

void (^block)(void);
void test()
{
    // __NSStackBlock__ 調(diào)用copy 轉(zhuǎn)化為_(kāi)_NSMallocBlock__
    int age = 10;
    block = [^{
        NSLog(@"block---------%d", age);
    } copy];
    [block release];
}

此時(shí)在打印就會(huì)發(fā)現(xiàn)數(shù)據(jù)正確

block本質(zhì).png

那么其他類型的block調(diào)用copy會(huì)改變block類型嗎?下面表格已經(jīng)展示的很清晰了磅甩。

block類型.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幫我們做了什么

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

什么情況下ARC會(huì)自動(dòng)將block進(jìn)行一次copy操作? 以下代碼都在RAC環(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;
}

看一下打印的內(nèi)容

block本質(zhì).png
上文提到過(guò)宛蚓,如果在block中訪問(wèn)了auto變量時(shí)激捏,block的類型為_(kāi)_NSStackBlock__,
上面打印內(nèi)容發(fā)現(xiàn)blcok為_(kāi)_NSMallocBlock__類型的凄吏,并且可以正常打印出a的值珍促,
說(shuō)明block內(nèi)存并沒(méi)有被銷毀。

上面提到過(guò)专钉,block進(jìn)行copy操作會(huì)轉(zhuǎn)化為_(kāi)_NSMallocBlock__類型刮刑,來(lái)講block復(fù)制到堆中,
那么說(shuō)明RAC在 block作為函數(shù)返回值時(shí)會(huì)自動(dòng)幫助我們對(duì)block進(jìn)行copy操作盖喷,以保存block爆办,
并在適當(dāng)?shù)牡胤竭M(jìn)行release操作。

2. 將block賦值給__strong指針時(shí)
block被強(qiáng)指針引用時(shí)课梳,RAC也會(huì)自動(dòng)對(duì)block進(jìn)行一次copy操作距辆。

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

查看打印內(nèi)容可以看出暮刃,當(dāng)block被賦值給__strong指針時(shí)跨算,RAC會(huì)自動(dòng)進(jìn)行一次copy操作。

block本質(zhì).png

3. block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時(shí)
例如:遍歷數(shù)組的block方法椭懊,將block作為參數(shù)的時(shí)候诸蚕。

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

4. block作為GCD API的方法參數(shù)時(shí)
例如:GDC的一次性函數(shù)或延遲執(zhí)行的函數(shù),執(zhí)行完block操作之后系統(tǒng)才會(huì)對(duì)block進(jìn)行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聲明寫(xiě)法

通過(guò)上面對(duì)MRC及ARC環(huán)境下block的不同類型的分析背犯,總結(jié)出不同環(huán)境下block屬性建議寫(xiě)法坏瘩。
MRC下block屬性的建議寫(xiě)法
@property (copy, nonatomic) void (^block)(void);
ARC下block屬性的建議寫(xiě)法
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);

注: 問(wèn)題2、4沒(méi)有解答完

本章完漠魏。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末倔矾,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子柱锹,更是在濱河造成了極大的恐慌哪自,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件禁熏,死亡現(xiàn)場(chǎng)離奇詭異壤巷,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)瞧毙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門胧华,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人升筏,你說(shuō)我怎么就攤上這事撑柔。” “怎么了您访?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵铅忿,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我灵汪,道長(zhǎng)檀训,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任享言,我火速辦了婚禮峻凫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘览露。我一直安慰自己荧琼,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布差牛。 她就那樣靜靜地躺著命锄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪偏化。 梳的紋絲不亂的頭發(fā)上脐恩,一...
    開(kāi)封第一講書(shū)人閱讀 51,737評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音侦讨,去河邊找鬼驶冒。 笑死苟翻,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的骗污。 我是一名探鬼主播崇猫,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼身堡!你這毒婦竟也來(lái)了邓尤?” 一聲冷哼從身側(cè)響起拍鲤,我...
    開(kāi)封第一講書(shū)人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤贴谎,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后季稳,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體擅这,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有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
  • 文/蒙蒙 一勉痴、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧树肃,春花似錦蒸矛、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至筛谚,卻和暖如春磁玉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背驾讲。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工蚊伞, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留席赂,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓时迫,卻偏偏與公主長(zhǎng)得像颅停,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子掠拳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

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