學會使用Objective-C中的block

Apple從OS X 10.4和iOS 4以后開始支持block,相對于delegate,block有很多便捷之處,使得代碼更簡潔,可讀性更強。但是如果使用不當,則會造成很多問題。本文結合自己的經驗和《Pro Multithreading and Memory Management for iOS and OS X: with ARC, Grand Central Dispatch, and Blocks》書中的知識點,介紹block的相關知識點。

block語法

我們通過以下圖來了解block的語法,圖片來自這里

block語法結構圖

我們來看看上面的圖,代碼如下

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

根據圖中的解釋,我們從左向右來看,該block返回值為int類型,'^'符號聲明一個名為myblock的block,該block有一個int類型的入參,等號右邊則為block的定義,block有一個名為num的int類型的入參,{return num * multiplier;};則為該block的block實現部分。

該block的調用方法如下,看起來像C的函數調用。

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

我們來看看復雜一點的情況:

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

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

控制臺輸出

NSStackBlock

以上代碼,新手看起來可能是會有些費勁的。我們一步一步來,首先,我們調用testBlock函數,在該函數中,

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

該代碼塊實際上是傳給了startWithBlock函數的參數block,當執行startWithBlock函數時,調用block(),實際上就是執行了以上代碼塊。

使用typedef定義block類型
以上代碼可以通過typedef來定義block,以便閱讀,如下

typedef int (^myBlock)(int num);

在定義某個block類型時,可以使用

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

這樣看起來,要比之前簡單得多。

block捕獲外部變量

block內可以訪問block之前定義的變量:

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

但是,如果想在block內部改變multiplier的值,編輯器則會報錯

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

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

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

此時,需要將該變量使用__block修飾:

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

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

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

因為static、static global或者global變量都是存儲在內存中的全局區(靜態區),對于這三種類型變量,block內部是捕獲了其指針,則可以直接訪問修改;而對于之前的臨時變量,block則只是捕獲了該變量的值,無法修改到外部的變量。

block內部還可以訪問類的實例變量和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的內部結構

block 的數據結構定義如下(圖片來自 這里):

block內存布局

對應的結構體定義如下:

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 實例實際上由 6 部分構成:

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

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

    對于block,isa指針可以指向
    _NSConcreteStackBlock_NSConcreteMallocBlock_NSConcreteGlobalBlock這三種類型

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

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

  • invoke:函數指針,指向block的實現代碼地址。

  • descriptor:指向結構體的指針,block的附加描述信息,比如保留變量數、block的大小、copy和dispose輔助函數的函數指針指針
    copy函數為當block執行copy操作或者當block從棧上拷貝到堆上時調用,dispose函數則是block在堆上釋放時調用

  • variables:block內部捕獲的對象,如

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

此時,variables中則為fmt和val這兩個變量

block的類型

block有_NSConcreteStackBlock_NSConcreteMallocBlock_NSConcreteGlobalBlock這三種類型。
三種block在內存中存儲位置如下圖

圖片來自《Pro Multithreading and Memory Management for iOS and OS X: with ARC, Grand Central Dispatch, and Blocks》
block類型的區分

以下情況,block為_NSConcreteGlobalBlock類型

  • block內部只使用了全局變量
  • block內部沒有使用任何外部的局部變量

除了以上兩種情況,其他的block為_NSConcreteStackBlock類型。
而對于_NSConcreteMallocBlock,只有當_NSConcreteStackBlock類型的block執行copy操作(手動或者系統執行)時,該block才會是_NSConcreteMallocBlock類型

我們來看看代碼,直觀的看看這三種類型的block

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

控制臺輸出

<__NSGlobalBlock__: 0x104fd52d0>

或者

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

控制臺輸出

<__NSGlobalBlock__: 0x104fd5310>

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

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

控制臺輸出

<__NSStackBlock__: 0x7fff59615a18>

以上代碼,block內部捕獲了multiplier這個外部的局部變量,所以是_NSConcreteStackBlock類型。
因為該block存在在棧上,在超過block的作用域時,該block就會被系統釋放,就有可能會出現block內部的代碼還沒有走完,就被釋放掉的情況。對于這種情況,應該對block執行copy操作,將block復制到堆上。
注:在ARC下,系統在大部分情況下,會將block從棧上復制到堆上,這個后面會細說

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

控制臺輸出

<__NSMallocBlock__: 0x6000000486a0>

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

ARC 下 block 的自動拷貝和手動拷貝

ARC下,以下幾種情況,系統會將block從棧上自動復制到堆上

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

因為在ARC下,對象默認是用__strong修飾的,所以大部分情況下編譯器都會將 block從棧自動復制到堆上,除了以下情況

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

控制臺輸出

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

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

控制臺輸出

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

控制臺輸出

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

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

執行testBlock方法,控制臺輸出

<__NSStackBlock__: 0x7fff563aa988>

此處可能會有疑問:既然當block作為函數參數時為_NSConcreteStackBlock類型,超出其作用域時,block會被釋放掉,那會不會出現函數先退出了,block還是沒有執行完畢的?

經過我測試,我發現,其實在函數中,在block執行完畢前,函數是不會退出的。因為函數中按順序執行的,函數中block后的代碼會等待block執行完畢,所以在block塊代碼未執行完畢時,該函數不會退出,從而沒有超過block的作用域,block不會被釋放。看下面的例子就可以明白了。

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

控制臺輸出

NSStackBlock
<__NSStackBlock__: 0x7fff54ba4a20>

從打印結果可以看出,當我們執行startWithBlock函數時,先是執行了block內的代碼,再是執行函數中block后的代碼,所以可以保證block執行完畢。

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

其實,把block放子線程中,無非是通過GCD和performSelectorInBackground方法,系統會自動GCD的block進copy操作,而performSelectorInBackground需要傳一個selector,又相當于走進了函數里,還是按順序執行了,函數還是會等待block執行完畢。

針對不同block類型的copy、retain、release操作

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

注:盡量不要對block使用retain操作。因為從上可以看出,retain操作對)_NSConcreteStackBlock并沒有效果,這樣會誤以為retain生效了,在后續調用block的時候,其實block早就被釋放了,從而導致crash

block循環引用問題

可以使用__weak__unsafe_unretained__block修飾詞修飾被block持有的對象來打破循環,還有就是在block執行完畢的時候,將block置nil的方法。具體細節這里就不講了,有興趣的童鞋可以看看我簡書上寫了另一篇文章

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 一、Objective-C發展史 Objective-C從1983年誕生,已經走過了30多年的歷程。隨著時間的推移...
    沒事蹦蹦閱讀 5,903評論 12 34
  • 《Objective-C高級編程》這本書就講了三個東西:自動引用計數、block、GCD,偏向于從原理上對這些內容...
    WeiHing閱讀 9,902評論 10 69
  • 要了解Block需要先了解什么是“閉包性” 2.3 閉包性 上文說過,block實際是Objc對閉包的實現。 我們...
    隨風飄蕩的小逗逼閱讀 353評論 0 0
  • 前言 Blocks是C語言的擴充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了這...
    小人不才閱讀 3,786評論 0 23
  • .相關概念 在這篇筆記開始之前,我們需要對以下概念有所了解。 1.1 操作系統中的棧和堆 注:這里所說的堆和棧與數...
    狼鳳皇閱讀 490評論 0 0