[編寫高質量iOS程式碼的52個有效方法](九)塊(block)

究極死胖獸發表於2016-07-27

[編寫高質量iOS程式碼的52個有效方法](九)塊(block)

參考書籍:《Effective Objective-C 2.0》 【英】 Matt Galloway

先睹為快

37.理解塊這一概念

38.為常用的塊型別建立typedef

39.用handler塊降低程式碼的分散程度

40.用塊引用其所屬物件時不要出現保留環

目錄

第37條:理解塊這一概念

塊與函式類似,只不過是直接定義在另一個函式裡,和定義它的那個函式共享同一個範圍內的東西。塊用^符號來表示,後面跟著一對花括號,括號裡面是塊的實現程式碼。

// 塊的語法結構
return_type (^block_name)(parameters)

// 定義一個沒有引數沒有返回值的最簡單的塊
void (^someBlock)() = ^{
    // code
};
// 定義一個接收2個int引數,返回int值的塊
int (^addBlock)(int, int) = ^(int a, int b){
    return a + b;
};

塊的強大之處在於,在它的宣告範圍你,所有變數都可以為其捕獲,也就是說範圍內的全部變數在塊內依然可用。

int additional = 5;
int (^addBlock)(int, int) = ^(int a, int b){
    return a + b + additional;
};
int add = addBlock(2,5); // add = 12

預設情況下,為塊所捕獲的變數是不可以在塊內修改的。不過宣告變數時加上__block修飾符就可以在塊內修改了。如果將塊定義在Objective-C類的例項方法中,則無須加上__block修飾符也能修改類的例項變數,並可以使用self屬性(存取例項變數時也會自動捕獲self屬性,需注意避免保留環)。

@implementation EOCClass{
    NSString *_anInstanceVariable;
}

- (void)anInstanceMethod{
    __block NSString *aBlockVariable = @"Anything";
    void (^someBlock)() = ^{
        _anInstanceVariable = @"Something";
        aBlockVariable = @"Another thing";
        NSlog(@"%@ %@", _anInstanceVariable, aBlockVariable);
    }
    someBlock();
}

@end

定義塊時,其所佔的記憶體區域是分配在棧中的。也就是說,塊只在定義它的那個範圍內有效。例如下面這段程式碼就有危險:

void (^someBlock)();
if(/* some condition */){
    someBlock = ^{
        NSLog(@"Block A");
    };
}else{
    someBlock = ^{
        NSLog(@"Block B");
    };
}
someBlock();

定義在if及else語句中的兩個塊都分配在棧記憶體中。編譯器會給每個塊分配好棧記憶體,然而等離開了相應範圍後,編譯器可能把分配給塊的內容覆寫掉,於是執行時可能會程式崩潰。

可以給塊物件傳送copy訊息以拷貝,這樣就把塊從棧上覆制到堆上了。拷貝後的塊可以在定義它的範圍之外使用,而且複製到堆上後,塊就成了帶引用計數的物件了。由ARC負責管理釋放。

void (^someBlock)();
if(/* some condition */){
    someBlock = [^{
        NSLog(@"Block A");
    } copy];
}else{
    someBlock = [^{
        NSLog(@"Block B");
    } copy];
}
someBlock();

除了棧塊和堆塊以外,還有一類塊叫做全域性塊,這種塊不會捕捉任何狀態,執行時也無需有狀態參與,塊所使用的整個記憶體區域在編譯期已經完全確定了。下面就是一個全域性塊:

void (^someBlock)() = ^{
    NSLog(@"This is a block");
};

執行全域性塊所需的全部資訊都能在編譯期確定。

第38條:為常用的塊型別建立typedef

每個塊都具備其固有型別,因而可將其賦值給適當型別的變數,這個型別由塊所接受的引數及其返回值組成。

// 定義新型別,表示接受BOOL及int引數並返回int值的塊,型別名為EOCSomeBlock
typedef int(^EOCSomeBlock) (BOOL flag, int value);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 建立EOCSomeBlock型別變數
        // block1與block2雖然實現不同,但型別相同
        EOCSomeBlock block1 = ^(BOOL flag, int value){
            if (flag) {
                return value * 5;
            }else{
                return value * 10;
            }
        };

        EOCSomeBlock block2 = ^(BOOL flag, int value){
            return flag * value;
        };
    }
    return 0;
}

