編寫高質(zhì)量iOS與OSX代碼的52個有效方法-第六章:塊Block

當(dāng)前多線程編程的核心就是塊(block)與大中樞派發(fā)(Grand Central Dispatch,GCD)。

塊是一種可在C、C++、OC代碼中使用的語法閉包(lexical closure),借由此機(jī)制,開發(fā)者可以將代碼想對象一樣傳遞,令其在不同環(huán)境下運行。還有個關(guān)鍵地方,在定義塊的范圍內(nèi),它可以訪問到其中的全部變量。

GCD是一種與塊相關(guān)的技術(shù),它提供了對線程的抽象,而這種抽象則基于派發(fā)隊列(dispatch Queue)。開發(fā)者可以將塊排入隊列中,有GCD負(fù)責(zé)處理所有調(diào)度事宜。CGD會根據(jù)系統(tǒng)資源情況,適時地創(chuàng)建、復(fù)用、摧毀后臺線程(background Thread),以便處理每個隊列。

GCD還可以完成常見編程任務(wù),如果只執(zhí)行一次的線層安全代碼(thread-safe single-code execution),或者根據(jù)可用的系統(tǒng)資源并發(fā)執(zhí)行多個操作。

37、理解“塊”這一概念

block基礎(chǔ)知識

block可以實現(xiàn)閉包。block與函數(shù)雷士,不過是直接定義在另一個函數(shù)里,和定義它的那個函數(shù)共享一個范圍內(nèi)的東西。

block 用^表示,后面跟一對花括號{},括號里就是要實現(xiàn)的的代碼。

^{
}

block其實就是個值,而且有其相關(guān)類型。block語法與函數(shù)指針近似。

//定義一個名為someBlock,沒有參數(shù)沒有返回值的block變量。
void (^someBlock)() = ^{

};

block的語法結(jié)構(gòu):
return_type (^block_name)(parameters)

示例:

int addtional = 10;
    
int  (^addBlock)(int a, int b) = ^(int a,int b) {
    
    return a+ b + addtional;
};
NSLog(@"%d",addBlock(2,3));//15

block可以捕獲聲明在它范圍內(nèi)的所有變量。這份范圍內(nèi)的所有變量,在block中都可以使用。默認(rèn)情況下,為塊所捕獲的變量,是不可以在塊里修改的。

如果直接修改就會報錯Variable is not assignable (missing __block type specifier)。但是如果聲明時加上__block,在block內(nèi)就可以修改了。

__block NSInteger count = 0;
NSArray *array = @[@1,@2,@3,@4,@5];
// 遍歷數(shù)組
[array enumerateObjectsUsingBlock:^(NSNumber *number, NSUInteger idx, BOOL * _Nonnull stop) {
    if ([number compare:@2] == NSOrderedAscending) {//比2小
        count ++;
    }
}];
NSLog(@"%zd",count);//1

這里數(shù)組遍歷的方法,演示了內(nèi)聯(lián)塊(inline block)的用法。傳給enumerateObjectsUsingBlock:的方法塊,并沒有先賦給局部變量,而是直接內(nèi)聯(lián)在函數(shù)里調(diào)用了。

如果block所捕獲的變量是對象類型,那么就會自動保留它。系統(tǒng)在釋放這個block的時候,也會將其一并釋放。這就引出一個與塊有關(guān)的重要問題,塊本身可視為對象。 在其他OC對象所能響應(yīng)的選擇子中,很多塊也可以響應(yīng)。塊本身也和其他對象一樣,有引用計數(shù)。當(dāng)最后一個指向塊的引用移走之后,塊就回收了,回收時也會釋放塊所捕獲的變量,以便平衡捕獲時所執(zhí)行的保留操作。

如果將塊定義在實例方法中,那么除了可以訪問類的所喲實例變量之外,還可以使用self變量。

塊總能修改實例變量,所在聲明時不用加__block。不過如果通過讀取或?qū)懭氩僮鞑蹲搅藢嵗兞?,那么也會自動吧self變量一并捕獲了。因為實例變量與self所指代的實例關(guān)聯(lián)在一起。

