iOS開(kāi)發(fā)讀書(shū)筆記:Objective-C高級(jí)編程 iOS與OS X多線程和內(nèi)存管理-中篇(Blocks)

iOS開(kāi)發(fā)讀書(shū)筆記:Objective-C高級(jí)編程 iOS與OS X多線程和內(nèi)存管理-上篇(自動(dòng)引用計(jì)數(shù))
iOS開(kāi)發(fā)讀書(shū)筆記:Objective-C高級(jí)編程 iOS與OS X多線程和內(nèi)存管理-中篇(Blocks)
iOS開(kāi)發(fā)讀書(shū)筆記:Objective-C高級(jí)編程 iOS與OS X多線程和內(nèi)存管理-下篇(GCD)

閱讀完Block此篇后,可以與iOS開(kāi)發(fā)經(jīng)驗(yàn)(25)-Block一塊閱讀,主要是可以加深對(duì)__forwarding的理解。

目錄

  • 2.1 Blocks概要
    • 2.1.1 什么是Blocks
  • 2.2 Blocks模式
    • 2.2.1 Blocks語(yǔ)法
    • 2.2.2 Blocks類(lèi)型變量
    • 2.2.3 截獲自動(dòng)變量值
    • 2.2.4 __block說(shuō)明符
    • 2.2.5 截獲的自動(dòng)變量
  • 2.3 Blocks的實(shí)現(xiàn)
    • 2.3.1 Block的實(shí)質(zhì)
    • 2.3.2 截獲自動(dòng)變量值
    • 2.3.3 __block說(shuō)明符
    • 2.3.4 Block存儲(chǔ)域
    • 2.3.5 __block變量存儲(chǔ)域
    • 2.3.6 截獲對(duì)象
    • 2.3.7 __block變量和對(duì)象
    • 2.3.8 Block循環(huán)引用
    • 2.3.9 copy/release

2.1 Blocks概要

2.1.1 什么是Blocks

Blocks是C語(yǔ)言的擴(kuò)充功能。可以用一句話來(lái)表示Blocks的擴(kuò)充功能:帶有自動(dòng)變量(局部變量)的匿名函數(shù)。
顧名思義,所謂匿名函數(shù)就是不帶有名稱(chēng)的函數(shù)。
C語(yǔ)言的標(biāo)準(zhǔn)函數(shù)如下:

int func(int count);//聲明函數(shù)
int result = func(10);//調(diào)用函數(shù)

如果像下面這樣,使用函數(shù)指針來(lái)代替直接調(diào)用函數(shù),必須使用該函數(shù)的名稱(chēng)func。

int result = (*funcptr)(10);

這樣以來(lái),函數(shù)func的地址就能賦值給函數(shù)指針類(lèi)型變量funcptr中了。
但其實(shí)使用函數(shù)指針也仍然需要知道函數(shù)名稱(chēng)。若不使用想賦值的函數(shù)的名稱(chēng),就無(wú)法取得該函數(shù)的地址。

int (*funcptr)(int) = &func;
int result = (*funcptr)(10);

通過(guò)Blocks,源代碼中就能夠使用匿名函數(shù),而不帶名稱(chēng)的函數(shù)。
到這里我們知道了"帶有自動(dòng)變量值的匿名函數(shù)"中"匿名函數(shù)"的概念。那么“帶有自動(dòng)變量值”究竟是什么呢?
首先回顧下函數(shù)中可能使用的變量:

  • 自定變量(局部變量)
  • 函數(shù)的參數(shù)
  • 靜態(tài)變量(靜態(tài)局部變量)
  • 靜態(tài)全局變量
  • 全局變量

雖然這些變量的作用域不同,但在整個(gè)程序當(dāng)中,一個(gè)變量總保持在一個(gè)內(nèi)存區(qū)域。
另外,“帶有自動(dòng)變量值的匿名函數(shù)”這一概念并不僅指Blocks,它還存在于其他許多程序語(yǔ)言中。在計(jì)算機(jī)科學(xué)中,此概念也稱(chēng)為閉包。

2.2 Blocks模式

2.2.1 Blocks語(yǔ)法

與一般的函數(shù)定義相比,僅有兩點(diǎn)不同

  • 沒(méi)有函數(shù)名
  • 帶有“^”記號(hào)(插入記號(hào)):因?yàn)镺S X、iOS應(yīng)用程序的源代碼中將大量使用Block,所以插入該記號(hào)便于查找。
    以下為Block語(yǔ)法的BN范式
