iOS 讀書筆記(2) - Block

Pro Multithreading And Memory Management for iOS and OS X

這次想寫的是這本書的第二章節 - Block 的介紹,

想先申明的是,在這篇裡你不會看到什麼高深的用法,這本書純粹就是介紹原理罷了。

好看的一部動畫,應該也會收BD XD

什麼是 Block ?

Block 是C語言的擴充功能,可以用一句話來表式Blocks的擴充功能。
帶有自動變數修飾符(區域變數)的匿名函數。

顧名思義,所謂匿名函數就是不帶有名稱的函數。 標準C語言是不允許這樣的函數。
所謂匿名函數在其他語言的名稱

程式語言 Block的名稱
C+ Blocks Block
Smalltalk Block
Ruby Block
LISP Lambda
Python Lambda
C++ 11 Lambda
Javascript Anonumous function

Block 語法

^(int event) {
    printf("int:%d",event);
}

實際上,該Block使用了省略方式,完整如下,

^void (int event) {
    printf("int:%d",event);
}

如上所示,完整形式的Block語法與一般C語言函數定義相比,僅有兩點不同。

  • 沒有函數名。
  • 帶有”^”。

以下為Block 語法的BN范式。

Block_literal_expression :: = ^ block_decl compound_statement_body
block_decl ::=
block_decl ::= parameter_list
block_decl ::= type_expression

即使不了解BN范式,通過說明也能有個概念

^ 返回值類型 參數列表 表達式

可以寫出如下的Block語法

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

但Block語法是可以省略好幾個項目的,首先是返回值類型
省略返回值類型時,如果表達式中有return時就使用該返回值的類息,沒有就使用void類型,表達式中有多個return 語句時,所有return的返回值類型必須相同。

Block 類型變數

block類型變數與一般C語言變數完全相同,可作為以下用途使用。

  • 自動變數(區域變數)
  • 函數參數
  • 靜態變數
  • 靜態全域變數
  • 全域變數

那麼,我們就式著使用Block語法將Block賦值給Block變數。

int (^blk)(int) = ^(int count){return count +1;}
因為變數相同所以blk可賦值給blk1
int (^blk1)(int) = blk;

int (^blk2)(int)
blk2 = blk;

但當函數參數或返回值中使用Block變數時,既難寫又難記,這時,我們可以使用typedef 來解決問題。
typedef int (^blk_t)int;

這時我們就可使用blk_t 來代替整個block變數。通過使用typedef,函數定義變的更容易理解了。

另外Block 也可以像C語言用指標的方式去使用,這邊就不細講了。

截獲自動變數值

一開始我有說明Block是帶有”自動變數的匿名函數”,那什麼是帶有自動變數值呢?舉個例子

int main()
{
    int  dmy = 256;
    int val = 10;
    const char *fmt = "val = %d \n";
    void (^blk)(void) = ^{printf(fmt,val);};

    val = 2;
    fmt = "These values were changed val = %d \n";

    blk();

    return 0;

}

執行結果會是 val = 10

執行結果並不是改寫後的值,而是執行Block語法時的自動變數的瞬間值。該Block語法在執行時,字符串指標”cal = %d \n”被賦值到自動變數fmt中,int 值被賦值到val 中,因此這邊值被保存(即被截獲),而在執行時被使用。

這就是自動變數值的截獲。

__block 說明符

實際上,截獲變數只能保存Block語法的瞬間值,保存後就不能改寫該值。

這時,若想在Block語法的表達式中將值賦給Block語法外聲明的自動變數,需要在該自動變數上附加__block。

使用附有__block說明符的自動變數可在Block中賦值,該變數稱為__block變數

Blocks的實現

Block的實質

Block 始帶有自動變數得匿名函數,但Block究竟是什麼呢? 下面將通過Block的實現進一步來了解。

書中使用了把Objective -C的程式碼編譯成了C語言程式碼,我盡量以我了解的情況說明

當你在.m檔中輸入如下語法

int main()
{
    void (^blk)(void) = ^{printf("Block\n");};
    blk();

    return 0;
}

在command Line中執行clang -rewrite-objc 文件名
發現會編譯成.cpp檔 打開後最下面main開始就是你的源始碼。

我們來看看棧上的__main_block_impl_0結構體實例(即Block)是如何根據這些參數進行初始化的。 如果展開__main_block_impl_0結構體的__block_impl結構體,可記成以下形式:

