學(xué)會(huì)使用Objective-C中的block

Apple從OS X 10.4和iOS 4以后開(kāi)始支持block,相對(duì)于delegate,block有很多便捷之處,使得代碼更簡(jiǎn)潔,可讀性更強(qiáng)。但是如果使用不當(dāng),則會(huì)造成很多問(wèn)題。本文結(jié)合自己的經(jīng)驗(yàn)和《Pro Multithreading and Memory Management for iOS and OS X: with ARC, Grand Central Dispatch, and Blocks》書(shū)中的知識(shí)點(diǎn),介紹block的相關(guān)知識(shí)點(diǎn)。

block語(yǔ)法

我們通過(guò)以下圖來(lái)了解block的語(yǔ)法,圖片來(lái)自這里

block語(yǔ)法結(jié)構(gòu)圖

我們來(lái)看看上面的圖,代碼如下

int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    return num * multiplier;
};

根據(jù)圖中的解釋,我們從左向右來(lái)看,該block返回值為int類(lèi)型,'^'符號(hào)聲明一個(gè)名為myblock的block,該block有一個(gè)int類(lèi)型的入?yún)ⅲ忍?hào)右邊則為block的定義,block有一個(gè)名為num的int類(lèi)型的入?yún)ⅲ?code>{return num * multiplier;};則為該block的block實(shí)現(xiàn)部分。

該block的調(diào)用方法如下,看起來(lái)像C的函數(shù)調(diào)用。

int result =  myBlock(2); //reslut = 14;

我們來(lái)看看復(fù)雜一點(diǎn)的情況:

- (void)startWithBlock:(void(^)())block {
    block();
}

- (void)testBlock {
    NSString *strBlock = @"NSStackBlock";
    [self startWithBlock:^{
        NSLog(@"%@",strBlock);
    }];
}

控制臺(tái)輸出

NSStackBlock

以上代碼,新手看起來(lái)可能是會(huì)有些費(fèi)勁的。我們一步一步來(lái),首先,我們調(diào)用testBlock函數(shù),在該函數(shù)中,

^{
     NSLog(@"%@",strBlock);
 }];

該代碼塊實(shí)際上是傳給了startWithBlock函數(shù)的參數(shù)block,當(dāng)執(zhí)行startWithBlock函數(shù)時(shí),調(diào)用block(),實(shí)際上就是執(zhí)行了以上代碼塊。

使用typedef定義block類(lèi)型
以上代碼可以通過(guò)typedef來(lái)定義block,以便閱讀,如下

typedef int (^myBlock)(int num);

在定義某個(gè)block類(lèi)型時(shí),可以使用

myBlock aBlock = ^(int num) {
    //Implemention
};   

這樣看起來(lái),要比之前簡(jiǎn)單得多。

block捕獲外部變量

block內(nèi)可以訪問(wèn)block之前定義的變量:

int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    return num * multiplier;
};
int result =  myBlock(2); //reslut = 14;

但是,如果想在block內(nèi)部改變multiplier的值,編輯器則會(huì)報(bào)錯(cuò)

int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    multiplier = 5;
    return num * multiplier;
};

編輯器會(huì)提示: 變量不能被賦值,需要加上__block修飾符

error: variable is not assignable (missing __block type specifier)

此時(shí),需要將該變量使用__block修飾:

__block int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    multiplier = 5;
    return num * multiplier;
};

如果multiplier變量是static、static global或者global變量,則不需要添加__block,該值也是可以在block內(nèi)部修改的。

static int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    multiplier = 5;
    return num * multiplier;
};

因?yàn)閟tatic、static global或者global變量都是存儲(chǔ)在內(nèi)存中的全局區(qū)(靜態(tài)區(qū)),對(duì)于這三種類(lèi)型變量,block內(nèi)部是捕獲了其指針,則可以直接訪問(wèn)修改;而對(duì)于之前的臨時(shí)變量,block則只是捕獲了該變量的值,無(wú)法修改到外部的變量。

block內(nèi)部還可以訪問(wèn)類(lèi)的實(shí)例變量和self變量

@interface EOCClass : NSObject 
@property (nonatomic, copy) NSString *anInstanceVariable;
@end

@implementation EOCClass

- (void)anInstanceMethod {
    
    void (^someBlock)() = ^ {
        self.anInstanceVariable = @"Something";
    };
    someBlock();
    NSLog(@"self.aninstanceVaraible = %@", self.anInstanceVariable);
    //self.aninstanceVaraible = Something
}

@end

block的內(nèi)部結(jié)構(gòu)

block 的數(shù)據(jù)結(jié)構(gòu)定義如下(圖片來(lái)自 這里):

