Objective-C中的Block

斯人如是丶發表於2016-05-13

原文連結:http://www.cocoachina.com/ios/20150109/10891.html

1.相關概念

在這篇筆記開始之前,我們需要對以下概念有所瞭解。

1.1 作業系統中的棧和堆

注:這裡所說的堆和棧與資料結構中的堆和棧不是一回事。

我們先來看看一個由C/C++/OBJC編譯的程式佔用記憶體分佈的結構: 

memory_structure.jpg

棧區(stack):由系統自動分配,一般存放函式引數值、區域性變數的值等。由編譯器自動建立與釋放。其操作方式類似於資料結構中的棧,即後進先出、先進後出的原則。

例如:在函式中申明一個區域性變數int b;系統自動在棧中為b開闢空間。

堆區(heap):一般由程式設計師申請並指明大小,最終也由程式設計師釋放。如果程式設計師不釋放,程式結束時可能會由OS回收。對於堆區的管理是採用連結串列式管理的,作業系統有一個記錄空閒記憶體地址的連結串列,當接收到程式分配記憶體的申請時,作業系統就會遍歷該連結串列,遍歷到一個記錄的記憶體地址大於申請記憶體的連結串列節點,並將該節點從該連結串列中刪除,然後將該節點記錄的記憶體地址分配給程式。

例如:在C中malloc函式 

char p1; 
p1 = (char )malloc(10);

但是p1本身是在棧中的。

連結串列:是一種常見的基礎資料結構,一般分為單向連結串列、雙向連結串列、迴圈連結串列。以下為單向連結串列的結構圖:

linked_list.jpg

單向連結串列是連結串列中最簡單的一種,它包含兩個區域,一個資訊域和一個指標域。資訊域儲存或顯示關於節點的資訊,指標域儲存下一個節點的地址。

上述的空閒記憶體地址連結串列的資訊域儲存的就是空閒記憶體的地址。

全域性區/靜態區:顧名思義,全域性變數和靜態變數儲存在這個區域。只不過初始化的全域性變數和靜態變數儲存在一塊,未初始化的全域性變數和靜態變數儲存在一塊。程式結束後由系統釋放。

文字常量區:這個區域主要儲存字串常量。程式結束後由系統釋放。

程式程式碼區:這個區域主要存放函式體的二進位制程式碼。

下面舉一個前輩寫的例子:

//main.cpp
int a = 0; // 全域性初始化區
char *p1; // 全域性未初始化區
main {
    int b; // 棧
    char s[] = "abc"; // 棧
    char *p2; // 棧
    char *p3 = "123456"; // 123456\0在常量區,p3在棧上
    static int c =0; // 全域性靜態初始化區
    p1 = (char *)malloc(10);
    p2 = (char *)malloc(20); // 分配得來的10和20位元組的區域就在堆區
    strcpy(p1, "123456"); // 123456\0在常量區,這個函式的作用是將"123456" 這串字串複製一份放在p1申請的10個位元組的堆區域中。
    // p3指向的"123456"與這裡的"123456"可能會被編譯器優化成一個地址。
}

strcpy函式

原型宣告:extern char *strcpy(char* dest, const char *src);

功能:把從src地址開始且含有NULL結束符的字串複製到以dest開始的地址空間。

1.2 結構體(Struct)

在C語言中,結構體(struct)指的是一種資料結構。結構體可以被宣告為變數、指標或陣列等,用以實現較複雜的資料結構。結構體同時也是一些元素的集合,這些元素稱為結構體的成員(member),且這些成員可以為不同的型別,成員一般用名字訪問。

我們來看看結構體的定義:

struct tag { member-list } variable-list;

struct:結構體關鍵字。

tag:結構體標籤。

member-list:結構體成員列表。

variable-list:為結構體宣告的變數列表。

在一般情況下,tag,member-list,variable-list這三部分至少要出現兩個。以下為示例:

// 該結構體擁有3個成員,整型的a,字元型的b,雙精度型的c
// 並且為該結構體宣告瞭一個變數s1
// 該結構體沒有標明其標籤
struct{
    int a;
    char b;
    double c;
} s1;
// 該結構體擁有同樣的三個成員
// 並且該結構體標明瞭標籤EXAMPLE
// 該結構體沒有宣告變數
struct EXAMPLE{
    int a;
    char b;
    double c;
};
//用EXAMPLE標籤的結構體,另外宣告瞭變數t1、t2、t3
struct EXAMPLE t1, t2[20], *t3;

以上就是簡單結構體的程式碼示例。結構體的成員可以包含其他結構體,也可以包含指向自己結構體型別的指標。結構體的變數也可以是指標。

下面我們來看看結構體成員的訪問。結構體成員依據結構體變數型別的不同,一般有2種訪問方式,一種為直接訪問,一種為間接訪問。直接訪問應用於普通的結構體變數,間接訪問應用於指向結構體變數的指標。直接訪問使用結構體變數名.成員名,間接訪問使用(*結構體指標名).成員名或者使用結構體指標名->成員名。相同的成員名稱依靠不同的變數字首區分。

struct EXAMPLE{
    int a;
    char b;
};
//宣告結構體變數s1和指向結構體變數的指標s2
struct EXAMPLE s1, *s2;
//給變數s1和s2的成員賦值,注意s1.a和s2->a並不是同一成員
s1.a = 5;
s1.b = 6;
s2->a = 3;
s2->b = 4;

最後我們來看看結構體成員儲存。在記憶體中,編譯器按照成員列表順序分別為每個結構體成員分配記憶體。如果想確認結構體佔多少儲存空間,則使用關鍵字sizeof,如果想得知結構體的某個特定成員在結構體的位置,則使用offsetof巨集(定義於stddef.h)。

struct EXAMPLE{
    int a;
    char b;
};
//獲得EXAMPLE型別結構體所佔記憶體大小
int size_example = sizeof( struct EXAMPLE );
//獲得成員b相對於EXAMPLE儲存地址的偏移量
int offset_b = offsetof( struct EXAMPLE, b );

1.3 閉包(Closure)

閉包就是一個函式,或者一個指向函式的指標,加上這個函式執行的非區域性變數。

說的通俗一點,就是閉包允許一個函式訪問宣告該函式執行上下文中的變數,甚至可以訪問不同執行上文中的變數。

我們用指令碼語言來看一下:

function funA(callback){
    alert(callback());
}
function funB(){
    var str = "Hello World"; // 函式funB的區域性變數,函式funA的非區域性變數
    funA(
        function(){
            return str;
        }
    );
}

通過上面的程式碼我們可以看出,按常規思維來說,變數str是函式funB的區域性變數,作用域只在函式funB中,函式funA是無法訪問到str的。但是上述程式碼示例中函式funA中的callback可以訪問到str,這是為什麼呢,因為閉包性。

2.blcok基礎知識

block實際上就是Objective-C語言對閉包的實現。

2.1 block的原型及定義

我們來看看block的原型:

NSString * ( ^ myBlock )( int );

上面的程式碼宣告瞭一個block(^)原型,名字叫做myBlock,包含一個int型的引數,返回值為NSString型別的指標。

下面來看看block的定義:

myBlock = ^( int paramA )
{
    return [ NSString stringWithFormat: @"Passed number: %i", paramA ];
};

上面的程式碼中,將一個函式體賦值給了myBlock變數,其接收一個名為paramA的引數,返回一個NSString物件。

注意:一定不要忘記block後面的分號。

定義好block後,就可以像使用標準函式一樣使用它了:

myBlock(7);

由於block資料型別的語法會降低整個程式碼的閱讀性,所以常使用typedef來定義block型別。例如,下面的程式碼建立了GetPersonEducationInfo和GetPersonFamilyInfo兩個新型別,這樣我們就可以在下面的方法中使用更加有語義的資料型別。