^ 返回值類(lèi)型 參數(shù)列表 表達(dá)式

^ int (int count) {
    return count + 1;
}

該源代碼可省略胃如下形式
^ {
    return count + 1;
}

2.2.2 Blocks類(lèi)型變量

在Block語(yǔ)法下,可將Block語(yǔ)法賦值給聲明為Block類(lèi)型的變量中。既源代碼中一旦使用Block語(yǔ)法就相當(dāng)于生成了可賦值給Block類(lèi)型變量的“值”。

int (^blk)(int);

與前面的使用函數(shù)指針的源代碼對(duì)比可知,聲明Block類(lèi)型變量?jī)H僅是將聲明函數(shù)指針類(lèi)型變量的“*”變?yōu)椤癪”。該Blcok類(lèi)型變量與一般的C語(yǔ)言變量完全相同,可作為以下用途使用 :

  • 自定變量(局部變量)
  • 函數(shù)的參數(shù)
  • 靜態(tài)變量(靜態(tài)局部變量)
  • 靜態(tài)全局變量
  • 全局變量

那么,下面我們就試著使用Block語(yǔ)法將Block賦值為Block類(lèi)型變量:

int (^blk) (int) = ^ (int count) {
    return count + 1;
}

也可以:

int (^blk1)(int) = blk;

但是此記述方式極為復(fù)雜。這時(shí),我們可以像使用函數(shù)指針類(lèi)型時(shí)那樣,使用typedef來(lái)解決問(wèn)題。

typedef int (^blk_t) (int);

如上所示,通過(guò)使用typedef可聲明“blk_t”類(lèi)型變量。這樣函數(shù)定義就變得更容易理解了。
另外,將賦值給Block類(lèi)型變量中的Block方法像C語(yǔ)言通常的函數(shù)調(diào)用那樣使用,這種方法與使用函數(shù)指針類(lèi)型變量調(diào)用函數(shù)的方法幾乎完全相同。

2.2.3 截獲自動(dòng)變量值

通過(guò)以上說(shuō)明,我們已經(jīng)理解了“帶有自動(dòng)變量值的匿名函數(shù)”中的“匿名函數(shù)”。而“帶有自動(dòng)變量值”究竟是什么呢?“帶有自動(dòng)變量值”在Block中表現(xiàn)為“截獲自動(dòng)變量值”。截獲自動(dòng)變量值的實(shí)例如下:

int main() {
    int val = 10;
    void (^blk)(void) = ^ {
         printf(val);
    }
    val = 2;
    blk();
    //打印結(jié)果為10;
}

Block中,Block表達(dá)式截獲所使用的自動(dòng)變量的值,既保持該自動(dòng)變量的瞬間值。這就是自動(dòng)變量值的截獲。

2.2.4 __block說(shuō)明符

實(shí)際上,自動(dòng)變量值截獲指南保持秩序Block語(yǔ)法瞬間的值。保存后就不能改寫(xiě)該值。如果嘗試在Block中改寫(xiě)截獲的自動(dòng)變量值,會(huì)發(fā)生編譯錯(cuò)誤。
若想在Block語(yǔ)法的表達(dá)式中將值賦值在Block語(yǔ)法外聲明的自動(dòng)變量,需要在該自動(dòng)變量上添加__block說(shuō)明符。該變量稱(chēng)為_(kāi)_block變量。

int main() {
    __block int val = 10;
    void (^blk)(void) = ^ {
        val = 1;
    }
    val = 2;
    blk();
    //打印結(jié)果為10;
}

2.2.5 截獲的自動(dòng)變量

截獲OC對(duì)象,調(diào)用變更該對(duì)象的方法不會(huì)產(chǎn)生編譯錯(cuò)誤,而向截獲的變量array賦值則會(huì)產(chǎn)生編譯錯(cuò)誤。

//編譯正常
id array = [[NSMutableArray alloc] init];
void (^blk) (void) = ^ {
    id obj  = [[NSObject alloc] init];
    [array addObject:obj];
}
//編譯錯(cuò)誤
id array = [[NSMutableArray alloc] init];
void (^blk) (void) = ^ {
    array = [[NSMutableArray alloc] init];
}

以上的第二段代碼需要給截獲的自動(dòng)變量附加__block說(shuō)明符。