block內(nèi)存布局

對(duì)應(yīng)的結(jié)構(gòu)體定義如下:

struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

從上面代碼看出,一個(gè) block 實(shí)例實(shí)際上由 6 部分構(gòu)成:

  • isa指針:指向該block類(lèi)型的類(lèi)的指針,
    每個(gè)Objective-C對(duì)象,都有一個(gè)isa指針,指向?qū)ο蟮念?lèi),而Class里也有個(gè)isa的指針, 指向meteClass(元類(lèi))。元類(lèi)保存了類(lèi)方法的列表。元類(lèi)也有isa指針,它的isa指針最終指向的是一個(gè)根元類(lèi)(root meteClass)。根元類(lèi)的isa指針指向本身。如下圖

    圖片來(lái)自《Pro Multithreading and Memory Management for iOS and OS X: with ARC, Grand Central Dispatch, and Blocks》

    對(duì)于block,isa指針可以指向
    _NSConcreteStackBlock_NSConcreteMallocBlock_NSConcreteGlobalBlock這三種類(lèi)型

  • flags:按bit位表示一些block的附加信息,比如判斷block類(lèi)型、判斷block引用計(jì)數(shù)、判斷block是否需要執(zhí)行輔助函數(shù)等。

  • reserved:保留變量,我的理解是表示block內(nèi)部的變量數(shù)。

  • invoke:函數(shù)指針,指向block的實(shí)現(xiàn)代碼地址。

  • descriptor:指向結(jié)構(gòu)體的指針,block的附加描述信息,比如保留變量數(shù)、block的大小、copy和dispose輔助函數(shù)的函數(shù)指針指針
    copy函數(shù)為當(dāng)block執(zhí)行copy操作或者當(dāng)block從棧上拷貝到堆上時(shí)調(diào)用,dispose函數(shù)則是block在堆上釋放時(shí)調(diào)用

  • variables:block內(nèi)部捕獲的對(duì)象,如

void (^blk)(void) = ^{print(fmt,val)};

此時(shí),variables中則為fmt和val這兩個(gè)變量

block的類(lèi)型

block有_NSConcreteStackBlock_NSConcreteMallocBlock_NSConcreteGlobalBlock這三種類(lèi)型。
三種block在內(nèi)存中存儲(chǔ)位置如下圖

圖片來(lái)自《Pro Multithreading and Memory Management for iOS and OS X: with ARC, Grand Central Dispatch, and Blocks》
block類(lèi)型的區(qū)分

以下情況,block為_NSConcreteGlobalBlock類(lèi)型

  • block內(nèi)部只使用了全局變量
  • block內(nèi)部沒(méi)有使用任何外部的局部變量

除了以上兩種情況,其他的block為_NSConcreteStackBlock類(lèi)型。
而對(duì)于_NSConcreteMallocBlock,只有當(dāng)_NSConcreteStackBlock類(lèi)型的block執(zhí)行copy操作(手動(dòng)或者系統(tǒng)執(zhí)行)時(shí),該block才會(huì)是_NSConcreteMallocBlock類(lèi)型

我們來(lái)看看代碼,直觀的看看這三種類(lèi)型的block

  1. _NSConcreteGlobalBlock
    由類(lèi)型名字可以得知,該block是存儲(chǔ)在在內(nèi)存中全局區(qū)的。
void (^block)() = ^{
    NSLog(@"This is a global block");
};
NSLog(@"%@",block);

控制臺(tái)輸出

<__NSGlobalBlock__: 0x104fd52d0>

或者

int globalVal = 1; //此處為全局變量
int (^myBlock)(int) = ^(int num) {
    return num * globalVal;
};
NSLog(@"%@",block);

控制臺(tái)輸出

<__NSGlobalBlock__: 0x104fd5310>

該block所需要的全部信息都能在編譯期確定。該block是全局存在的,相當(dāng)于單例了。

  1. _NSConcreteStackBlock
    由該類(lèi)型的名字可以看出,該block所占的內(nèi)存區(qū)域是分配在棧(stack)中的。也就是說(shuō),塊只在定義它的那個(gè)范圍內(nèi)(作用域)內(nèi)有效。如下面代碼:
int multiplier = 7;
NSLog(@"%@",^(int num) {
    return num * multiplier;
};);

控制臺(tái)輸出

<__NSStackBlock__: 0x7fff59615a18>