通過這項特性,可以把使用塊的API做的更易用些。類裡面有些方法可能需要塊來做引數,不如執行非同步任務時所用的“completion handler”,引數就是塊,凡是遇到這種情況都可以通過定義別名使程式碼變得更為易讀。

// 不用typedef
- (void)startWithCompletionHandler:(void(^)(NSdata *data, NSError *error))completion;

// 使用typedef
typedef void(^EOCCompletionHandler)(NSdata *data, NSError *error);
- (void)startWithCompletionHandler:(EOCCompletionHandler)completion;

在使用塊型別的類中定義這些typedef時,還應該把這個類的名字加在由typedef所定義的新型別前面,這樣可以闡明塊的用途,還可以用typedef給同一個塊簽名型別建立數個別名。例如Mac OS X與iOS的Accounts框架就是個例子,其中有:

typedef void(^ACAccountStoreSaveCompletionHandler)(BOOL success, NSError *error);
typedef void(^ACAccountStoreRequestAccessCompletionHandler)(BOOL success, NSError *error);

這兩個型別定義的簽名相同,但是用在不同地方,便於開發者理解其用途。

第39條:用handler塊降低程式碼的分散程度

非同步方法在執行完任務之後,需要以某種方式通知相關程式碼。實現此功能有很多辦法,常見的技巧是設計一個委託協議,令關注此事件的物件遵從該協議。物件成為delegate之後,就可以在相關事件發生時得到通知了。
例如用委託模式設計一個從URL中獲取資料的類:

#import <Foundation/Foundation.h>

@class EOCNetworkFetcher
@protocol EOCNetworkFetcherDelegate <NSObject>
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher didFinishWithData:(NSData*)data;
@end

@interface EOCNetworkFetcher : NSObject
@property (nonatomic, weak) id<EOCNetworkFetcherDelegate> delegate;

- (id)initWithURL:(NSURL*)url;
- (void)start;
@end

而其他類則可以像這樣來使用此類提供的API

- (void)fetchFooData{
    NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/foo.dat"];
    EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    fetcher.delegate = self;
    [fetcher start];
}

-(void)networkFetcher:(EOCNetworkFetcher*)fetcher didFinishWithData:(NSData*)data{
    _fetchedFooData = data;
}

這樣做沒有問題,但是改用塊來寫的話會更加清晰。將獲取資料的類修改為:

#import <Foundation/Foundation.h>

typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);

@interface EOCNetworkFetcher : NSObject

- (id)initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler;
@end

這和使用委託協議很像,不過可以直接以內聯形式定義completion handler:

- (void)fetchFooData{
    NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/foo.dat"];
    EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    [fetcher startWithCompletionHandler:^(NSData *data){
        _fetcherFooData = data;
    }];
}

與委託模式的程式碼相比,用寫出來的程式碼顯然更為整潔。非同步任務執行完畢後所需執行的業務邏輯和啟動非同步任務所用的程式碼放在了一起。而且塊宣告在建立獲取器的範圍內,它可以訪問此範圍內的全部變數。

這種寫法還有其他用途,比如現在很多基於塊的API都是用塊來處理錯誤。這裡又分為兩種寫法,可以分別用兩個處理程式來分別處理成功和失敗的情況,也可以把處理成功和失敗情況的程式碼都封裝到同一個handler中:

// 分別處理成功和失敗
#import <Foundation/Foundation.h>

typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
typedef void(^EOCNetworkFetcherErrorHandler)(NSError *error);

@interface EOCNetworkFetcher : NSObject

- (id)initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion failureHandler:(EOCNetworkFetcherErrorHandler)failure;
@end

// 處理成功和失敗封裝到同一個handler
#import <Foundation/Foundation.h>

typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data, NSError *error);

@interface EOCNetworkFetcher : NSObject

