《Effective Objective-C 2.0》6.塊與大中樞派發(fā)

第6章 塊與大中樞派發(fā)

block顿锰、塊呛哟、Block 塊癣诱、Block 對象在大多數(shù) Objective-C 文檔中語義相同。

block 是一種可在 C龄章、C++ 及 Objective-C 代碼中使用的“詞法閉包"(lexical closure)吃谣,借由此機制,開發(fā)者可將代碼像對象一樣傳遞做裙,令其在不同環(huán)境(context)下運行岗憋。還有個關鍵的地方是,在定義 block 的范圍內锚贱,它可以訪問到其中的全部變量仔戈。

GCD 是一種與 block 有關的技術,它提供了對線程的抽象,而這種抽象則基于“派發(fā)隊列” (dispatch queue) 监徘。開發(fā)者可將塊排入隊列中晋修,由GCD負責處理所有調度事宜。GCD會根據(jù)系統(tǒng)資源情況耐量,適時地創(chuàng)建飞蚓、復用、摧毀后臺線程(background thread)廊蜒,以便處理每個隊列趴拧。 此外,使用GCD還可以方便地完成常見編程任務山叮,比如編寫 “只執(zhí)行一次的線程安全代碼” (thread-safe single-code execution)著榴,或者根據(jù)可用的系統(tǒng)資源來并發(fā)執(zhí)行多個操作。

第37條:理解“塊”這一概念

塊的基礎知識

簡單的 block:

^{
    //Block implementation here
};

block 語法結構:

typedef returnType(^name)(arguments);

定義一個名為 someBlock 的變量:

void (^someBlock)() = ^{
    //Block implementation here
};

block 的強大之處在于屁倔,在 block 內部可以訪問 block 外部變量:

// 將變量聲明為 __block 之后才可以在 Block 內部對此變量進行修改
__block int additional = 5;
// 聲明Block塊
int (^addBlock)(int a, int b) = ^(int a, int b) {
    additional = 10;
    return a + b + additional;
};
// 使用Block塊
int add = addBlock(2, 5);
  • 如果block所捕獲的變量是對象類型脑又,那么就會自動保留它。系統(tǒng)在釋放這個塊的時候锐借,也會將其一并釋放问麸。
  • block本身可視為對象,它也有引用計數(shù)钞翔。
  • 如果將block定義在 Objective-C 類的實例方法中严卖,那么除了可以訪問類的所有實例變童之外,還可以使用 self 變量布轿。block總能修改實例變量哮笆,所以在聲明時無須加 _block。不過汰扭,如果通過讀寫操作捕獲了實例變量稠肘,那么也會自動把 self 變量一并捕獲,因為實例變量是與self所指代的實例關聯(lián)在一起的萝毛。
- (void)anInstanceMethod {
    //...
    void (^someBlock)() = ^{
        _anInstanceVariable = @"Something";
        NSLog(@"_anInstanceVariable = %@",_anInstanceVariable);
    };
    //...
}
  • 在block中项阴,直接訪問實例變量和通過 self 來訪問該實例變量是等效的。
  • self 也是個對象珊泳,因而 block 在捕獲它時也會將其保留鲁冯。如果 self 所指代的那個對象同時也保留了塊,那么這種情況通常就會導致引用循環(huán)色查。

塊的內部結構

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. */
};
  • isa指針:指向表明該block類型的類。
  • flags:按bit位表示一些block的附加信息撞芍,比如判斷block類型秧了、判斷block引用計數(shù)、判斷block是否需要執(zhí)行輔助函數(shù)等序无。
  • reserved:保留變量验毡,我的理解是表示block內部的變量數(shù)衡创。
  • invoke:函數(shù)指針,指向具體的block實現(xiàn)的函數(shù)調用地址晶通。
  • descriptor:block的附加描述信息璃氢,比如保留變量數(shù)、block對象的大小狮辽、進行copydispose的輔助函數(shù)指針一也。
  • variables:即捕獲到的變量,因為block有閉包性喉脖,所以可以訪問block外部的局部變量椰苟。這些variables就是復制到結構體中的外部局部變量或變量的地址。

全局塊树叽、棧塊及堆塊

  • 雖然 block 是對象舆蝴,但是其所占的內存區(qū)域是分配在中的。這就是說题诵,塊只在定義它的那個范圍內有效洁仗。
  • 給 block 對象發(fā)送 copy 消息可以把 Block 從復制到⌒远В拷貝后的塊赠潦,可以在定義它的那個范圍之外使用。