// Person.h
#import // Define a new type for the block
typedef NSString * (^GetPersonEducationInfo)(NSString *);
typedef NSString * (^GetPersonFamilyInfo)(NSString *);
@interface Person : NSObject
- (NSString *)getPersonInfoWithEducation:(GetPersonEducationInfo)educationInfo
    andFamily:(GetPersonFamilyInfo)familyInfo;
@end

我們用一張大師文章裡的圖來總結一下block的結構:

block.png

2.2 將block作為引數傳遞

// .h
-(void) testBlock:( NSString * ( ^ )( int ) )myBlock;
// .m
-(void) testBlock:( NSString * ( ^ )( int ) )myBlock
{
    NSLog(@"Block returned: %@", myBlock(7) );
}

由於Objective-C是強制型別語言,所以作為函式引數的block也必須要指定返回值的型別,以及相關引數型別。

2.3 閉包性

上文說過,block實際是Objc對閉包的實現。

我們來看看下面程式碼:

#import void logBlock( int ( ^ theBlock )( void ) )
{
    NSLog( @"Closure var X: %i", theBlock() );
}
int main( void )
{
    NSAutoreleasePool * pool;
    int ( ^ myBlock )( void );
    int x;
    pool = [ [ NSAutoreleasePool alloc ] init ];
    x = 42;
    myBlock = ^( void )
    {
        return x;
    };
    logBlock( myBlock );
    [ pool release ];
    return EXIT_SUCCESS;
}

上面的程式碼在main函式中宣告瞭一個整型,並賦值42,另外還宣告瞭一個block,該block會將42返回。然後將block傳遞給logBlock函式,該函式會顯示出返回的值42。即使是在函式logBlock中執行block,而block又宣告在main函式中,但是block仍然可以訪問到x變數,並將這個值返回。

注意:block同樣可以訪問全域性變數,即使是static。

2.4 block中變數的複製與修改

對於block外的變數引用,block預設是將其複製到其資料結構中來實現訪問的,如下圖:

block-capture-1.jpg

通過block進行閉包的變數是const的。也就是說不能在block中直接修改這些變數。來看看當block試著增加x的值時,會發生什麼:

myBlock = ^( void )
{
    x++;
    return x;
};

編譯器會報錯,表明在block中變數x是隻讀的。

有時候確實需要在block中處理變數,怎麼辦?彆著急,我們可以用__block關鍵字來宣告變數,這樣就可以在block中修改變數了。

基於之前的程式碼,給x變數新增__block關鍵字,如下:

__block int x;

對於用__block修飾的外部變數引用,block是複製其引用地址來實現訪問的,如下圖:

block-capture-2.jpg

3.編譯器中的block

3.1 block的資料結構定義

我們通過大師文章中的一張圖來說明:

block-struct.jpg

上圖這個結構是在棧中的結構,我們來看看對應的結構體定義:

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. */
};

從上面程式碼看出,Block_layout就是對block結構體的定義:

isa指標:指向表明該block型別的類。

flags:按bit位表示一些block的附加資訊,比如判斷block型別、判斷block引用計數、判斷block是否需要執行輔助函式等。

reserved:保留變數,我的理解是表示block內部的變數數。

invoke:函式指標,指向具體的block實現的函式呼叫地址。

descriptor:block的附加描述資訊,比如保留變數數、block的大小、進行copy或dispose的輔助函式指標。

variables:因為block有閉包性,所以可以訪問block外部的區域性變數。這些variables就是複製到結構體中的外部區域性變數或變數的地址。

3.2 block的型別

block有幾種不同的型別,每種型別都有對應的類,上述中isa指標就是指向這個類。這裡列出常見的三種型別:

_NSConcreteGlobalBlock:全域性的靜態block,不會訪問任何外部變數,不會涉及到任何拷貝,比如一個空的block。例如:

#include int main()
{
    ^{ printf("Hello, World!\n"); } ();
    return 0;
}

_NSConcreteStackBlock:儲存在棧中的block,當函式返回時被銷燬。例如:

#include int main()
{
    char a = 'A';
    ^{ printf("%c\n",a); } ();
    return 0;
}