2.3 Blocks的實(shí)現(xiàn)

2.3.1 Block的實(shí)質(zhì)

Block上“帶有自動(dòng)變值的匿名函數(shù)”,但Block究竟是什么呢?
它實(shí)際上是作為極普通的C語(yǔ)言源代碼來(lái)處理的,通過(guò)支持Block的編譯器,含有Block的編譯器,含有Block語(yǔ)法的源代碼轉(zhuǎn)換為一般C語(yǔ)言編譯器能夠處理的源代碼,并作為極普通的C語(yǔ)言源代碼被編譯。
clang(LLVM編譯器)具有轉(zhuǎn)換為我們可讀源代碼的功能。通過(guò)“-rewrite-objc”選項(xiàng)就能將含有Block語(yǔ)法的源代碼變換為C++的源代碼。

clang -rewrite-objc xxx.m

其實(shí),所謂Block就是Objective-C對(duì)象。Block指針賦值給Block的結(jié)構(gòu)體成員變量isa。

struct _main_block_impl_0 {
    void *isa;   
    int flags;
    int Reserved; 
    void *FuncPtr;   
};

此_main_block_impl_0結(jié)構(gòu)體相當(dāng)于基于objc_object結(jié)構(gòu)體的Objective-C類(lèi)對(duì)象的結(jié)構(gòu)體。另外,對(duì)其中的成員變量isa進(jìn)行初始化,具體如下:

isa = &_NSConcreteStackBlock;

既_NSConcreteStackBlock相當(dāng)于class_t結(jié)構(gòu)體實(shí)例。在將Block作為Objective-C的對(duì)象處理時(shí),關(guān)于該類(lèi)的信息放置于_NSConcreteStackBlock中;

2.3.2 截獲自動(dòng)變量值

本節(jié)主要講解如何截獲自動(dòng)變量值。將截獲自動(dòng)值的源代碼用過(guò)clang進(jìn)行轉(zhuǎn)換(源代碼略)。
我們注意到,Block語(yǔ)法表達(dá)式中使用的自動(dòng)變量被作為成員變量追加到了_main_block_impl_0結(jié)構(gòu)體中。

struct _main_block_impl_0 {
    struct __block_impl impl;   
    struct __main_block_desc_0 *Desc;   
    int var; 
};

_main_block_impl_0結(jié)構(gòu)體內(nèi)聲明的成員變量類(lèi)型與自動(dòng)變量類(lèi)型完全相同。請(qǐng)注意,Block語(yǔ)法表達(dá)式中沒(méi)有使用的自動(dòng)變量不會(huì)被追加。Block的自動(dòng)變量截獲只針對(duì)Block中使用的自動(dòng)變量。
總的來(lái)說(shuō),所謂“截獲自動(dòng)變量值”意味著在執(zhí)行Block語(yǔ)法時(shí),Block語(yǔ)法表達(dá)式所使用的自動(dòng)變量值被保存到Block的結(jié)構(gòu)體實(shí)例(既Block自身)中。

2.3.3 __block說(shuō)明符

以上的截獲自動(dòng)變量的代碼例子,在Block的結(jié)構(gòu)體實(shí)例中重寫(xiě)該自動(dòng)變量也不會(huì)改變?cè)冉孬@的自動(dòng)變量。因?yàn)樵趯?shí)現(xiàn)上不能改寫(xiě)被截獲自動(dòng)變量的值,所以會(huì)發(fā)生編譯錯(cuò)誤。
不過(guò)這樣以來(lái),無(wú)法在Block中保存值了,極為不便。但是有兩個(gè)方法:

  1. 有如下幾個(gè)變量,允許Block改寫(xiě)值:
    • 靜態(tài)變量
    • 靜態(tài)全局變量
    • 全局變量
  2. 使用__block修飾變量 :__block 存儲(chǔ)域類(lèi)說(shuō)明符

C語(yǔ)言有如下存儲(chǔ)域類(lèi)說(shuō)明符:

  1. typedef
  2. extern
  3. static:表示作為靜態(tài)變量存在在數(shù)據(jù)區(qū)中
  4. auto:表示作為自動(dòng)變量存儲(chǔ)在棧中
  5. register

__block說(shuō)明符類(lèi)似于static、auto和register說(shuō)明符,它們用于指定將變量值設(shè)置到哪個(gè)存儲(chǔ)域中。

個(gè)人筆記