void (^block)();
if (/** condition */) {
    block = [^{
        NSLog(@"Block A");
    } copy];
}else {
    block = [^{
        NSLog(@"Block B");
    } copy];
}
block();
  • 全局塊(global block)不會捕捉任何狀態(tài)(比如外圍的變量等)篷店,運行時也無須有狀態(tài)來參與祭椰。塊所使用的整個內存區(qū)域,在編譯期已經(jīng)完全確定了疲陕,因此方淤,全局塊可以聲明在全局內存里,而不需要在每次用到的時候于棧中創(chuàng)建蹄殃。另外携茂,全局塊的拷貝操作是個空操作,因為全局塊決不可能為系統(tǒng)所回收诅岩。這種塊實際上相當于單例讳苦。下面就是個全局塊:
void (^block) () = ^{
    NSLog(@"This is a block");
);
  • 全局的靜態(tài) block: _NSConcreteGlobalBlock類型的block要么是空block吩谦,要么是不訪問任何外部變量的block鸳谜。它既不在棧中,也不在堆中式廷,我理解為它可能在內存的全局區(qū)咐扭。
  • 保存在棧中的block:_NSConcreteStackBlock類型的block有閉包行為,也就是有訪問外部變量,并且該block只且只有有一次執(zhí)行蝗肪,因為棧中的空間是可重復使用的袜爪,所以當棧中的block執(zhí)行一次之后就被清除出棧了,所以無法多次使用薛闪。
  • 保存在堆中的block:_NSConcreteMallocBlock類型的block有閉包行為辛馆,并且該block需要被多次執(zhí)行。當需要多次執(zhí)行時豁延,就會把該block從棧中復制到堆中昙篙,供以多次執(zhí)行。

要點

  • Clang 是開發(fā) Mac OS X 及 iOS 程序所用的編譯器术浪。
  • block 塊是 C瓢对、C++、Objective-C 中的詞法閉包胰苏。
  • block 塊可以接收參數(shù)硕蛹,也可以有返回值。
  • block 塊可以分配在椝恫ⅲ或堆上法焰,也可以是全局的。分配在棧上的 block 塊可拷貝到堆里倔毙,這樣的話埃仪,就和標準的 Objective-C 對象一樣,具備引用計數(shù)了陕赃。

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

代碼塊便捷寫法:typedefBlock

typedef <#returnType#>(^<#name#>)(<#arguments#>);

示例一:

// 定義 Block 塊
typedef int(^EOCSomeBlock)(BOOL flag, int value);

// 使用 Block 塊
EOCSomeBlock blcok = ^(BOOL flag, int value) {
    // Implementation
};

示例二:

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

// 方法使用 Block 塊作為參數(shù)
- (void)startingWithCompletionHandler:(EOCCompletionHandler)completion;

使用 typedef 類型定義還便于重構 block 的類型簽名卵蛉。

// 新增一個參數(shù),用以表示完成任務所花的時間
typedef void(^EOCCompletionHandler)
        (NSData *data, NSTimeInterval duration, NSError *error);

要點

  • typedef 重新定義 block 類型么库,可以令 block 變量用起來更加簡單傻丝。
  • 定義新類型時應遵循現(xiàn)有的命名習慣,勿使其名稱與別的的類型相沖突诉儒。
  • 不妨為同一個 block 簽名定義多個類型別名葡缰。如果要重構的代碼使用了 block 類型的某個別名,那么只需修改相應的 typedef 中的 block 簽名即可忱反,無需改動其他 typedef泛释。

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

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

異步方法在執(zhí)行完任務之后韭畸,需要以某種手段通知相關代碼宇智。實現(xiàn)此功能有很多辦法蔓搞。常用的技巧是設計一個委托協(xié)議(參見第23條)胰丁,令關注此事件的對象遵從該協(xié)議。對象成為delegate之后喂分,就可以在相關事件發(fā)生時(例如某個異步任務執(zhí)行完畢時)得到通知了锦庸。

Delegate 模式:

#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;
- (instancetype)initWithURL:(NSURL *)url;
- (void)start;
@end

其他類則可以像下面這樣使用此類所提供的 API 并遵守實現(xiàn)相應的 delegate 協(xié)議:

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

- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didFinishWithData:(NSData *)data {
    // deal with data
}

block 模式:

#import <Foundation/Foundation.h>

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

@interface EOCNetworkFetcher : NSObject
- (instancetype)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler;
@end

其他類獲取數(shù)據(jù):

- (void)fetchFooData {
    NSURL *url = [[NSURL alloc] initWithString:
                  @"https://www.pinterest.com"];
    EOCNetworkFetcher *fetcher =
        [[EOCNetworkFetcher alloc] initWithURL:url];
    // 調用 start 方法時直接以內聯(lián)形式定義 Completion Handler。
    [fetcher startWithCompletionHandler:^(NSData *data) {
        // deal with data
    }];
}

相比于委托模式蒲祈,block 模式可以使代碼更清晰整潔甘萧、API 更緊致、邏輯關聯(lián)性更強梆掸。