int  (^addBlock)(int a, int b) = ^(int a,int b) {
    self->_titleString = @"xxxxx";
    _titleString = @"aaa";
    return a+ b + addtional;
};

如果不加self指代_titleString = @"aaa";,會有警告
Block implicitly retains 'self'; explicitly mention 'self' to indicate this is intended behavior

它等同于self->_titleString = @"xxxxx";,可以通過屬性訪問:self.titleString = @"ccccc";

注:self也是個對象,因而在捕獲它時也會將其保留,如果self所指代的那個對象同時也保留了塊,那么這種情況就會導(dǎo)致保留環(huán)(循環(huán)引用)。

塊的內(nèi)部結(jié)構(gòu)

塊本身是對象,在存放塊對象的內(nèi)存區(qū)域中,首個變量是指向Class對象的指針,該指針叫做isa。其余內(nèi)存里含有塊對象正常運轉(zhuǎn)所需的各種信息。

全局塊,棧塊,堆塊

定義塊的時候,其所占的內(nèi)存區(qū)域是分配在棧中的,這就是說,塊只有在定義它的那個范圍內(nèi)有效。

void (^block)(void);
if (addtional % 2 == 0) {
    block  = ^{
        NSLog(@"block a");
    };
} else {
    block = ^{
        NSLog(@"block b");
    };
}
block;

// 

定義在if和else語句中的兩個塊分配在棧內(nèi)存中,編譯器會給每個塊分配好棧內(nèi)存,等離開了相應(yīng)的范圍之后,編譯器有可能把分配給塊的內(nèi)存覆寫掉。于是這兩個塊只能保證在if或else語句范圍內(nèi)有效。這樣的代碼可以編譯,但是運行起來,時而正確,時而錯誤。若編譯器未覆寫執(zhí)行的塊,照常執(zhí)行,如果覆寫,則崩潰。

為解決此問題,可給塊對象發(fā)送copy消息拷貝。把塊從棧復(fù)制到堆,拷貝后的塊可以在定義它的那個范圍之外使用。并且,一旦復(fù)制到堆上,塊就成了帶引用計數(shù)的對象了。后續(xù)的復(fù)制都不會真的執(zhí)行復(fù)制,只是遞增塊對象的引用計數(shù)。堆上的塊服從引用計數(shù)規(guī)則,棧上的塊無需明確釋放,因為棧內(nèi)存本來就自動回收。

//拷貝之后,代碼就沒有上面的安全問題了
void (^block)(void);
if (addtional % 2 == 0) {
    block  = [^{
        NSLog(@"block a");
    } copy];
} else {
    block = [^{
        NSLog(@"block b");
    } copy];
}
block();

除了棧塊和堆塊之外,還有全局塊(global block)。這種塊不會捕捉任何狀態(tài)(比如外圍變量等),運行時也無需有狀態(tài)來參與,塊所使用的整個內(nèi)存區(qū)域,在編譯期已經(jīng)完全確定了,因此,全局塊可以聲明在全局內(nèi)存里,而不需要在每次用到的時候與棧內(nèi)創(chuàng)建。

另外,全局塊的拷貝操作是空操作,因為全局塊不能為系統(tǒng)所回收,相當(dāng)于單例。

void (^block) (void) = ^{
    NSLog(@"Block");
};

由于運行該塊所需的全部信息都能在編譯期確定,所以可把它做成全局塊。這完全是優(yōu)化技術(shù):若把如此簡單的塊當(dāng)成復(fù)雜的塊處理,那就會在復(fù)制及丟棄該塊時執(zhí)行一些無謂的操作。


  • 塊是C、C++、OC中的詞法閉包
  • 塊可以接受參數(shù),可以返回值
  • 塊可以分配在棧上、堆上,也可以是全局的。分配在棧上可拷貝到堆里,這樣的話就和標(biāo)準(zhǔn)OC對象一樣,具備引用計數(shù)。