struct __main_block_impl_0 {

    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
    struct __main_block_desc_0* Desc;
}

該結構體根據構造函數會像下面這樣進行初始化。

isa = &_NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;

什麼是isa = &_NSConcreteStackBlock 呢?

將Block 指標賦給Block結構體成員isa。為了理解它,首先要理解Objective-C類和對象的實質。其實,Block就是Objective-C的對象。

“id”這一變數類型用於存儲Objective-C對象,在Objective-C源代碼中,雖然可以像使用void*類型那樣隨意使用id,但此id類型也能夠在C語言中聲名,在/usr/unclude/objc/runtime.h中是如下聲明的:

typedef struct objc_object {
    Class isa;
} *id;

id為objc_object結構體的指標類型,我們再來看看Class。

typedef struct objc_class *Class;

Class為objc_class結構體的指標類型, objc_class又是如下聲明

struct objc_class {
    Class isa;
};

這與objc_object結構體相同。然而,objc_object和objc_class歸根究底是在各個對象和類的實現中使用的最基本的結構體。

@interface MyObject : NSObject
{
    int cal0;
    int val1;
}
@end

基於objc_object結構體,該類的對象結構體如下:

struct MyObject {
    Class isa;
    int val0;
    int val1;
};    

意味著,生成的各個對象即由該類生成的對象的各個結構體實例,通過成員變量isa保持該類的結構體實例指標。 如圖所示

在Objcetive-C中,比如NSObject 的class_t結構體實例或是NSmutableArray的class_t結構體實例等,均生成並保持各個類的屬性以及父類的指標,並被Objcetive-C運行時庫所使用。

到這裡,就可以理解Objective-C的類與對象的實質了。

回到剛才的Block結構體。

isa = &_NSConcreteStackBlock;

此_NSConcreteStackBlock相當於class_t的結構體實例。在將Block作為Objective-C類對象處理時,關於該類的信息放置於_NSConcreteStackBlock中。

現在終於能理解Block的實質,知道Block即為Objective-C的對象了。

__block 說明符

前述我們知截取的自動變數在Block中改變其值時,會產生編譯錯誤。

解決這個問題的方法有兩種。
第一種:C語言中有一個變數,允許Block改寫值,具體如下:

  • 靜態變數
  • 靜態全域變數
  • 全域變數

    雖然Block語法的匿名函數部份簡單地變換了C語言函數,但從這個變換的函數中訪問靜態全域變數/全域變數 並沒有任何改變,可直接使用。

    但是靜態變數的情況下,轉換後的函數原本就設置在含有Block語法的函數外,所以無法從變數作用域訪問。,

    解決Block中不能保存值這種問題的第二種方法是使用 “__block說明符”


    __block說明符類似於static,auto和register,它們用於將變數值設置到哪個存儲域中。

Block存儲域

Block 與 __block 變數的實質

名稱 實質
Block 棧上Block結構體實例
__block變數 棧上__block變數的結構體實例

通過之前的說明可知Block也是Objective-C對象。將Block當作Objective-C對象來看時,該Block為_NSConcreteStackBlock。

雖然該類並沒有出現在已變換的源代碼中,但有很多與之類似的類,如

  • _NSConcreteStackBlock
  • _NSConcreteGlobalBlock
  • _NSConcreteMallocBlock

其名稱與內容可整理如下

設置對象的存儲域
_NSConcreteStackBlock 棧(stack)
_NSConcreteGlobalBlock 程序的數據區域(Data區)
_NSConcreteMallocBlock 推(heap)

雖然我們在很多地方都看到的是_NSConcreteStackBlock
但有兩種情況下Block是_NSConcreteGlobalBlock

  • 記述全局變量的地方有Block語法時
  • Block語法的表達式中不使用應截獲的自動變數時

那_NSConcreteMallocBlock呢?

設置在stack中的Block在作用域完後就會被廢棄,因為__block也配置在stack上所以同時也會被廢棄。

Block提供了將Block和__block變數從棧上複製到推上的方法來解決這個問題。既使stack上的作用域已結束,heap上的Block依舊存在。

這時isa = &_NSConcreteMallocBlock;


__block變數結構體成員中有個forwarding 可以實現無論`block在stack或是heap上都能正卻地訪問__block變數`。

在大多數情形下,編譯器會恰當地進行判斷是否該複製Block,自動生成將Block殂棧上複製到推上的代碼。我們來看個例子

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