委托模式有個缺點:如果類要分別使用多個獲取器下載不同數(shù)據(jù)扬卷,那么就得在 delegate 回調方法里根據(jù)傳入的獲取器參數(shù)來切換。

而使用 block 來寫的好處是:無須保存獲取器酸钦,也無須在回調方法里切換怪得。每個 completion handler 的業(yè)務邏輯都是和相關的獲取器對象一起來定義的。

1. 分別用兩個處理程序來處理操作失敗和操作成功

這種 API 設計風格很好卑硫,由于成功和失敗的情況要分別處理徒恋,所以調用此 API 的代碼也就會按照邏輯,把應對成功和失敗情況的代碼分開來寫欢伏,這將令代碼更易讀懂入挣。而且,若有需要硝拧,還可以把處理失敗情況或成功情況所用的代碼省略径筏。

#import <Foundation/Foundation.h>

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

@interface EOCNetworkFetcher : NSObject
- (instancetype)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:
            (EOCNetworkFetcherCompletionHandler)handler
                    failureHandler:
            (EOCNetworkFetcherErrorHandler)failure;
@end

// 其他類使用:
- (void)fetchFooData {
    NSURL *url = [[NSURL alloc] initWithString:
                  @"https://www.pinterest.com"];
    EOCNetworkFetcher *fetcher =
        [[EOCNetworkFetcher alloc] initWithURL:url];
    [fetcher startWithCompletionHandler:^(NSData *data) {
        // deal with data
        
    } failureHandler:^(NSError *error) {
        // deal with error
        
    }];
}
2. 把處理失敗所需代碼與處理成功所用代碼,都封裝到同一個 completion handle 塊里

缺點:由于全部邏輯都寫在一起障陶,導致塊代碼冗長復雜滋恬。

優(yōu)點:能把所有業(yè)務邏輯都放在一起使其更加靈活。例如咸这,在傳入錯誤信息時夷恍,可以把數(shù)據(jù)也傳進來。有時數(shù)據(jù)正下載到一半媳维,突然網(wǎng)絡故障了酿雪。在這種情況下,可以把數(shù)據(jù)及相關的錯誤都回傳給塊侄刽。這樣的話指黎,completion handler 就能據(jù)此判斷問題并適當處理了,而且還可利用已下載好的這部分數(shù)據(jù)做些事情州丹。

總體來說醋安,筆者建議使用同一個塊來處理成功與失敗情況杂彭。

#import <Foundation/Foundation.h>

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

@interface EOCNetworkFetcher : NSObject
- (instancetype)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:
            (EOCNetworkFetcherCompletionHandler)handler;
@end

// 其他類使用:
- (void)fetchFooData {
    NSURL *url = [[NSURL alloc] initWithString:
                  @"https://www.pinterest.com"];
    EOCNetworkFetcher *fetcher =
        [[EOCNetworkFetcher alloc] initWithURL:url];
    // 需要在塊代碼中檢測傳人的error變量,并且要把所有邏輯代碼都放在一處
    [fetcher startWithCompletionHandler:^(NSData *data, NSError *error) {
        if (error) {
            // handle failure
        }else {
            // handle success
        }
    }];
}

基于 handler 來設計API還有個原因吓揪,就是某些代碼必須運行在特定的線程上亲怠,比如,Cocoa 與 Cocoa Touch 中的 UI 操作必須在主線程上執(zhí)行:

// NSNotificationCenter
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name
                             object:(nullable id)obj
                              queue:(nullable NSOperationQueue *)queue
                         usingBlock:(void (^)(NSNotification *note))block;

要點

  • 在創(chuàng)建對象時柠辞,使用內聯(lián)的 handler 塊將相關業(yè)務邏輯一并聲明团秽。
  • 在有多個實例需要監(jiān)控時,如果采用委托模式叭首,那么經(jīng)常需要根據(jù)傳入的對象來切換习勤,而若改用 handler 塊來實現(xiàn),則可直接將 block 與相關對象放在一起焙格。
  • 設計 API 時如果用到了 handler 塊图毕,那么可以增加一個參數(shù),使調用者可通過此參數(shù)來決定應該把 block 安排在哪個隊列上執(zhí)行眷唉。

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

使用 block 很容易導致循環(huán)引用:

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

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

@interface EOCNetworkFetcher : NSObject
@property (nonatomic, strong, readonly) NSURL *url;
- (instancetype)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 *downloadData;
@end

@implementation EOCNetworkFetcher

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

- (void)startWithCompletionHandler:
        (EOCNetworkFetcherCompletionHandler)completion {
    self.completionHandler = completion;
    // 開啟網(wǎng)絡請求
    // 設置 downloadData 屬性
    // 下載完成后予颤,以回調方式執(zhí)行 Block
    [self p_requestCompleted];
}