38、為常用的塊類型創(chuàng)建typedef

C語言中的類型定義(type definition)特性,typedef關(guān)鍵字用于給類型重新起一個別名。

可以為塊起一個別名:

typedef int (^ZYDBlock)(BOOL flag,int value);


ZYDBlock sBlock = ^(BOOL flag,int value) {
    int result = 0;
    return result;
};
    
sBlock(YES,12);

實現(xiàn)塊的方法,如果返回值不對,就會報相應(yīng)錯誤:
Incompatible block pointer types initializing '__strong ZYDBlock' (aka 'int (^__strong)(BOOL, int)') with an expression of type 'void (^)(BOOL, int)'
實現(xiàn)塊的方法,輸入類型不對,報錯:
Incompatible block pointer types initializing '__strong ZYDBlock' (aka 'int (^__strong)(BOOL, int)') with an expression of type 'int (^)(int)'

通過這項特性,可以把使用塊的API做的更為易用些。類里的某些方法可能需要用塊來做參數(shù),不如執(zhí)行異步任務(wù)時所用的completion handler,參數(shù)就是塊,遇到這種情況,都可以通過定義別名使代買變得更為易讀。

#import <Foundation/Foundation.h>

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

@interface ZYDNetWorkFetcher : NSObject

- (void)startWithCompletionHandle:(ZYDCompletionHandler)completion;

@end

如果不定義塊類型別名,則是這樣的:

- (void)startWithCompletionHandles:(void (^)(NSData *data,NSError *error))completion;
相對而言比較復(fù)雜,不符合以往的方法定義習(xí)慣。

定義參數(shù)方法所用的塊類型語法,可定義變量時不同。

使用類型定義還有個好處,當(dāng)重構(gòu)塊的類型簽名時會很方便。比如說要添加一個參數(shù),只需要修改類型定義語句即可:

typedef void (^ ZYDCompletionHandler) (NSData *data,NSError *error,NSTimeInterval duration);
修改之后,凡是用了這個類型定義的地方,都無法編譯,需要逐個修復(fù)。若不用類型定義,直接寫塊類型,那么代碼中修改就多了,同時容易漏下一兩處,出現(xiàn)問題。

最好在使用塊類型的的類中定義這些typedef,而且還應(yīng)該把這個類的名字加在由typedef所定義的新類型名前面,可以闡明塊的用途,還可以用typedef給同一個塊簽名類型創(chuàng)建數(shù)個別名。在這件事上,多多益善。


  • 以typedef重新定義塊類型,可令塊變量用起來更簡單。
  • 定義新類型時應(yīng)遵從命名習(xí)慣,勿使其名稱與別的類型相沖突。
  • 不妨為同一個塊簽名明提供多個類型別名。如果重構(gòu)代碼使用了塊類型的某被別名,那么只需要修改相應(yīng)typedef中的塊簽名即可,無需改動其他typedef。

39、用handler塊降低代碼分散程度

為用戶界面編碼時,一種常用的范式就是異步執(zhí)行任務(wù)(perform task asynchronously)。這種范式的好處是:處理用戶界面的顯示已觸摸操作所用的線程,不會因為要執(zhí)行I/O或網(wǎng)絡(luò)通信這類耗時的任務(wù)而阻塞。這個線程通常稱為主線程(main thread)。

iOS系統(tǒng),系統(tǒng)監(jiān)控器(system watchdog)在發(fā)現(xiàn)某個應(yīng)用程序的主線程已經(jīng)阻塞額一段時間之后,就會令其終止。所以避免一些任務(wù)阻塞主線程,需要采用一些異步的方式處理耗時任務(wù)。

異步方法在執(zhí)行完任務(wù)之后,需要以某種手段通知相關(guān)代碼。實現(xiàn)此功能的方法有很多。

  • 1、委托協(xié)議

聲明代理協(xié)議

#import <Foundation/Foundation.h>

@class ZYDNetWorkFetcher;

