block詳解

毫不夸張地說,block讓objc這門語言變得更有魅力,它就是在其它語言中常見的閉包的概念。
在block之前,objc重度依賴delegate來完成一些用戶行為,是block讓我們開發(fā)者多了一個(gè)
更簡(jiǎn)單的選擇,本文就《objc高級(jí)編程》來總結(jié)一下block的實(shí)現(xiàn)原理。

什么是block

用書中的話來概括block,就是:帶自動(dòng)變量的匿名函數(shù)。block可以有很多理解,它可以被
看作一個(gè)代碼塊,這個(gè)代碼塊即是匿名函數(shù)的函數(shù)體,它和普通的函數(shù)一樣可以有參數(shù),可
以有返回值,由此可見,block在使用上除了沒有函數(shù)名之外和普通的函數(shù)沒有區(qū)別,但是在
這里要注意區(qū)分函數(shù)和方法的區(qū)別。其帶有自動(dòng)變量,即是它可以保持block聲明作用域內(nèi)變
量的值,無論把這個(gè)block拿到哪里去執(zhí)行,這些變量的值都為你保存著。這些都是顯然的閉
包的概念,通過block來深入理解objc(甚至是其它語言)的閉包未嘗不是一個(gè)好的方法。

雖然使用和理解block給接觸objc不久的開發(fā)者帶來了困擾,但是block的語法卻很簡(jiǎn)單,通
過下面三個(gè)實(shí)例概括一下block的使用:

  1. 定義:
typedef void (^RFAudioBasicBlock) (void);
typedef void (^RFAudioSuccessBlock) (BOOL flag);
typedef void (^RFAudioSuccessDetailBlock) (BOOL flag, NSURL *url, NSTimeInterval duration);
typedef void (^RFAudioSuccessURLBlock) (BOOL flag, NSURL *url);
  1. 作為參數(shù):
- (void)playWithURL:(NSURL *)url finishedBlock:(RFAudioSuccessDetailBlock)block;
  1. 使用:
[[RFAudioManager defaultManager] playWithURL:url finishedBlock:^(BOOL flag, NSURL *url) {
NSLog(@"播放結(jié)束:%@", url);
}];

block的本質(zhì)與實(shí)現(xiàn)

直接通過代碼來分析block的實(shí)現(xiàn)比較有說服力:

  • objc源碼
int main {
void (^blk)(void) = ^{printf("Block/n");};
blk();
return 0;
}
  • 編譯之后的代碼
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself){
printf("Block/n");
}

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

int main() {
void (^blk)(void) = (void (*)(void))&__main_block_impl_0(
(void *)__main_block_func_0, &__main_block_desc_0_DATA
);

( (void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr )(
(struct __block_impl *)blk
);
}

這段代碼是我照著PDF文檔手打出來的,等我打出來之后也就明白了這段代碼的意思,不信你
可以試試。由源代碼我們可以清晰地看到:

  1. Block就是__main_block_impl_0結(jié)構(gòu)體指針類型的變量blk,即棧上生成的__main_block_impl_0
    結(jié)構(gòu)體實(shí)例(對(duì)象)。
  2. __block_impl即為Block的類信息,理解這里的類信息需要借助與objc對(duì)象與類之間的關(guān)系。
    里面的isa變量我們很熟悉,標(biāo)識(shí)了__block_impl的元類型NSConcreteStackBlock(其實(shí)標(biāo)識(shí)了
    該對(duì)象所處的位置在stack中)。
  3. Block如何截獲自動(dòng)變量,再簡(jiǎn)單不過了,將自動(dòng)變量保存在__main_block_impl_0對(duì)象中即可。

由此,我們可以下結(jié)論:Block就是一個(gè)Objective-c對(duì)象。

__block說明符

Block能夠輕松地截獲自動(dòng)變量的值,并在其調(diào)用內(nèi)使用它,然而卻不能更改它,如果需要在
Block內(nèi)部更改外部的變量值,需要加上__block修飾符。咋一看,這個(gè)要求很簡(jiǎn)單,在Block
對(duì)象中保存變量的指針不就完事了嗎?然而實(shí)際情況卻不是,在Block調(diào)用的時(shí)候,自動(dòng)變量
可能早已超出其作用域被銷毀了,也就不能通過指針來訪問自動(dòng)變量了。看代碼:

  • objc代碼
__block int val = 10;

*編譯之后的代碼

struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int flags;
int __size;
int val;
};

int main() {
__Block_byref_val_0 val = {
0,
&val,
0,
sizeof(__Block_byref_val_0),
10
}
}

由上,我們不難看出所有被__block修飾的變量均會(huì)變成一個(gè)結(jié)構(gòu)體(即對(duì)象),所以在block
中改變其值并不難,然而即便是對(duì)象也并沒有解決超出作用域被收回的問題,下面我們來探討
Block及__block變量超出作用域而存在的道理。

Block存儲(chǔ)域

分解上述的isa指針,Block分為三種類型:

  • _NSConcreteGlobalBlock
    通過名稱就可以判斷出此Block為全局的,處于程序的數(shù)據(jù)區(qū),生成此種類型的Block有
    兩種情況:
  1. 記述全局變量的地方有Block語法
  2. Block語法的表達(dá)式中不使用應(yīng)該截獲的自動(dòng)變量時(shí)
    此種類型的Block就如同全局變量一樣,在全局環(huán)境下通過指針安全地使用。
  • _NSConcreteStackBlock
    當(dāng)Block截獲自動(dòng)變量的時(shí)候,其所處的區(qū)域在于Stack中。設(shè)置在棧上的Block,如果其
    所屬的作用域結(jié)束,該Block即會(huì)被廢棄;同理__block類型的變量也配置在棧上,如果
    其所屬的變量作用域結(jié)束,該__block變量也會(huì)被廢棄。

  • _NSConcreteMallocBlock
    大部分Block的結(jié)構(gòu)體實(shí)例均是在棧上,如果在實(shí)例作用域結(jié)束的時(shí)候,要保留這個(gè)Block
    就需要將其復(fù)制到堆上,如果堆上的Block引用也過期,就會(huì)被收回。下面探討將Block
    從棧復(fù)制到堆的情形。