// 為了能在下載完成后通過 p_requestCompleted 方法執(zhí)行調用者所指定的塊,
// 需要把 completion handler 保存到實例變量
// ?? _networkFetcher → _completionHandler
- (void)p_requestCompleted {
    if (_completionHandler) {
        _completionHandler(_downloadData);
    }
}

@end

// 某個類可能會創(chuàng)建以上網(wǎng)絡數(shù)據(jù)獲取器對象厢破,并用其從URL中下載數(shù)據(jù):
@implementation EOCDataModel {
    // ?? EOCDataModel → _networkFetcher
    EOCNetworkFetcher *_networkFetcher;
    NSData *_fetchedData;
}

- (void)downloadData {
    NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com"];
    _networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    [_networkFetcher startWithCompletionHandler:^(NSData *data) {
        
        NSLog(@"Request URL %@ finished",_networkFetcher.url);
        // 因為 completion handler 塊要設置 _fetchedData 實例變量,所以它必須捕獲 self 變量荣瑟,而 self 指向 EOCDataModel 類
        // ?? _completionHandler → EOCDataModel
        _fetchedData = data;
      
        // ??等 completion handler 塊執(zhí)行完畢后,再打破保留環(huán)摩泪,以便使獲取器對象在handler 塊執(zhí)行期間保持存活狀態(tài)笆焰。
        _networkFetcher = nil;
    }];
}

問題:

  • 在上例中,唯有 completion handler 運行過后见坑,方能解除保留環(huán)嚷掠。若是completion handler—直不運行,那么保留環(huán)就無法打破荞驴,于是內存就會泄漏不皆。
  • 如果 completion handler 塊所引用的對象最終又引用了這個塊本身,那么就會出現(xiàn)保留環(huán)熊楼。
- (void)downloadData {
    NSURL *url = [[NSURL alloc] initWithString:
                  @"http://www.example.com"];
    EOCNetworkFetcher *networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    [networkFetcher startWithCompletionHandler:^(NSData *data) {
        // completionHandler → networkFetcher.url
        // networkFetcher → completionHandler
        NSLog(@"Request URL %@ finished",networkFetcher.url);
        _fetchedData = data;
    }];
}

解決方法:獲取器對象之所以要把 completion handler 塊保存在屬性里面霹娄,其唯一目的就是想稍后使用這個塊■昶可是犬耻,獲取器一旦運行過 completion handler 之后,就沒有必要再保留它了执泰。所以枕磁,只需將 p_requestCompleted 方法按如下方式修改即可:

- (void)p_requestCompleted {
    if (_completionHandler) {
        _completionHandler(_downloadData);
    }
    self.completionHandler = nil; // ??
}

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

要點

  • 如果 block 所捕獲的對象直接或間接的保留了 block 本身茸苇,那么就得當心循環(huán)引用的問題。
  • 一定要找個適當?shù)臅r機解除循環(huán)引用沦寂,而不能把責任推給 API 的調用者学密。

第41條:多用派發(fā)隊列,少用同步鎖

使用同步鎖實現(xiàn)同步機制:

  1. @synchronized 同步塊:

    - (void)synchronizedMethod {
        @synchronized (self) {
            // ...
            // 根據(jù)給定對象凑队,自動創(chuàng)建一個鎖则果,并等待塊中的代碼執(zhí)行完畢。
            // 濫用 @synchronized (self) 會降低代碼效率
            
        }   // 執(zhí)行到這段代碼結尾處漩氨,釋放鎖。
    }
    
  2. NSRecursiveLock 遞歸鎖

    • 線程能夠多次持有該鎖遗增, 而不會出現(xiàn)死鎖(deadlock)現(xiàn)象叫惊。
    • 在極端情況下,同步塊會導致死鎖做修。

GCD:串行并發(fā)隊列

@implementation HQLBlockObject {
    dispatch_queue_t _syncQueue;
}

    // 自定義并發(fā)隊列
    // ??注意到霍狰,文章中此處作者使用的是全局并發(fā)隊列,而在 Ray Wenderlich 的GCD系列教程中使用的是自定義并發(fā)隊列:
    // 原因在于:全局隊列中還可能有其他任務正在執(zhí)行饰及,一旦加鎖就會阻塞其他任務的正常執(zhí)行蔗坯,因此我們開辟一個新的自定義并發(fā)隊列專門處理這個問題。
    _syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", DISPATCH_QUEUE_CONCURRENT);

- (NSString *)someString {
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
    return localSomeString;
}

- (void)setSomeString:(NSString *)someString {
    dispatch_barrier_async(_syncQueue, ^{
        _someString = someString;
    })
}