- (id)initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;
@end

全部邏輯寫到一起,會令塊變得比較長,且比較複雜。但是更靈活,比如資料下載到一半時網路故障了,可以把資料及相關錯誤都回傳給塊。除此之外還有個優點,呼叫API的程式碼可能會在處理成功響應的過程中發現錯誤,採用單一塊的話,那麼就能把這種情況和獲取器認定的失敗情況統一處理了。

handler塊也能定義成類的屬性:

typedef void(^EOCNetworkFetcherCompletionHandler)(float progress);

@interface EOCNetworkFetcher : NSObject
@property(nonatomic, copy) EOCNetworkFetcherCompletionHandler progressHandler;
@end

第40條:用塊引用其所屬物件時不要出現保留環

使用塊時,若不仔細思量,很容易導致保留環。例如下面這個類,提供了一套從URL中下載資料的介面:

// EOCNetworkFetcher.h
#import <Foundation/Foundation.h>

typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);

@interface EOCNetworkFetcher : NSObject
@property(nonatomic, strong, readonly) NSURL *url;
- (id)initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;
@end

// EOCNetworkFetcher.m
#import "EOCNetworkFetcher.h"

@interface EOCNetworkFetcher ()
@property(nonatomic, strong, readwrite) NSURL *url;
@property(nonatomic, copy) EOCNetworkFetcherCompletionHandler completionHandler;
@property(nonatomic, strong) NSData *downloadedData;
@end

@implementation EOCNetworkFetcher

- (id)initWithURL:(NSURL*)url{
    if((self = [super init])){
        _url = url;
    }
    reutrn self;
}

- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion{
    self.completionHandler = completion;
    // 開始請求網路資料,結束後呼叫p_requestCompleted方法
}

// 處理下載資料的私有方法
- (void)p_requestCompleted{
    if(_completionHandler){
        _completionHandler(_downloadedData);
    }
}
@end

某個類可能建立這種網路資料獲取器物件,並用其從URL中下載資料

@implementation EOCClass{
    EOCNetworkFetcher *_networkFetcher;
    NSData *_fetchData;
}

-(void)downloadData{
    NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/something.dat"]
    _networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    [_networkFetcher startWithCompletionHandler:^(NSData *data){
        NSLog(@"Request URL %@ finished", _networkFetcher.url);
        _fetchData = data;
    }];
}
@end

這個時候就出現了保留環,由於completion handler塊要設定EOCClass物件的例項變數,所以它必須捕獲self屬性,也就是說,handler塊保留了建立網路資料獲取器的EOCClass物件。而EOCClass又通過strong例項變數保留了獲取器,最後獲取器物件又保留了handler塊,形成了保留環。

要打破保留環可以令_networkFetcher不再引用獲取器,或者獲取器的completionHandler屬性不再持有handler塊。例如,可以這樣修改:

[_networkFetcher startWithCompletionHandler:^(NSData *data){
        NSLog(@"Request URL %@ finished", _networkFetcher.url);
        _fetchData = data;
        // 令_networkFetcher不再引用獲取器
        _networkFetcher = nil;
    }];

但這樣做的缺點是,必須等completion handler執行後才能打破環,如果completion handler一直不執行,那麼保留環就會一直存在,於是記憶體就會洩漏。

如果用另一種寫法,EOCClass不再保留獲取器:

-(void)downloadData{
    NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/something.dat"]
    EOCNetworkFetcher *networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    [networkFetcher startWithCompletionHandler:^(NSData *data){
        NSLog(@"Request URL %@ finished", networkFetcher.url);
        _fetchData = data;
    }];
}
@end

這樣不會出現之前的保留環,但又會出現了新的保留環。completion handler需要通過獲取器物件來引用其中的URL,於是塊要保留獲取器,而獲取器又經由completionHandler屬性保留了塊。

要打破這個保留環可以令獲取器一旦執行過completion handler後就不再保留它。

- (void)p_requestCompleted{
    if(_completionHandler){
        _completionHandler(_downloadedData);
    }
    self.completionHandler = nil;
}

相關文章