@protocol ZYDNetworkFetcherDelegate <NSObject>

- (void)networkFetcher:(ZYDNetWorkFetcher *)fetcher didFinishWithData:(NSData *)data;

@end

被委托對象

#import <Foundation/Foundation.h>
#import "ZYDNetworkFetcherDelegate.h"

@interface ZYDNetWorkFetcher : NSObject

@property (nonatomic,weak) id<ZYDNetworkFetcherDelegate>delegate;
@end

委托對象

#import "ViewController.h"
#import "ZYDNetWorkFetcher.h"

@interface ViewController ()<ZYDNetworkFetcherDelegate>

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    ZYDNetWorkFetcher *fetcher = [[ZYDNetWorkFetcher alloc] init];
    fetcher.delegate = self;
}


#pragma mark -- ZYDNetworkFetcher Delegat
- (void)networkFetcher:(ZYDNetWorkFetcher *)fetcher didFinishWithData:(NSData *)data {
    
}

@end

遇到錯誤:

Undefined symbols for architecture x86_64:
  "l_OBJC_PROTOCOL_$_ZYDNetworkFetcherDelegate", referenced from:
      l_OBJC_CLASS_PROTOCOLS_$_ViewController in ViewController.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

報錯原因:
在代理頭文件里誤用@protocol ZYDNetworkFetcherDelegate;應(yīng)該是造成了命名沖突,同時沒有引入ZYDNetworkFetcherDelegate.h的緣故。刪除該行代碼,重新引入代理頭文件#import "ZYDNetworkFetcherDelegate.h"解決問題。

這種方法確實可行,沒有錯誤,不過改由塊來寫的話,更清晰。

#import <Foundation/Foundation.h>

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

@interface ZYDNetWorkFetcher : NSObject

- (void)startWithCompletionHandle:(ZYDCompletionHandler)completion;
@end
ZYDNetWorkFetcher *fetcher = [[ZYDNetWorkFetcher alloc] init];
    
[fetcher startWithCompletionHandle:^(NSData *data, NSError *error) {
    //處理數(shù)據(jù)和錯誤
}];

將異步執(zhí)行完畢后所需要運行的業(yè)務(wù)邏輯和啟動異步任務(wù)所用的的代碼放在一起,更整潔。由于塊聲明在創(chuàng)建獲取器的范圍內(nèi),所以他可以訪問此范圍內(nèi)的所有變量。

委托模式有兩個缺點:如果類要分別使用多個獲取器下載不同數(shù)據(jù),那么就得在delegate回調(diào)方法里傳入的獲取器參數(shù)來切換。這么做,不僅會讓delegate回調(diào)方法變得很長,而且還要把網(wǎng)絡(luò)數(shù)據(jù)獲取器對象保存為實例變量,以便在判斷語句中使用。這會使類的代碼激增。代碼塊的好處是:無需保存獲取器,也無需在回調(diào)方法里切換。每個completion handler的業(yè)務(wù)邏輯,都是和相關(guān)的獲取器對象一起來定義的。

另外代碼塊還可以處理不同的數(shù)據(jù),比如將正確結(jié)果與錯誤分開處理:

#import <Foundation/Foundation.h>

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

typedef void (^ ZYDCompletionFailureHandler) (NSError *error);

@interface ZYDNetWorkFetcher : NSObject

- (void)startWithCompletionSuccess:(ZYDCompletionSuccessHandler)successHandler failure:(ZYDCompletionFailureHandler)failureHandler;

@end

ZYDNetWorkFetcher *fetcher = [[ZYDNetWorkFetcher alloc] init];
    
[fetcher startWithCompletionSuccess:^(NSData *data) {
    
} failure:^(NSError *error) {
    
}];

只用一個塊處理返回結(jié)果的好處:

1、處理更為靈活,比如傳入錯誤信息時,可以把數(shù)據(jù)也傳進(jìn)來,尤其是在下載過程中,可以利用已經(jīng)下載的數(shù)據(jù),做些處理。