把設置操作與獲取操作都安排在序列化的隊列里執(zhí)行燎含,這樣的話宾濒,所有針對屬性的訪問操作就都同步了。

柵欄塊

在隊列中屏箍,柵欄塊必須單獨執(zhí)行绘梦,不能與其他塊并行。這只對并發(fā)隊列有意義赴魁,因為串行隊列中的塊總是按順序逐個來執(zhí)行的卸奉。并發(fā)隊列如果發(fā)現(xiàn)接下來要處理的塊是個柵欄塊 (barrier block) ,那么就一直要等當前所有并發(fā)塊都執(zhí)行完畢颖御,才會單獨執(zhí)行這個柵欄塊榄棵。待柵欄塊執(zhí)行過后,再按正常方式繼續(xù)向下處理潘拱。

dispatch_barrier_sync(dispatch_queue_t  _Nonnull queue, ^{

})
dispatch_barrier_async(dispatch_queue_t  _Nonnull queue, ^{

})
- (NSString *)someString {
    // 后臺執(zhí)行
    _syncQueue = dispatch_get_global_queue(0, 0);
    
    __block NSString *localSomeString;
    // 同步后臺隊列
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
    return localSomeString;
}

- (void)setSomeString:(NSString *)someString {
    // 異步柵欄隊列
    dispatch_barrier_async(_syncQueue, ^{
       _someString = someString;
    });
}

要點

  • 派發(fā)隊列可用來表述同步語義疹鳄,這種做法要比使用 @synchronized 塊或 NSLock 對象更簡單。
  • 將同步與異步派發(fā)結合起來泽铛,可以實現(xiàn)與普通加鎖機制一樣的同步行為尚辑,而這么做卻不會阻塞執(zhí)行異步派發(fā)的線程。
  • 使用同步隊列及柵欄塊盔腔,可以令同步行為更加高效杠茬。

第42條:多用GCD月褥,少用 performSelector 系列方法

// 接受的參數(shù)就是要執(zhí)行的選擇子
- (id)performSelector:(SEL)aSelector;

該方法與直接調用選擇子等效:

[self performSelector:@selector(selectorMethod)];
[self selectorMethod];

特點:編譯器要等到運行期才能知道執(zhí)行的選擇子∑昂恚可以在動態(tài)綁定之上再次使用動態(tài)綁定宁赤,因而可以實現(xiàn)出下面這種功能:

SEL selector;
if ( /** condition A */ ) {
    selector = @selector(foo);
}else if ( /** condition B */ ) {
    selector = @selector(bar);
}else {
    selector = @selector(baz);
}
[self performSelector:selector];

不推薦使用 performSelector 方法的原因:

?? ARC 下會引起內存泄漏:編譯器并不知道將要執(zhí)行的選擇子是什么,因此栓票,也就不了解其方法簽名及返回值决左,甚至連是否有返回值都不清楚,而由于編譯器不知道方法名走贪,所以就沒辦法運用ARC的內存管理規(guī)則來判定返回值是不是應該釋放佛猛。鑒于此,ARC采用了比較謹慎的做法坠狡,就是不添加釋放操作继找。然而這么做可能導致內存泄漏,因為方法在返回對象時可能已經(jīng)將其保留了逃沿。

即使使用靜態(tài)分析器婴渡,也很難偵測到隨后的內存泄漏馋记。

??返回值只能是 void 或對象類型辅鲸,performSelector 方法的返回值類型是 id张惹。如果想返回整數(shù)或浮點數(shù)等類型的值歼狼,那么就需要執(zhí)行一些復雜的轉換操作了籽慢,而這種轉換很容易出錯怠堪。

如果返冋值的類型為C語言結構體弛饭,則不可以使用 performSelector 方法倘感。

??performSelector 方法還有諸多局限性置谦,傳入的參數(shù)類型必須是對象類型且最多只能接受2個參數(shù)堂鲤、具備延后執(zhí)行的方法無法處理帶有2個參數(shù)的選擇子。

下面是幾個使用 Block 的替代方案:

延后執(zhí)行

?推薦:

double delayInSeconds = 5.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW,
                                        delayInSeconds *NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^{
    [self doSomething];
});

?反對:

[self performSelector:@selector(doSomething)
             withObject:nil
             afterDelay:5.0];

主線程執(zhí)行

?推薦:

// 同步主線程(waitUntilDone:YES)
dispatch_sync(dispatch_get_main_queue(), ^{
    [self doSomething];
});

// 異步主線程(waitUntilDone:NO)
dispatch_async(dispatch_get_main_queue(), ^{
    [self doSomething];
});

?反對:

[self performSelectorOnMainThread:@selector(doSomething)
                       withObject:nil
                    waitUntilDone:NO];