以上代碼,block內(nèi)部捕獲了multiplier這個(gè)外部的局部變量,所以是_NSConcreteStackBlock類(lèi)型。
因?yàn)樵揵lock存在在棧上,在超過(guò)block的作用域時(shí),該block就會(huì)被系統(tǒng)釋放,就有可能會(huì)出現(xiàn)block內(nèi)部的代碼還沒(méi)有走完,就被釋放掉的情況。對(duì)于這種情況,應(yīng)該對(duì)block執(zhí)行copy操作,將block復(fù)制到堆上。
注:在ARC下,系統(tǒng)在大部分情況下,會(huì)將block從棧上復(fù)制到堆上,這個(gè)后面會(huì)細(xì)說(shuō)

  1. _NSConcreteMallocBlock
    對(duì)以上代碼中的block執(zhí)行copy操作,block就變成了_NSConcreteMallocBlock類(lèi)型,如下
int multiplier = 7;
NSLog(@"mallocBlock:%@",[^(int num) {
    return num * multiplier;
} copy]);

控制臺(tái)輸出

<__NSMallocBlock__: 0x6000000486a0>

拷貝到堆后,block的生命周期就與一般的OC對(duì)象一樣了。

ARC 下 block 的自動(dòng)拷貝和手動(dòng)拷貝

ARC下,以下幾種情況,系統(tǒng)會(huì)將block從棧上自動(dòng)復(fù)制到堆上

  • 當(dāng) block 作為函數(shù)返回值返回時(shí);
  • 當(dāng) block 被賦值給__strong修飾的 id 類(lèi)型的對(duì)象或 block 對(duì)象時(shí);
  • 當(dāng) block 作為參數(shù)被傳入方法名帶有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 時(shí)(比如使用NSArray的enumerateObjectsUsingBlock和GCD的dispatch_async方法時(shí),其block不需要我們手動(dòng)執(zhí)行copy操作)
    注:系統(tǒng)方法內(nèi)部對(duì)block進(jìn)行了copy操作

因?yàn)樵贏RC下,對(duì)象默認(rèn)是用__strong修飾的,所以大部分情況下編譯器都會(huì)將 block從棧自動(dòng)復(fù)制到堆上,除了以下情況

  • block 作為方法或函數(shù)的參數(shù)傳遞時(shí),編譯器不會(huì)自動(dòng)調(diào)用 copy 方法;
  • block 作為臨時(shí)變量,沒(méi)有賦值給其他block
看看代碼
block作為函數(shù)的返回值,如下
- (void(^)())blockReturn {
    NSString *strBlock = @"NSMallocBlock";
    return ^(){
        NSLog(@"%@",strBlock);
    };
}
NSLog(@"%@",[self blockReturn]);

控制臺(tái)輸出

<__NSMallocBlock__: 0x7fa161f081f0>
block賦值給強(qiáng)引用block
typedef void(^block)();

NSString *strBlock = @"NSMallocBlock";
block mallocBlock = ^(){
    NSLog(@"%@",strBlock);
};
NSLog(@"%@",mallocBlock);

控制臺(tái)輸出

<__NSMallocBlock__: 0x7fedd0d26110>
將block作為臨時(shí)變量
NSString *strBlock = @"NSStackBlock";
NSLog(@"%@",^(){
    NSLog(@"%@",strBlock);
});    

控制臺(tái)輸出

<__NSStackBlock__: 0x7fff563aa9b0>
block作為函數(shù)參數(shù)
- (void)startWithBlock:(void(^)())block {
    NSLog(@"%@",block);
}

- (void)testBlock {
    NSString *strBlock = @"NSStackBlock";
    [self startWithBlock:^{
        NSLog(@"%@",strBlock);
    }];
}

執(zhí)行testBlock方法,控制臺(tái)輸出

<__NSStackBlock__: 0x7fff563aa988>

此處可能會(huì)有疑問(wèn):既然當(dāng)block作為函數(shù)參數(shù)時(shí)為_NSConcreteStackBlock類(lèi)型,超出其作用域時(shí),block會(huì)被釋放掉,那會(huì)不會(huì)出現(xiàn)函數(shù)先退出了,block還是沒(méi)有執(zhí)行完畢的?

經(jīng)過(guò)我測(cè)試,我發(fā)現(xiàn),其實(shí)在函數(shù)中,在block執(zhí)行完畢前,函數(shù)是不會(huì)退出的。因?yàn)楹瘮?shù)中按順序執(zhí)行的,函數(shù)中block后的代碼會(huì)等待block執(zhí)行完畢,所以在block塊代碼未執(zhí)行完畢時(shí),該函數(shù)不會(huì)退出,從而沒(méi)有超過(guò)block的作用域,block不會(huì)被釋放。看下面的例子就可以明白了。

- (void)startWithBlock:(void(^)())block {
    block();
    NSLog(@"%@",block);
}
- (void)testBlock {
    NSString *strBlock = @"NSStackBlock";
    [self startWithBlock:^{
        NSLog(@"%@",strBlock);
    }];
}