可轉換如下

blk_t func(int rate)
{
    blk_t tmp = &__func_block_impl_0(
            __func_block_0, &__func_block_desc_0_DATA, rate);

    tmp = objc_retainBlock(tmp);

    return objc_autoreleaseReturnValue(tmp);
}

因為在ARC下,所以blk_t是附有__strong修飾符的。
objc_retainBlock實際上就是_Block_copy函數。

所以將Block作為函數返回值返回時,編譯器會自動生成複製到heap的代碼。

前面說過大多數情況下編譯器會自動幫我們生成代碼,另外則是需我們手動使用copy方法。那什麼情況下需要手動Copy呢

  • 向方法或函數的參數中傳遞Block時

但是如果在方法或函數中適當地複製了傳遞過來的參數,那麼就不必在調用該方法或函數前手動複製了。 以下方法不用手動複製;

  • Cocoa框架的方法且方法名中含有usingBlock等時。
  • Grant Central Dispatch 的API。
1
2
3
4
5
6
7
8
- (id)getBlockArray
{
int val = 10;
NSArray *aa = [[NSArray alloc]initWithObjects:
^{NSLog(@"blk0:%d",val);},
^{NSLog(@"blk1:%d",val);}, nil];
return aa;
}
1
2
3
4
5
6
7
id obj = getBlockArray();
typedef void (^blk_t)(void);
blk_t blk = (blk_t)[obj objectAtIndex:0];
blk();

此代碼在執行blk()時就會當掉,因為getBlockArray結束時,棧上的Block被廢棄的緣故,可惜此時編譯器不能判斷是否需要複製,當然你也可以用讓編譯器進行判斷自行手動COPY,但是從stack到heap是會耗CPU資源的,過多的copy只是在消耗cpu資源,因此只在此情形下手動進行複製。

經由自己測試,執行時,並不會當在blk(),網路查了許多資料,
以我個人觀點,因為當成return值返回時,array被帶有
autoreleasePool中, array裡的block前面有說過,
當block被當作參數傳遞時不能被判斷,但是當作返回值時,
會進行_Block_copy,所以這邊element 0 會被複製到heap中 
執行是沒問題 有值可以出現,但是當出了了id的作用域時,
被釋放裡面的element 1 因為編譯器誤判,沒有複製到heap上,
所以雙重release ,造成程式Crash。

另外,對於已配置在heap的Block以及配置在Data區的Block調用copy又會如何呢?

Block的副本

Block的類 副本源的配致存儲域 複製效果
_NSConcreteStackBlock 棧(stack) 從棧複製到推
_NSConcreteGlobalBlock 程序讀數據區域 什麼也不做
_NSConcreteMallocBlock 推(heap) 引用計數增加

那麼如果多次調用copy對同一個Block時有沒有問題呢?答案是沒有問題的。
請各位自行想想__strong 賦值後的情況即可得知一二。

截穫對象

以下程式碼生成並持有NSMutableArray對象,由于附有__strong的賦值目標變數的作用域立即結束,因此對象被立即釋放並廢棄。

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

我們來看一下在Block語法中使用該變數array的程式碼。

blk_t blk;
{
    id array = [[NSMUtableArray alloc]init]; 
    blk = [^(id obj) {

        [array addObject:obj];

        NSLog(@"array cpunt = %ld",[array count]);

    }copy];
}]

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

變數作用域結束的同時,變數array被廢棄,其強引用失效,因此賦值給array的NSMurableArray被定被釋放。 但是該程式碼運行正常,其結果如下

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

這一結果意味著賦值給變數array類的對象在該程式碼最後Block的執行部份超出其變數作用域而存在。

為什麼呢,還記得我們說過copy方法會把Block從stack複製到heap。

在Block從stack -> heap時會調用Copy函數,從heap廢棄時會調用dispose函數。

那麼什麼時候棧上的Block會複製到推上呢?

  • 調用Block的Copy實例方法時
  • Block作為函數返回值返回時
  • 將Block賦值給附有__strong修飾符id類型或Block類型成員變數時
  • 在方法明中含有usingBlock的cocoa方法或GCD的API傳遞Block時

也就是說,雖然從源代碼來看,在上面這些情況下棧上的Block被複製到推上,但其實可歸結為_Block_copy函數被調用時Block從棧複製到推。

鄉對的,在釋放複製到推上的Block後,誰都不持有Block而使其被廢棄時調用dispose函數,這相當於對象的dealloc方法。