要點

  • performSelector 系列方法在內存管理方面容易有疏失媒峡。它無法確定將要執(zhí)行的 selector 具體是什么瘟栖,因而ARC編譯器就無法插入適當?shù)膬却婀芾矸椒ā?/li>
  • performSelector 系列方法所能處理的 selector 太過局限了,selector 的返回值類型及發(fā)送給方法的參數(shù)個數(shù)都受到限制谅阿。
  • 如果想把任務放在另一個線程上執(zhí)行半哟,那么最好不要用 performSelector 系列方法而是應該把任務封裝到 block 里然后調用 GCD 機制的相關方法來實現(xiàn)。

第43條:掌握 GCD 及操作隊列的使用時機

GCD & NSOperation

  • GCD 是純 C 的 API签餐,而 NSOperation(操作隊列)則是 Objective-C 的對象寓涨。

  • 在GCD中,任務用 Block 來表示氯檐,而 Block 是個輕量級數(shù)據(jù)結構(參見第37條)戒良。與之相反,“操作"(operation) 則是個更為重量級的 Objective-C 對象

  • 用 NSOperationQueue 類的 addOperationWithBlock: 方法搭配 NSBlockOperation 類來使用操作隊列冠摄,其語法與純 GCD 方式非常類似糯崎。使用 NSOperation 及 NSOperationQueue 的好處如下:

    • 取消某個操作几缭。運行任務之前, 可以在 NSOperation 對象上調用 cancel 方法取消任務執(zhí)行。
    • 指定操作間的依賴關系沃呢。
    • 通過鍵值觀測機制(簡稱 KVO)監(jiān)控 NSOperation 對象的屬性年栓。
    • 指定操作的優(yōu)先級。操作的優(yōu)先級表示此操作與隊列中其他操作之間的優(yōu)先關 系薄霜。優(yōu)先級高的操作先執(zhí)行某抓,優(yōu)先級低的后執(zhí)行。
    • 重用 NSOperation 對象惰瓜。
  • 操作隊列有很多地方勝過派發(fā)隊列否副。操作隊列提供了多種預設的執(zhí)行任務的方式,開發(fā)者可以直接使用鸵熟。

  • 有一個API選用了操作隊列而非派發(fā)隊列副编,這就是 NSNotificationCemer,開發(fā)者可通過其中的方法來注冊監(jiān)聽器流强,以便在發(fā)生相關事件時得到通知,而這個方法接受的參數(shù)是塊呻待,不是選擇子打月。

    - (id <NSObject>)addObserverForName:(nullable NSNotificationName)name
                                 object:(nullable id)obj
                                  queue:(nullable NSOperationQueue *)queue
                             usingBlock:(void (^)(NSNotification *note))bloc;
    
  • 應該盡可能選用高層API,只在確有必要時才求助于底層蚕捉。筆者也同意這個說法奏篙,但我并不盲從。某些功能確實可以用高層的Objective-C方法來做迫淹,但這并不等于說它就一定比底層實現(xiàn)方案好秘通。要想確定哪種方案更佳,最好還是測試一下性能敛熬。

要點

  • 在解決多線程與任務管理問題時肺稀,派發(fā)隊列并非唯一方案。
  • 操作隊列提供了一套高層的 Objective-C API应民,能實現(xiàn)純 GCD 所具備的絕大部分功能话原,而且還能完成一些更為復雜的操作,那些操作若改用GCD來實現(xiàn)诲锹,則需另外編寫代碼繁仁。

第44條:通過 Dispatch Group 機制,根據(jù)系統(tǒng)資源狀況來執(zhí)行任務

dispatch group 是 GCD 的一項特性归园,能夠把任務分組黄虱。調用者可以等待這組任務執(zhí)行完畢,也可以在提供回調函數(shù)之后繼續(xù)往下執(zhí)行庸诱,這組任務完成時捻浦,調用者會得到通知晤揣。這個功能有許多用途,其中最重要默勾、最值得注意的用法碉渡,就是把將要并發(fā)執(zhí)行的多個任務合為一組,于是調用者就可以知道這些任務何時才能全部執(zhí)行完畢母剥。比方說滞诺,可以把壓縮一系列文件的任務表示成 dispatch group

創(chuàng)建及使用 dispatch group

dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
    // 并行執(zhí)行的線程一
});
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
    // 并行執(zhí)行的線程二
});
dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
   // 匯總結果
});

給任務編組的兩種方法:

// 1.比 dispatch_async 多一個參數(shù)环疼,用于表示待執(zhí)行的塊所歸屬的組
dispatch_group_async(dispatch_group_t group,
                     dispatch_queue_t queue,
                     dispatch_block_t block);