2、調(diào)用API的代碼可能會在處理成功響應(yīng)的過程中發(fā)現(xiàn)錯誤。

一般情況下,使用一個塊來處理成功與失敗的情況。

基于handler設(shè)計API還有一個原因,就是某些代碼必須運行在特定的線程上。可以設(shè)計自己的PAI,根據(jù)API所處的細(xì)節(jié)層次,選做那個操作隊列甚至GCD隊列來作為參數(shù)。


  • 在創(chuàng)建對象時,可以使用內(nèi)聯(lián)的handler塊將相關(guān)業(yè)務(wù)一并聲明。
  • 在有多個實例需要監(jiān)控時,如果采用委托模式,那么經(jīng)常需要根據(jù)傳入的對象來切換,如果該有handler來實現(xiàn),可直接將塊與相關(guān)對象放在一起。
  • 設(shè)計API時如果用到了handler塊,那么可以增加一個參數(shù),使調(diào)用者可以通過此參數(shù)來決定應(yīng)該把這個塊安排在哪個隊列上執(zhí)行。

40、用塊引用其所屬對象時不要出現(xiàn)保留環(huán)

即不要出現(xiàn)循環(huán)引用。

  • 問題:
// ZYDNetWorkFetcher.m
@interface ZYDNetWorkFetcher()
@property (nonatomic,copy) ZYDCompletionHandler completeHandler;

@property (nonatomic,strong) NSData *downloadData;

@property (nonatomic,strong,readwrite) NSURL *url;
@end

@implementation ZYDNetWorkFetcher

- (instancetype)initWithUrl:(NSURL *)url {
    if (self = [super init]) {
        _url = url;
    }
    return self;
}

- (void)startWithCompletionHandle:(ZYDCompletionHandler)completion {
    self.completeHandler = completion;
}

- (void)p_requestCompleted {
    if (_completeHandler) {
        _completeHandler(_downloadData,nil);
    }
}

// viewController.m
@interface ViewController ()<ZYDNetworkFetcherDelegate>
{
    ZYDNetWorkFetcher *_fetcher;
    // self 持有fetcher
    NSData *_fetcherData;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _fetcher = [[ZYDNetWorkFetcher alloc] init];
    
    [_fetcher startWithCompletionHandle:^(NSData *data, NSError *error) {
        //處理數(shù)據(jù)和錯誤
        self->_fetcherData = data;
        // _fetcher持有 self
    }];
}

有一點說明:completion handler中引用實例變量時,會捕獲self變量。

  • 解決方式:
    要么令_fetcher不再引用獲取器,要么令獲取器的CompletionHandle不再持有handler塊。

調(diào)用方法更改,解開獲取器與self的一個環(huán)。

NSURL *url = [NSURL URLWithString:@""];
ZYDNetWorkFetcher *fetcher = [[ZYDNetWorkFetcher alloc] initWithUrl:url];
[fetcher startWithCompletionHandle:^(NSData *data, NSError *error) {
    //處理數(shù)據(jù)和錯誤
    self->_fetcherData = data;
}];

獲取器與completionHandler的環(huán),運行過completion handler后,不在保留handler。

- (void)p_requestCompleted {
    if (_completeHandler) {
        _completeHandler(_downloadData,nil);
    }
    self.completeHandler = nil;
}

設(shè)計API用到了這樣的回調(diào)塊,就很容易形成保留環(huán),必須注意這個問題。一般來說,只要適時清理掉環(huán)中的而某個引用,即可解決此問題。

要想清楚塊可能會捕獲并保留哪些對象,如果這些對象又直接或間接保留了塊,那么就要考慮怎樣在適當(dāng)?shù)臅r間解除保留環(huán)。


  • 如果塊所捕獲的對象直接或間接地保留了塊本身,那么就得當(dāng)心保留環(huán)問題。
  • 一定要找個適當(dāng)?shù)臋C(jī)會解除保留環(huán),而不能把責(zé)任推給API的調(diào)用者。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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