有了惡些構造,通過使用附有__strong修飾符的自動變數,Block中截取的對象就能夠超出其變數作用域而存在。

那麼在剛才的程式碼中,如果不掉用copy時又會如何呢?

執行該程式碼後,程式會強制結束。

因為只有調用_Block_copy函數才能持有截獲附有__strong的對象類型的自動變數值,所以不進行copy情況下,即使截獲饹為象,它也會隨著變數作用域的結束而被廢棄。

因此,Block中使用對象類型自動變數時,除以下情況,推毽使用copy實例方法。

  • Block作為函數返回值返回時
  • 將Block賦值給附有__strong修飾符id類型或Block類型成員變數時
  • 在方法明中含有usingBlock的cocoa方法或GCD的API傳遞Block時

__block變量和對象

前面說過當stack -> heap時除了Block外還有__block變數也會被複製到heap上,即使對象賦值複製到推上的附有__strong的對象類型__block變數中,只要__block在推上繼續存在,那麼該對象就會繼續處於被持有狀態。

這與Block中使用賦值給附有__strong的對象類型自動變數的對象相同。

前例,若用__weak修飾符時會發生什麼事呢?

程式碼一樣可正常運行但是結果確都是 0 。

這是由於附有__strong的變數array在該變數作用域結束時同時被釋放,廢棄 nil被賦值在附有__weak的變數中。

若同時指定block , weak時會怎樣呢?

理由同__weak,即使加__block說明符,附有__strong的變量array也會再該作用域結束被釋放,nil同時會指給__weak指向的變數。

Block循環引用

如果在Block中使用附有__strong的對象類型自動變量,那麼當Block從棧複製到推時,該對象為Block所持有。這樣容易引起循環引用。 我們來看個例子

typedef void (^blk_t)(void);

@interface MyObject : NSObject
{
    blk_t blk;
}
@end

@implementation MyObject
- (id)init
{
    self = [super init];

    blk = ^{NSLog(@"self = %@", self);};
}

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

int main() 
{
    id a =[[MyObject alloc]init];

    NSlog(@"%@",a);

    return 0;
}

該程式碼中dealloc方法一定不會被調用。

為避免此循環引用,可聲明附有__weak修飾符的變數,並將self賦值使用。

- (id)init
{
    self = [super init];

    id __weak tmp = self;

    blk = ^{NSLog(@"self = %@", tmp);};
}

這樣即可打破循環引用,因為我們用了弱引用來打斷引用的方式。

另外,以下程式碼沒有使用self一樣引起了循環引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface MyObject : NSObject
{
blk_t blk;
id obj;
}
@end
@implementation MyObject
- (id)init
{
self = [super init];
blk = ^{NSLog(@"obj = %@", obj);};
return self;
}

光這樣打時,編譯器就會出現警告,

及Block語法內使用的obj實際上截獲了self。對編譯器來說,obj只不過是對象用結構體的成員變量

blk = ^{NSLog(@"obj = %@", self->obj);};

另外,還可以使用__block來避免循環引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
typedef void (^blk_t)(void);
@interface MyObject : NSObject
{
blk_t blk;
}
@end
@implementation MyObject
- (id)init
{
self = [super init];
__block id tmp = self;
blk = ^{NSLog(@"self = %@", tmp);};
tmp = nil;
}
- (void)execBlock
{
blk();
}
- (void)dealloc
{
NSLog(@"dealloc");
}
@end
int main()
{
id a =[[MyObject alloc]init];
[a execBlock];
return 0;
}

這段代碼沒有引起循環引用,但是如果不調用execBlock實例方法時,即不執行賦值給成員變數blk的Block,便會循環引用並引起內存洩漏。

下面我們對使用__block變數避免循環引用的方法和使用__weak及__unsafe_unretained修飾符避免循環引用做個比較。

使用__block變數的優點如下

  • 通過__block變數可控制對象的持有期間。
  • 在不能使用__weak的環境中事unsafe_unretained修飾符即可。在直行Block時可動態地決定是否將nil或其它對象賦值在block變數中。

使用__block缺點如下

  • 為了避免循環引用必須執行Block

存在值行Block語法,卻不執行Block的路徑時,無法避免循環引用。若由於Block引發了循環引用時,跟據Block的用途選擇使用 __block , __weak , _unsafe_unretained 修飾符來避免循環引用。