控制臺(tái)輸出

NSStackBlock
<__NSStackBlock__: 0x7fff54ba4a20>

從打印結(jié)果可以看出,當(dāng)我們執(zhí)行startWithBlock函數(shù)時(shí),先是執(zhí)行了block內(nèi)的代碼,再是執(zhí)行函數(shù)中block后的代碼,所以可以保證block執(zhí)行完畢。

可能,還有人會(huì)問(wèn),如果把block()放在子線程中執(zhí)行呢,這樣就不是按順序執(zhí)行了,在block塊代碼執(zhí)行之前,函數(shù)就退出了,這樣是不是block就不能執(zhí)行完畢呢?

其實(shí),把block放子線程中,無(wú)非是通過(guò)GCD和performSelectorInBackground方法,系統(tǒng)會(huì)自動(dòng)GCD的block進(jìn)copy操作,而performSelectorInBackground需要傳一個(gè)selector,又相當(dāng)于走進(jìn)了函數(shù)里,還是按順序執(zhí)行了,函數(shù)還是會(huì)等待block執(zhí)行完畢。

針對(duì)不同block類(lèi)型的copy、retain、release操作

  • 對(duì)block不管是retain、copy、release都不會(huì)改變引用計(jì)數(shù)retainCount,retainCount始終是1;
  • 針對(duì)NSConcreteGlobalBlock:retain、copy、release操作都無(wú)效;
  • 針對(duì)NSConcreteStackBlock:retain、release操作無(wú)效
    注意的是,NSConcreteStackBlock離開(kāi)其作用域后,該block內(nèi)存將被回收,即使retain也沒(méi)用。容易犯的錯(cuò)誤是[[mutableAarry addObject:stackBlock],在stackBlock離開(kāi)其作用域失效后,從mutableAarry中取到的stackBlock已經(jīng)被回收,變成了野指針。正確的做法是先將stackBlock copy到堆上,然后加入數(shù)組:[mutableAarry addObject:[stackBlock copy]]。
  • NSConcreteMallocBlock支持retain、release,雖然retainCount始終是1,但內(nèi)存管理器中仍然會(huì)增加、減少計(jì)數(shù)。copy之后不會(huì)生成新的對(duì)象,只是增加了一次引用,類(lèi)似retain;

注:盡量不要對(duì)block使用retain操作。因?yàn)閺纳峡梢钥闯觯瑀etain操作對(duì))_NSConcreteStackBlock并沒(méi)有效果,這樣會(huì)誤以為retain生效了,在后續(xù)調(diào)用block的時(shí)候,其實(shí)block早就被釋放了,從而導(dǎo)致crash

block循環(huán)引用問(wèn)題

可以使用__weak__unsafe_unretained__block修飾詞修飾被block持有的對(duì)象來(lái)打破循環(huán),還有就是在block執(zhí)行完畢的時(shí)候,將block置nil的方法。具體細(xì)節(jié)這里就不講了,有興趣的童鞋可以看看我簡(jiǎn)書(shū)上寫(xiě)了另一篇文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評(píng)論 6 538
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,034評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 177,413評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,449評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,165評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,559評(píng)論 1 325
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,781評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,327評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,084評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,278評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,495評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,927評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,172評(píng)論 1 291
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,010評(píng)論 3 396
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,241評(píng)論 2 375

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

  • 一、Objective-C發(fā)展史 Objective-C從1983年誕生,已經(jīng)走過(guò)了30多年的歷程。隨著時(shí)間的推移...
    沒(méi)事蹦蹦閱讀 5,857評(píng)論 12 34
  • 《Objective-C高級(jí)編程》這本書(shū)就講了三個(gè)東西:自動(dòng)引用計(jì)數(shù)、block、GCD,偏向于從原理上對(duì)這些內(nèi)容...
    WeiHing閱讀 9,863評(píng)論 10 69
  • 要了解Block需要先了解什么是“閉包性” 2.3 閉包性 上文說(shuō)過(guò),block實(shí)際是Objc對(duì)閉包的實(shí)現(xiàn)。 我們...
    隨風(fēng)飄蕩的小逗逼閱讀 353評(píng)論 0 0
  • 前言 Blocks是C語(yǔ)言的擴(kuò)充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了這...
    小人不才閱讀 3,779評(píng)論 0 23
  • .相關(guān)概念 在這篇筆記開(kāi)始之前,我們需要對(duì)以下概念有所了解。 1.1 操作系統(tǒng)中的棧和堆 注:這里所說(shuō)的堆和棧與數(shù)...
    狼鳳皇閱讀 488評(píng)論 0 0