2.3.4 Block存儲(chǔ)域

通過(guò)前面說(shuō)明可知,Block轉(zhuǎn)換為Block的結(jié)構(gòu)體類(lèi)型的自動(dòng)變量,__block變量轉(zhuǎn)換為_(kāi)_block變量的結(jié)構(gòu)體類(lèi)型的自動(dòng)變量。所謂結(jié)構(gòu)體類(lèi)型的自動(dòng)變量,既棧上生成的該結(jié)構(gòu)體的實(shí)例。
另外通過(guò)之前的說(shuō)明可知Block也是Objective-C對(duì)象,該Block的類(lèi)為_(kāi)NSConcreteStackBlock。有很多與之類(lèi)似的類(lèi),如:

  • _NSConcreteStackBlock,既該類(lèi)的對(duì)象Block設(shè)置在棧上
  • _NSConcreteGlobalBlock,設(shè)置在程序的數(shù)據(jù)區(qū)域(.data)中
  • _NSConcreteMallocBlock,設(shè)置在由malloc函數(shù)分配的內(nèi)存塊(既堆)中

在記述全局變量的地方使用Block語(yǔ)法時(shí),生成的Block為_(kāi)NSConcreteGlobalBlock類(lèi)對(duì)象。例如

void (^blk)(void) = ^ {
    printf("Global Block");
}

此源代碼通過(guò)聲明全局變量blk來(lái)使用Block語(yǔ)法。如果轉(zhuǎn)換該源代碼,Block用結(jié)構(gòu)體的成員變量isa的初始化如下:

impl.isa = & _NSConcreteGlobalBlock;

該Block的類(lèi)為_(kāi)NSConcreteGlobalBlock類(lèi)。此Block既該Block用結(jié)構(gòu)體實(shí)例設(shè)置在程序的數(shù)據(jù)區(qū)域中。
在以下情況下,Block為_(kāi)NSConcreteGlobalBlock類(lèi)對(duì)象

  • 記述全局變量的地方有Block語(yǔ)法時(shí)
  • Block語(yǔ)法的表達(dá)式中不使用應(yīng)截獲的自動(dòng)變量時(shí)

除此之外的Block語(yǔ)法生成的Block為_(kāi)NSConcreteStackBlock類(lèi)對(duì)象,且設(shè)置在棧上。
配置在全局變量上的Block,從變量作用域外也可以通過(guò)指針安全的使用。但設(shè)置在棧上的Block,如果其所屬的變量作用域結(jié)束,該Block就被廢棄。由于__block變量也配置在棧上,同樣的,如果其所屬的變量作用域結(jié)束,則該__block變量也會(huì)被廢棄。
Block提供了將Block和__block變量從棧上復(fù)制到堆上的方法來(lái)解決這個(gè)問(wèn)題。將配置在棧上的Block復(fù)制到堆上,這樣即使Block語(yǔ)法記述的變量作用域結(jié)束,堆上的Block還可以繼續(xù)存在。
復(fù)制到堆上的Block將_NSConcreteMallocBlock類(lèi)對(duì)象寫(xiě)入Block用結(jié)構(gòu)體實(shí)例的成員變量isa。

impl.isa = & _NSConcreteMallocBlock;

而__block變量用結(jié)構(gòu)體成員變量_forwarding可以實(shí)現(xiàn)無(wú)論__block變量配置在棧上還是堆上時(shí)都能夠正確的訪問(wèn)__block變量。在此情形下,只要棧上的結(jié)構(gòu)體實(shí)例成員變量__forwarding指向堆上的結(jié)構(gòu)體實(shí)例,那么不管是從棧上的__block變量還是從堆上的__block變量都能夠正確的訪問(wèn)。

那么Block提供的復(fù)制方法是什么呢?當(dāng)ARC時(shí),大多數(shù)情形下編譯器會(huì)恰當(dāng)?shù)呐袛?,自?dòng)生成將Block從棧上復(fù)制到堆上的代碼。
當(dāng)Block作為函數(shù)返回值返回時(shí),執(zhí)行objc_retainBlock方法,實(shí)際上是copy函數(shù)。
那么少數(shù)情形有幾種呢?

  1. XXXX

另外,對(duì)于已配置在堆上的Block以及配置在程序的數(shù)據(jù)區(qū)域的Block,調(diào)用copy方法又會(huì)如何呢?

  • _NSConcreteMallocBlock:引用計(jì)數(shù)增加
  • _NSConcreteStackBlock:從棧復(fù)制到堆
  • _NSConcreteGlobalBlock:什么也不做