_NSConcreteMallocBlock:儲存在堆中的block,當引用計數為0時被銷燬。該型別的block都是由_NSConcreteStackBlock型別的block從棧中複製到堆中形成的。例如下面程式碼中,在exampleB_addBlockToArray方法中的block還是_NSConcreteStackBlock型別的,在exampleB方法中就被複制到了堆中,成為_NSConcreteMallocBlock型別的block:

void exampleB_addBlockToArray(NSMutableArray *array) {
    char b = 'B';
    [array addObject:^{
            printf("%c\n", b);
    }];
}
void exampleB() {
    NSMutableArray *array = [NSMutableArray array];
    exampleB_addBlockToArray(array);
    void (^block)() = [array objectAtIndex:0];
    block();
}

總結一下:

_NSConcreteGlobalBlock型別的block要麼是空block,要麼是不訪問任何外部變數的block。它既不在棧中,也不在堆中,我理解為它可能在記憶體的全域性區。

_NSConcreteStackBlock型別的block有閉包行為,也就是有訪問外部變數,並且該block只且只有有一次執行,因為棧中的空間是可重複使用的,所以當棧中的block執行一次之後就被清除出棧了,所以無法多次使用。

_NSConcreteMallocBlock型別的block有閉包行為,並且該block需要被多次執行。當需要多次執行時,就會把該block從棧中複製到堆中,供以多次執行。

3.3 編譯器如何編譯

我們通過一個簡單的示例來說明:

#import typedef void(^BlockA)(void);
__attribute__((noinline))
void runBlockA(BlockA block) {
    block();
}
void doBlockA() {
    BlockA block = ^{
        // Empty block
    };
    runBlockA(block);
}

上面的程式碼定義了一個名為BlockA的block型別,該block在函式doBlockA中實現,並將其作為函式runBlockA的引數,最後在函式doBlockA中呼叫函式runBloackA。

注意:如果block的建立和呼叫都在一個函式裡面,那麼優化器(optimiser)可能會對程式碼做優化處理,從而導致我們看不到編譯器中的一些操作,所以用__attribute__((noinline))給函式runBlockA新增noinline,這樣優化器就不會在doBlockA函式中對runBlockA的呼叫做內聯優化處理。

我們來看看編譯器做的工作內容:

#import __attribute__((noinline))
void runBlockA(struct Block_layout *block) {
    block->invoke();
}
void block_invoke(struct Block_layout *block) {
    // Empty block function
}
void doBlockA() {
    struct Block_descriptor descriptor;
    descriptor->reserved = 0;
    descriptor->size = 20;
    descriptor->copy = NULL;
    descriptor->dispose = NULL;
    struct Block_layout block;
    block->isa = _NSConcreteGlobalBlock;
    block->flags = 1342177280;
    block->reserved = 0;
    block->invoke = block_invoke;
    block->descriptor = descriptor;
    runBlockA(&block);
}

上面的程式碼結合block的資料結構定義,我們能很容易得理解編譯器內部對block的工作內容。

3.4 copy()和dispose()

上文中提到,如果我們想要在以後繼續使用某個block,就必須要對該block進行拷貝操作,即從棧空間複製到堆空間。所以拷貝操作就需要呼叫Block_copy()函式,block的descriptor中有一個copy()輔助函式,該函式在Block_copy()中執行,用於當block需要拷貝物件的時候,拷貝輔助函式會retain住已經拷貝的物件。

既然有有copy那麼就應該有release,與Block_copy()對應的函式是Block_release(),它的作用不言而喻,就是釋放我們不需要再使用的block,block的descriptor中有一個dispose()輔助函式,該函式在Block_release()中執行,負責做和copy()輔助函式相反的操作,例如釋放掉所有在block中拷貝的變數等。

4.總結

以上內容是我學習各大師的文章後對自己學習情況的一個記錄,其中有部分文字和程式碼示例是來自大師的文章,還有一些自己的理解,如有錯誤還請大家勘誤。

相關文章