typedef int (^blk_t)(int);

blk_t func(int rate) {
return ^(int count){return rate * count;};
}

該Block即是屬于Stack上的實(shí)例變量,當(dāng)函數(shù)返回的時(shí)候,Block也相繼被廢棄,但是此Block
作為函數(shù)的返回值,編譯器會(huì)自動(dòng)生成將Block復(fù)制到堆上的代碼。一般情況下,編譯器會(huì)自行
判斷需要將Block復(fù)制到堆上的情形,但是也存在編譯器不能判斷的場(chǎng)景:

  • 向方法或者函數(shù)的參數(shù)中傳遞Block時(shí)需要手動(dòng)復(fù)制Block。這就是很好地解釋了@property
    后面的類型為Block時(shí)需要申明copy,當(dāng)然申明strong/retain也不會(huì)出現(xiàn)問題,因?yàn)槠鋵?duì)于
    Block的默認(rèn)行為也是copy。

還有兩個(gè)我們不需要copy的場(chǎng)景:

  • Cocoa框架的方法且方法名中含有usingBlock等時(shí);
  • 使用GCD的API時(shí)

至此我們可以稍微總結(jié)一下Block被復(fù)制的情形:

  • 調(diào)用Block的copy實(shí)例方法時(shí)
  • Block作為函數(shù)返回值返回時(shí)
  • 將Block賦值給附有__strong修飾符的id類型的類或者Block類型的成員變量時(shí)
  • 方法名中含有usingBlock的cocoa框架方法或者使用GCD時(shí)

__block變量作用域

由上文可知,__block變量被轉(zhuǎn)化為結(jié)構(gòu)體實(shí)例,其實(shí)就是對(duì)象,其隨著持有它的Block一起被
復(fù)制到堆中或者被廢棄。當(dāng)其被復(fù)制之后,不管是棧中還是堆中,均可以訪問到該對(duì)象,并對(duì)其
值進(jìn)行的更改均有效,這是如何做到的呢?

在上文__Block_byref_val_0結(jié)構(gòu)體中有個(gè)_forwarding字段,其即為__block變量對(duì)象的
指針,棧中對(duì)象的_forwarding實(shí)際上指向了堆中對(duì)象的地址,所以不管是在棧中還是
在堆中訪問這個(gè)變量均指向了同一個(gè)地方即堆中的對(duì)象。所以在Block內(nèi)外均能夠?qū)ζ?br> 進(jìn)行賦值和訪問。

截獲對(duì)象

有一種情況,如果非__block變量指向的是對(duì)象,那么我們依然得克服變量作用域失效而導(dǎo)致
對(duì)象被釋放的困境,以達(dá)到Block截獲自動(dòng)變量的目的。這個(gè)問題也似乎很好解決,只需要在
Block中帶入該自動(dòng)變量的修飾符(strong/weak),于是Block對(duì)象中保持了對(duì)自動(dòng)變量指向
的對(duì)象的強(qiáng)引用,那么目標(biāo)對(duì)象即不會(huì)被釋放,這樣就達(dá)到了Block持有這個(gè)對(duì)象的目的。
實(shí)際上蘋果也是這么做的,只是這個(gè)修飾符在Block被copy的時(shí)候才會(huì)生效,也即要求Block
對(duì)象處于堆中,如果該Block并沒有被copy,那么自動(dòng)變量的修飾符會(huì)丟失,自動(dòng)變量所指
向的對(duì)象也會(huì)因?yàn)槌鲎饔糜蚨会尫拧?/p>

如果__block變量指向的是對(duì)象,情況與非block變量類似。

最后

提醒:使用block是循環(huán)引用的高發(fā)區(qū),所以要小心杜絕循環(huán)引用引起的內(nèi)存泄露。

最后編輯于
?著作權(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)容

  • 轉(zhuǎn)自李峰峰博客 一、概述 閉包 = 一個(gè)函數(shù)「或指向函數(shù)的指針」+ 該函數(shù)執(zhí)行的外部的上下文變量「也就是自由變量」...
    Joshua520閱讀 1,018評(píng)論 0 0
  • 1、概述 閉包 = 一個(gè)函數(shù)「或指向函數(shù)的指針」+ 該函數(shù)執(zhí)行的外部的上下文變量「也就是自由變量」;Block 是...
    DeerRun閱讀 682評(píng)論 0 0
  • Blocks Blocks Blocks 是帶有局部變量的匿名函數(shù) 截取自動(dòng)變量值 int main(){ ...
    南京小伙閱讀 964評(píng)論 1 3
  • Block基礎(chǔ)回顧 1.什么是Block? 帶有局部變量的匿名函數(shù)(名字不重要,知道怎么用就行),差不多就與C語言...
    Bugfix閱讀 6,801評(píng)論 5 61
  • 2.1 Blcoks概要 2.1.1 什么是Blocks Blocks是C語言的擴(kuò)充功能——“帶有自動(dòng)變量(即局部...
    SkyMing一C閱讀 2,378評(píng)論 6 18