不管是Block配置在何處,用copy方法復(fù)制都不會(huì)引起任何問(wèn)題。在不確定時(shí)調(diào)用copy方法即可

2.3.5 __block變量存儲(chǔ)域

上節(jié)只對(duì)Block的copy進(jìn)行了說(shuō)明,使用__block變量的Block從棧復(fù)制到堆時(shí),使用的所有__block變量也必定配置在棧上。這些__block變量也全部從棧復(fù)制到堆。此時(shí),Block持有__block變量。
如果配置在堆上的Block被廢棄,那么它所使用的__block變量也就被釋放。
此思考方式與OC的引用計(jì)數(shù)內(nèi)存管理完全相同。使用__block變量的Block持有__block變量。日光Block被廢棄,它所持有的__block變量也就被釋放。

那么在理解了__block變量的存儲(chǔ)域之后,在回顧下之前講過(guò)的使用__block變量用結(jié)構(gòu)體成員變量__forwarding的原因?!安还躝_block變量配置在棧上還是在堆上,都能夠正確的訪問(wèn)該變量”。正如這句話所述,通過(guò)Block的復(fù)制,__block變量也從棧復(fù)制到堆。此時(shí)可同時(shí)訪問(wèn)棧上的__block變量和堆上的__block變量。
源代碼可轉(zhuǎn)換為如下形式:

(val.__forwarding->val);

在變換Block語(yǔ)法的函數(shù)中,該變量val為復(fù)制到堆上的__block變量用結(jié)構(gòu)體實(shí)例,而使用的與Block無(wú)關(guān)的變量val,為復(fù)制前棧上的__block變量用結(jié)構(gòu)體實(shí)例。
但是棧上的__block變量用結(jié)構(gòu)體實(shí)例在__block變量從棧復(fù)制帶堆上時(shí),會(huì)將成員變量__forwarding的值替換為復(fù)制目標(biāo)堆上的__block變量用結(jié)構(gòu)體實(shí)例的地址。
通過(guò)該功能,無(wú)論上在Block語(yǔ)法中、block語(yǔ)法外使用__block變量,還是__block變量配置在棧上或堆上,都可以順利的訪問(wèn)同一個(gè)__block變量。

2.3.6 截獲對(duì)象

以下源代碼生成并持有NSMutableArray類(lèi)的對(duì)象,由于附有__strong修飾符的賦值目標(biāo)變量的作用域立即結(jié)束,因此對(duì)象被立即釋放并廢棄。

{
    id array = [[NSMutableArray alloc] init];
}

下面我們看一下在Block語(yǔ)法中使用該變量array的代碼:

//運(yùn)行正常
blk_t blk;
{
    id array = [[NSMutableArray alloc] init];
    blk = [^(id obj) {
        [array addObject:obj];
    } copy];
}

blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);

該代碼運(yùn)行正常,執(zhí)行結(jié)果如下

array count = 1;
array count = 2;
array count = 3;

請(qǐng)注意被賦值NSMutableArray類(lèi)對(duì)象并被截獲的自動(dòng)變量array。我們可以發(fā)現(xiàn)它是Block用的結(jié)構(gòu)體中附有__strong修飾符的成員變量。

struct _main_block_impl_0 {
    struct __block_impl impl;   
    struct __main_block_desc_0 *Desc;   
    id __strong array; 
};

在OC中,C語(yǔ)言結(jié)構(gòu)體不能含有附有__strong修飾的變量。因?yàn)榫幾g器不知道應(yīng)何時(shí)進(jìn)行C語(yǔ)言結(jié)構(gòu)體的初始化和廢棄操作,不能很好的管理內(nèi)存。

但是OC的運(yùn)行時(shí)庫(kù)能夠很準(zhǔn)確把握Block從棧復(fù)制到堆以及堆上的Block被廢棄的時(shí)機(jī),因此Block用結(jié)構(gòu)體中即使含有附有__strong修飾符或__weak修飾符的變量,也可以恰當(dāng)?shù)倪M(jìn)行初始化和廢棄。為此需要使用在__main_block_desc_0結(jié)構(gòu)體中增加的成員變量copy和dispose,以及作為指針賦值給該成員變量的_main_block_copy_0函數(shù)和_main_block_dispose_0函數(shù)。
恰當(dāng)管理賦值給變量array的對(duì)象:__main_block_copy_0函數(shù)使用_Block_object_assign函數(shù)將對(duì)象類(lèi)型對(duì)象復(fù)制給Block用結(jié)構(gòu)體的成員變量array中并持有該對(duì)象。