// 2.dispatch_group_enter习霹、dispatch_group_leave 需要成對使用
dispatch_group_enter(dispatch_group_t group); // 使分組里正要執(zhí)行的任務數(shù)遞增,
dispatch_group_leave(dispatch_group_t group); // 使分組里正要執(zhí)行的任務數(shù)遞減.

dispatch_group_wait 函數(shù)用于等待 dispatch group 執(zhí)行完畢:

dispatch_group_wait(dispatch_group_t group, 
                    dispatch_time_t timeout);

示例:

令數(shù)組中的每個對象都執(zhí)行某項任務炫隶,并且等待所有任務執(zhí)行完畢:

dispatch_queue_t queue =
    dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
for (id object in collection) {
    dispatch_group_async(group, queue, ^{
        [object performTask];
    });
}

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// 任務執(zhí)行完畢后繼續(xù)操作

// 若當前線程不應阻塞淋叶,則可以使用 dispatch_group_notify 代替 dispatch_group_wait
dispatch_queue_t notifyQueue = dispatch_get_main_queue();
dispatch_group_notify(group, notifyQueue, ^{
    // 任務執(zhí)行完畢后繼續(xù)操作
});

開發(fā)者未必總需要使用 dispatch group。有時候采用單個隊列搭配標準的異步派發(fā)伪阶,也可實現(xiàn)同樣效果:

// 自定義串行隊列
dispatch_queue_t queue =
    dispatch_queue_create("com.effecitveobjectivec.queue", NULL);
for (id object in collection) {
    dispatch_async(queue, ^{
        [object performTask];
    });
}
dispatch_async(queue, ^{
    // 任務執(zhí)行完畢后繼續(xù)操作
});

根據(jù)系統(tǒng)資源狀況來執(zhí)行任務:

為了執(zhí)行隊列中的塊煞檩,GCD會在適當?shù)臅r機自動創(chuàng)建新線程或復用舊線程。如果使用并發(fā)隊列栅贴,那么其中有可能會有多個線程斟湃,這也就意味著多個塊可以并發(fā)執(zhí)行。在并發(fā)隊列中檐薯,執(zhí)行任務所用的并發(fā)線程數(shù)量凝赛,取決于各種因素,而GCD主要是根據(jù)系統(tǒng)資源狀況來判定這些因素的坛缕。假如CPU有多個核心墓猎,并且隊列中有大量任務等待執(zhí)行,那么GCD就可能會給該隊列配備多個線程赚楚。通過dispatch group所提供的這種簡便方式毙沾,既可以并發(fā)執(zhí)行一系列給定的任務,又能在全部任務結束時得到通知直晨。由于GCD 有并發(fā)隊列機制搀军,所以能夠根據(jù)可用的系統(tǒng)資源狀況來并發(fā)執(zhí)行任務。而開發(fā)者則可以專注于業(yè)務邏輯代碼勇皇,無須再為了處理并發(fā)任務而編寫復雜的調度器罩句。

遍歷某個 collection ,并在其每個元素上執(zhí)行任務敛摘,而這也可以用另外一個 GCD 函數(shù)來實現(xiàn):

dispatch_apply(size_t iterations,
               dispatch_queue_t queue,
               void (^block)(size_t));

此函數(shù)會將塊反復執(zhí)行一定的次數(shù)门烂,每次傳給塊的參數(shù)值都會遞增,從0開始,直至 “ iterations-1 ”屯远。

// 自定義串行隊列
dispatch_queue_t queue =
    dispatch_queue_create("com.effecitveobjectivec.queue", NULL);    
dispatch_apply(10, queue, ^(size_t) {
    // Perform Task:0~9
});

與 for 循環(huán)不同的是蔓姚, dispatch_apply 所用的隊列可以是并發(fā)隊列。但是 dispatch_apply 會持續(xù)阻塞慨丐,直到所有任務都執(zhí)行完畢為止坡脐。

假如把塊派給了當前隊列(或者體系中高于當前隊列的某個串行隊列),就將導致死鎖房揭。若想在后臺執(zhí)行任務备闲,則應使用 dispatch group

要點

  • 一系列任務可歸入一個 dispatch group 之中捅暴。開發(fā)者可以在這組任務執(zhí)行完畢時獲得通知恬砂。
  • 通過 dispatch group,可以在并發(fā)式派發(fā)隊列里同時執(zhí)行多項任務蓬痒。此時GCD會根據(jù)系統(tǒng)資源狀況來調度這些并發(fā)執(zhí)行的任務泻骤。開發(fā)者若自己來實現(xiàn)此功能,則需編寫大量代碼梧奢。

第45條:使用 dispatch_once 來執(zhí)行只需運行一次的線程安全代碼

單例模式:

+ (instancetype)sharedInstance {
    static EOCClass *sharedInstance = nil;
    // 為保證線程安全狱掂,將創(chuàng)建單例實例的代碼包裹在同步塊里。
    @synchronized (self) {
        if (!sharedInstance) {
            sharedInstance = [[self alloc] init];
        }
    }
    return sharedInstance;
}

dispatch_once 函數(shù):

_dispatch_once(dispatch_once_t *predicate,
               dispatch_block_t block)

該函數(shù)保證相關的塊必定會執(zhí)行亲轨,且僅執(zhí)行一次符欠。首次調用該函數(shù)時,必然會執(zhí)行塊中的代碼瓶埋,最重要的一點在于,此操作完全是線程安全的诊沪。

+ (instancetype)sharedInstance {
    static EOCClass *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}
  • 使用 dispatch_once 可以簡化代碼并且徹底保證線程安全养筒。
  • 由于每次調用時都必須使用完全相同的標記, 所以標記要聲明成 static。把該變量定義在static 作用域中端姚,可以保證編譯器在每次執(zhí)行 sharedlnstance 方法時都會復用這個變量晕粪,而不會創(chuàng)建新變量。
  • 此外渐裸,dispatch_once 更高效巫湘。 @synchronized 使用了重量級的同步機制,每次運行代碼前都要獲取鎖昏鹃。而 dispatch_once 采用原子訪問(atomic access)來查詢標記尚氛,以判斷其所對應的代碼原來是否已經(jīng)執(zhí)行過。

要點

  • 經(jīng)常需要編寫只需執(zhí)行一次的線程安全代碼洞渤。通過 GCD 所提供的 dispatch_once 函數(shù)阅嘶,很容易就能實現(xiàn)此功能。
  • 標記應該聲明在 staticglobal 作用域中,這樣的話讯柔,在把只需執(zhí)行一次的 block 傳給dispatch_once 函數(shù)時抡蛙,傳進去的標記也是相同的。

第46條:不要使用 dispatch_get_current_queue

// 此函數(shù)返回當前正在執(zhí)行代碼的隊列魂迄。
// This function is deprecated and will be removed in a future release.
dispatch_get_current_queue(void);

Tips:iOS 系統(tǒng)從 6.0 版本起粗截,已經(jīng)正式棄用此函數(shù)了。

要點

  • dispatch_get_current_queue 函數(shù)的行為常常與開發(fā)者所預期的不同捣炬。此函數(shù)已廢棄熊昌,只應做調試之用。
  • 由于派發(fā)隊列是按層級來組織的遥金,所以無法單用某個隊列對象來描述當前隊列這一概念浴捆。
  • dispatch_get_current_queue 函數(shù)用于解決由不可重入的代碼所引發(fā)的死鎖,然而能用此函數(shù)解決的問題稿械,通常也能改用“隊列特定數(shù)據(jù)”來解決选泻。

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市美莫,隨后出現(xiàn)的幾起案子页眯,更是在濱河造成了極大的恐慌,老刑警劉巖厢呵,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窝撵,死亡現(xiàn)場離奇詭異,居然都是意外死亡襟铭,警方通過查閱死者的電腦和手機碌奉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來寒砖,“玉大人赐劣,你說我怎么就攤上這事×ǘ迹” “怎么了魁兼?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長漠嵌。 經(jīng)常有香客問我咐汞,道長,這世上最難降的妖魔是什么儒鹿? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任化撕,我火速辦了婚禮,結果婚禮上挺身,老公的妹妹穿的比我還像新娘侯谁。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布墙贱。 她就那樣靜靜地躺著热芹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪惨撇。 梳的紋絲不亂的頭發(fā)上伊脓,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天,我揣著相機與錄音魁衙,去河邊找鬼报腔。 笑死,一個胖子當著我的面吹牛剖淀,可吹牛的內容都是我干的纯蛾。 我是一名探鬼主播,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼纵隔,長吁一口氣:“原來是場噩夢啊……” “哼翻诉!你這毒婦竟也來了?” 一聲冷哼從身側響起捌刮,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤碰煌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后绅作,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體芦圾,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年俄认,在試婚紗的時候發(fā)現(xiàn)自己被綠了个少。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡眯杏,死狀恐怖稍算,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情役拴,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布钾埂,位于F島的核電站河闰,受9級特大地震影響,放射性物質發(fā)生泄漏褥紫。R本人自食惡果不足惜姜性,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望髓考。 院中可真熱鬧部念,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至乌询,卻和暖如春榜贴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背妹田。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工唬党, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鬼佣。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓驶拱,卻偏偏與公主長得像,于是被迫代替她去往敵國和親晶衷。 傳聞我的和親對象是個殘疾皇子蓝纲,可洞房花燭夜當晚...
    茶點故事閱讀 43,514評論 2 348

推薦閱讀更多精彩內容