_Block_object_assign函數(shù)調(diào)用相當(dāng)于retain實(shí)例方法的函數(shù),將對(duì)象賦值在對(duì)象類(lèi)型的結(jié)構(gòu)體成員變量中。

另外,__main_block_dispose_0函數(shù)使用_Block_object_dispose函數(shù),釋放賦值在Block用結(jié)構(gòu)體成員變量array中的對(duì)象。
_Block_object_dispose函數(shù)調(diào)用相當(dāng)于release實(shí)例方法的函數(shù),釋放賦值在對(duì)象類(lèi)型的結(jié)構(gòu)體成員變量中的對(duì)象。

雖然此__main_block_copy_0函數(shù)(以下簡(jiǎn)稱(chēng)copy函數(shù))和__main_block_dispose_0函數(shù)(以下簡(jiǎn)稱(chēng)dispose函數(shù))指針被賦值在__main_block_desc_0結(jié)構(gòu)體成員變量copy和dispose。在Block從棧復(fù)制到堆時(shí)以及堆上的Block被廢棄時(shí)會(huì)調(diào)用這些函數(shù)。

  • copy函數(shù):棧上的Block復(fù)制到堆時(shí);
  • dispose函數(shù):堆上的Block被廢棄時(shí);

那么什么時(shí)候棧上的Block會(huì)復(fù)制到堆呢?

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

在上面這些情況下棧上的Block賦值到堆上,其實(shí)可歸結(jié)為_(kāi)Block_copy函數(shù)被調(diào)用時(shí)Block從棧復(fù)制到堆。相對(duì)的,在釋放復(fù)制到堆上的Block后,誰(shuí)都不持有Block而使其被廢棄時(shí)調(diào)用dispose函數(shù)。這相當(dāng)于對(duì)象的dealloc實(shí)例方法。
有了這種構(gòu)造,通過(guò)使用附有__strong修飾符的自動(dòng)變量,因而B(niǎo)lock中截獲的對(duì)象就能夠超出其變量作用域而存在。

2.3.7 __block變量和對(duì)象

__block說(shuō)明符可指定任何類(lèi)型的自動(dòng)變量。

__block id obj = [[NSObject alloc] init];

其實(shí)該代碼等同于

__block id __strong obj = [[NSObject alloc] init];

ARC有效時(shí),id類(lèi)型以及對(duì)象類(lèi)型變量必定附加所有權(quán)修飾符,缺省為附有__strong修飾符的變量。
在Block中使用附有__strong修飾符的id類(lèi)型或?qū)ο箢?lèi)型自動(dòng)變量的情況下,當(dāng)Block從棧復(fù)制到堆時(shí),使用Block_object_assign函數(shù),持有Block截獲的對(duì)象。當(dāng)堆上的Block被廢棄時(shí),使用_block_object_dispose函數(shù),釋放Block截獲的對(duì)象。
在__block變量為附有_strong修飾符的id類(lèi)型或?qū)ο箢?lèi)型自動(dòng)變量的情形下會(huì)發(fā)生同樣的過(guò)程。當(dāng)__block變量從棧復(fù)制到堆時(shí),使用_Block_object_assign函數(shù),持有賦值給__block變量的對(duì)象。當(dāng)堆上的__block變量被廢棄時(shí),使用_Block_object_dispose函數(shù),釋放賦值給__block變量的對(duì)象。
由此可知,即使對(duì)象賦值復(fù)制到堆上的附有_strong修飾符的對(duì)象類(lèi)型__block變量中,只要__block變量在堆上繼續(xù)存在,那么該對(duì)象就會(huì)繼續(xù)處于被持有的狀態(tài)。這與Block中使用賦值給附有__strong修飾符的對(duì)象類(lèi)型自動(dòng)變量的對(duì)象相同。

另外,我們前面用到的只有附有__strong修飾符的id類(lèi)型或?qū)ο箢?lèi)型自動(dòng)變量。如果使用__weak修飾符會(huì)如何呢?首先是在Block中使用附有__weak修飾符的id類(lèi)型變量的情況。

blk_t blk;
{
    id array = [[[NSMutableArray alloc] init];
    id __weak array2 = array;
    blk = [^(id obj) {
        [array2 addObject:obj];
    } copy];
}

blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);

該代碼可正常執(zhí)行。 執(zhí)行結(jié)果,這與以上代碼的結(jié)果不同:

array2 count = 0;
array2 count = 0;
array2 count = 0;

這是由于附有__strong修飾符的變量array在該變量作用域結(jié)束的同時(shí)被釋放、廢棄,nil被賦值在附有__weak修飾符的變量array2中。

2.3.8 Block循環(huán)引用

如果在Block中使用附有__strong修飾符的對(duì)象類(lèi)型自動(dòng)變量,那么當(dāng)Block從棧復(fù)制到堆時(shí),該對(duì)象為Block所持有。這樣容易引起循環(huán)利用。我們來(lái)看看下面的源代碼:

typedef void (^blk_t)(void);

@interface MOyObject : NSObject
{
      blk_t blk_;
}
@end

@implementation MyObject

- (id)init {
    self = [super init];
    blk_ = ^ {
        NSLog(@"self = %@",self);
    } ;
    return self;
}

- (void)dealloc {
    NSLog(@:dealloc:);
}
@end

int main() {
    id o = [[MyObject alloc] init];
    NSLog(@"%@",o);
   return 0;
}

該源代碼中MyObject類(lèi)的dealloc實(shí)例方法一定沒(méi)有被調(diào)用。
MyObject類(lèi)對(duì)象的Block類(lèi)型成員變量blk_持有賦值為Block的強(qiáng)引用。既MyObject類(lèi)對(duì)象持有Nlock。init實(shí)例方法中執(zhí)行的Block語(yǔ)法使用附有_strong修飾符的id類(lèi)型變量self。并且由于Block語(yǔ)法賦值在了成員變量blk中,因此通過(guò)Block語(yǔ)法生成在棧上的Block此時(shí)由棧復(fù)制到堆,并持有所使用的self。self持有Block,Block持有self。這正是循環(huán)引用。
另外,編譯器在編譯該源代碼是能夠查處循環(huán)引用,因此編譯器能正確的進(jìn)行警告。
為避免此循環(huán)引用,可聲明附有__weak修飾符的變量,并將self賦值使用。

- (id)init {
    self = [super init];
    id __weak tmp = self;
    blk_ = ^ {
        NSLog(@"self = %@",tmp);
    } ;
    return self;
}

在該源代碼中,由于Block存在時(shí),持有該Block的MyObject類(lèi)對(duì)象賦值在變量tmp中的self必須存在,因此不需要判斷tmp的值是否為nil。
在面相iOS4(MRC),必須使用__unsafe_unretained修飾符代替__weak修飾符。在此源代碼中也可使用__unsafe_unretained修飾符,且不必?fù)?dān)心懸掛指針。

- (id)init {
    self = [super init];
    id __unsafe_unretained tmp = self;
    blk_ = ^ {
        NSLog(@"self = %@",tmp);
    } ;
    return self;
}

另外在以下源代碼中Block內(nèi)沒(méi)有使用self也同樣截獲了self,引起了循環(huán)引用。

typedef void (^blk_t)(void);

@interface MOyObject : NSObject
{
      blk_t blk_;
      id obj_;
}
@end

@implementation MyObject

- (id)init {
    self = [super init];
    blk_ = ^ {
        NSLog(@"obj_ = %@",obj_);
    } ;
    return self;
}

既Block語(yǔ)法內(nèi)使用的obj_實(shí)際上截獲了self。對(duì)編譯器來(lái)說(shuō),obj_只不過(guò)是對(duì)象用結(jié)構(gòu)體的成員變量。

blk_ = ^ {
    NSLog(@"obj_ = %@",self->obj_);
};

該源代碼也基本與前面一樣,聲明附有_weak修飾符的變量并賦值obj使用來(lái)避免循環(huán)引用。在此源代碼中也可安全的使用__unsafe_unretained修飾符,原因同上。

- (id)init {
    self = [super init];
    id __weak obj = obj_;
    blk_ = ^ {
        NSLog(@"obj = %@",obj);
    } ;
    return self;
}

在為避免循環(huán)引用而使用__weak修飾符時(shí),雖說(shuō)可以確認(rèn)使用附有__weak修飾符的變量時(shí)是否為nil,但更有必要使之生成以使用賦值給附有__weak修飾符變量的對(duì)象。
另外,還可以使用__block變量來(lái)避免循環(huán)引用。

typedef void (^blk_t)(void);

@interface MOyObject : NSObject
{
      blk_t blk_;
}
@end

@implementation MyObject

- (id)init {
    self = [super init];
    __block id tmp = self;
    blk_ = ^ {
        NSLog(@"self = %@",tmp);
        tmp = nil;
    } ;
    return self;
}

- (void)execBlock {
    blk();
}

- (void)dealloc {
    NSLog(@:dealloc:);
}

@end

int main() {
    id o = [[MyObject alloc] init];
    [o execBlock];
   return 0;
}

該源代碼沒(méi)有循環(huán)引用。原因:通過(guò)執(zhí)行execBlock實(shí)例方法,Block被實(shí)行,nil被賦值在__block變量tmp中。因此,_block變量tmp對(duì)MyObject類(lèi)對(duì)象的強(qiáng)引用失效。但是如果不調(diào)用execBlock實(shí)例方法,既不執(zhí)行賦值給成員變量blk的Block,便會(huì)循環(huán)引用并引起內(nèi)存泄漏。
在生成并持有MyObject類(lèi)對(duì)象的狀態(tài)下會(huì)引起以下循環(huán)引用:

  • MyObject類(lèi)對(duì)象持有Block;
  • Block持有__block變量;
  • __block變量持有MyObject類(lèi)對(duì)象;

下面我們對(duì)使用__block變量避免循環(huán)引用的方法和使用__weak 修飾符及__unsafe_unretained修飾符避免循環(huán)引用的方法做個(gè)比較。
使用__block變量的優(yōu)點(diǎn)如下:

  • 通過(guò)__block變量可控制對(duì)象的持有期間
  • 在不能使用__weak修飾符的環(huán)境中不使用__unsafe_unretained修飾符即可(不必?fù)?dān)心懸垂指針)

在執(zhí)行Block時(shí)可動(dòng)態(tài)的決定是否將nil或其他對(duì)象賦值在__block變量中。

使用__block變量的缺點(diǎn)如下:

  • 為避免循環(huán)引用必須執(zhí)行Block

存在執(zhí)行了Block語(yǔ)法,卻不執(zhí)行Block的路徑時(shí),無(wú)法避免循環(huán)引用。若有雨Block引發(fā)了循環(huán)引用時(shí),根據(jù)Block的用途選擇使用__block變量、__weak修飾符或__unsafe_unretained修飾符來(lái)避免循環(huán)引用。

2.3.9 copy/release

ARC無(wú)效時(shí),一般需要手動(dòng)將Block從棧復(fù)制到堆。另外,由于ARC無(wú)效,所以肯定要釋放賦值的Block。這時(shí)我們用copy實(shí)例方法用來(lái)賦值,用release實(shí)例方法來(lái)釋放。

    [blk_ release];

只要Block有一次復(fù)制并配置在堆上,就可通過(guò)retain實(shí)例方法持有。

    [blk_ retain];

但是對(duì)于配置在棧上的Block調(diào)用retain實(shí)例方法則不起任何作用。

    [blk_ retain];

該源代碼中,雖然堆賦值給blk_的棧上的Block調(diào)用了retain實(shí)例方法,但實(shí)際上對(duì)此源代碼不起任何作用。因此推薦使用copy實(shí)例方法來(lái)持有Block。

另外,ARC無(wú)效時(shí),__block說(shuō)明符被用來(lái)避免Block中的循環(huán)引用。這是由于當(dāng)Block從棧復(fù)制到堆時(shí),若Block使用的變量為附有__block說(shuō)明符的id類(lèi)型或?qū)ο箢?lèi)型的自動(dòng)變量,不會(huì)被retain;若Block使用的變量為沒(méi)有__block說(shuō)明符的id類(lèi)型或?qū)ο箢?lèi)型的自動(dòng)變量,則被retain。

注意:正好在ARC有效時(shí)能夠同__unsafe_unretained修飾符一樣來(lái)使用。由于ARC有效時(shí)和無(wú)效時(shí)__block說(shuō)明符的用途有很大的區(qū)別,因此在編寫(xiě)源代碼時(shí),必須知道該源代碼是在ARC有效情況下編譯還是在ARC無(wú)效情況下編譯。

最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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