《Effective Objective-C》乾貨三部曲(三):技巧篇

本篇是《Effective Objective-C 》乾貨三部曲的了最後一篇:技巧篇。這一篇總結了這本書中一些關於開發技巧以及偏向「設計模式」的知識點。git

不知道筆者所說的三部曲的童鞋們能夠看一下這張圖:程序員

三部曲分佈圖

前兩篇傳送門:github

《Effective Objective-C 》乾貨三部曲(一):概念篇編程

《Effective Objective-C 》乾貨三部曲(二):規範篇設計模式

第9條 以「類族模式「隱藏實現細節

在iOS開發中,咱們也會使用「類族」(class cluster)這一設計模式,經過「抽象基類」來實例化不一樣的實體子類。數組

舉個🌰 :安全

+ (UIButton *)buttonWithType:(UIButtonType)type;
複製代碼

在這裏,咱們只須要輸入不一樣的按鈕類型(UIButtonType)就能夠獲得不一樣的UIButton的子類。在OC框架中廣泛使用這一設計模式。bash

爲何要這麼作呢?

筆者認爲這麼作的緣由是爲了「弱化」子類的具體類型,讓開發者無需關心建立出來的子類具體屬於哪一個類。(這裏以爲還有點什麼,可是尚未想到,歡迎補充!)服務器

咱們能夠看一個具體的例子: 對於「員工」這個類,能夠有各類不一樣的「子類型」:開發員工,設計員工和財政員工。這些「實體類」能夠由「員工」這個抽象基類來得到:網絡

1. 抽象基類

//EOCEmployee.h

typedef NS_ENUM(NSUInteger, EOCEmployeeType) {
    EOCEmployeeTypeDeveloper,
    EOCEmployeeTypeDesigner,
    EOCEmployeeTypeFinance,
};

@interface EOCEmployee : NSObject

@property (copy) NSString *name;
@property NSUInteger salary;


// Helper for creating Employee objects
+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type;

// Make Employees do their respective day's work - (void)doADaysWork; @end 複製代碼
//EOCEmployee.m

@implementation EOCEmployee

+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type {
     switch (type) {
         case EOCEmployeeTypeDeveloper:
            return [EOCEmployeeDeveloper new];
         break; 

        case EOCEmployeeTypeDesigner:
             return [EOCEmployeeDesigner new];
         break;

        case EOCEmployeeTypeFinance:
             return [EOCEmployeeFinance new];
         break;
    }
}

- (void)doADaysWork {
 // 須要子類來實現
}



@end

複製代碼

咱們能夠看到,將EOCEmployee做爲抽象基類,這個抽象基類有一個初始化方法,經過這個方法,咱們能夠獲得多種基於這個抽象基類的實體子類:

2. 實體子類(concrete subclass):

@interface EOCEmployeeDeveloper : EOCEmployee
@end

@implementation EOCEmployeeDeveloper

- (void)doADaysWork {
    [self writeCode];
}

@end

複製代碼

注意: 若是對象所屬的類位於某個類族中,那麼在查詢類型信息時就要當心。由於類族中的實體子類並不與其基類屬於同一個類。

第10條:在既有類中使用關聯對象存放自定義數據

咱們能夠通「關聯對象」機制來把兩個對象鏈接起來。這樣咱們就能夠從某個對象中獲取相應的關聯對象的值。

先看一下關聯對象的語法:

1. 爲某個對象設置關聯對象的值:

void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)

這裏,第一個參數是主對象,第二個參數是鍵,第三個參數是關聯的對象,第四個參數是存儲策略:是枚舉,定義了內存管理語義。

2. 根據給定的鍵從某對象中獲取相應的關聯對象值:

id objc_getAssociatedObject(id object, void *key)

3. 移除指定對象的關聯對象:

void objc_removeAssociatedObjects(id object)

舉個例子:

#import <objc/runtime.h>

static void *EOCMyAlertViewKey = "EOCMyAlertViewKey";


- (void)askUserAQuestion {

         UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question"
                                                         message:@"What do you want to do?"
                                                        delegate:self
                                               cancelButtonTitle:@"Cancel"
                                               otherButtonTitles:@"Continue", nil];

         void (^block)(NSInteger) = ^(NSInteger buttonIndex){

                     if (buttonIndex == 0) {
                            [self doCancel];
                     } else {
                            [self doContinue];
                    }
         };

         //將alert和block關聯在了一塊兒
         objc_setAssociatedObject(alert,EOCMyAlertViewKey,block, OBJC_ASSOCIATION_COPY);
         [alert show];
}

// UIAlertViewDelegate protocol method
- (void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
     //alert取出關聯的block
      void (^block)(NSInteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey)
     //給block傳入index值
      block(buttonIndex);
}

複製代碼

第13條:用「方法調配技術」調試「黑盒方法」

與選擇子名稱相對應的方法是能夠在運行期被改變的,因此,咱們能夠不用經過繼承類並覆寫方法就能改變這個類自己的功能。

那麼如何在運行期改變選擇子對應的方法呢? 答:經過操縱類的方法列表的IMP指針

什麼是類方法表?什麼是IMP指針呢?

類的方法列表會把選擇子的名稱映射到相關的方法實現上,使得「動態消息派發系統」可以據此找到應該調用的方法。這些方法均以函數指針的形式來表示,這些指針叫作IMP。例如NSString類的選擇子列表:

類方法表的映射

有了這張表,OC的運行期系統提供的幾個方法就能操縱它。開發者能夠向其中增長選擇子,也能夠改變某選擇子對應的方法實現,也能夠交換兩個選擇子所映射到的指針以達到交換方法實現的目的。

舉個 :交換lowercaseStringuppercaseString方法的實現:

Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class],@selector(uppercaseString));

method_exchangeImplementations(originalMethod, swappedMethod);

複製代碼

這樣一來,類方法表的映射關係就變成了下圖:

交換兩個方法

這時,若是咱們調用lowercaseString方法就會實際調用uppercaseString的方法,反之亦然。

然而! 在實際應用中,只交換已經存在的兩個方法是沒有太大意義的。咱們應該利用這個特性來給既有的方法添加新功能(聽上去吊吊的):

它的實現原理是:先經過分類增長一個新方法,而後將這個新方法和要增長功能的舊方法替換(舊方法名 對應新方法的實現),這樣一來,若是咱們調用了舊方法,就會實現新方法了。

不知道這麼說是否抽象。仍是舉個 :

**需求:**咱們要在原有的lowercaseString方法中添加一條輸出語句。

步驟一:咱們先將新方法寫在NSString的分類裏:

@interface NSString (EOCMyAdditions)
- (NSString*)eoc_myLowercaseString;
@end


@implementation NSString (EOCMyAdditions)

- (NSString*)eoc_myLowercaseString {
     NSString *lowercase = [self eoc_myLowercaseString];//eoc_myLowercaseString方法會在未來方法調換後執行lowercaseString的方法
     NSLog(@"%@ => %@", self, lowercase);//輸出語句,便於調試
     return lowercase;
}
@end

複製代碼

步驟二:交換兩個方法的實現(操縱調換IMP指針)

Method originalMethod =
 class_getInstanceMethod([NSString class],
 @selector(lowercaseString));
Method swappedMethod =
 class_getInstanceMethod([NSString class],
 @selector(eoc_myLowercaseString));

method_exchangeImplementations(originalMethod, swappedMethod);
複製代碼

這樣一來,咱們若是交換了lowercaseStringeoc_myLowercaseString的方法實現,那麼在調用原來的lowercaseString方法後就能夠輸出新增的語句了。

「NSString *string = @"ThIs iS tHe StRiNg";
NSString *lowercaseString = [string lowercaseString];
// Output: ThIs iS tHe StRiNg => this is the string」
複製代碼

第16條:提供"全能初始化方法"

有時,因爲要實現各類設計需求,一個類能夠有多個建立實例的初始化方法。咱們應該選定其中一個做爲全能初始化方法,令其餘初始化方法都來調用它。

注意

  • 只有在這個全能初始化方法裏面才能存儲內部數據。這樣一來,當底層數據存儲機制改變時,只需修改此方法的代碼就好,無需改動其餘初始化方法。
  • 全能初始化方法是全部初始化方法裏參數最多的一個,由於它使用了儘量多的初始化所須要的參數,以便其餘的方法來調用本身。
  • 在咱們擁有了一個全能初始化方法後,最好仍是要覆寫init方法來設置默認值。
//全能初始化方法
- (id)initWithWidth:(float)width andHeight:(float)height
{
     if ((self = [super init])) {
        _width = width;
        _height = height;
    }
    return self;
}

//init方法也調用了全能初始化方法
- (id)init {
     return [self initWithWidth:5.0f andHeight:10.0f];
}
複製代碼

如今,咱們要創造一個squre類繼承這上面這個ractangle類,它有本身的全能初始化方法:

- (id)initWithDimension: (float)dimension{
    return [super initWithWidth:dimension andHeight:dimension];
}
複製代碼

這裏有問題!

然而,由於square類是rectangle類的子類,那麼它也可使用initWithWidth: andHeight:方法,更可使用init方法。那麼這兩種狀況下,顯然是沒法確保初始化的圖形是正方形。

所以,咱們須要在這裏覆寫square的父類rectangle的全能初始化方法:

- (id)initWithWidth:(float)width andHeight:(float)height
{
    float dimension = MAX(width, height);
    return [self initWithDimension:dimension];
}

複製代碼

這樣一來,當square用initWithWidth: andHeight:方法初始化時,就會獲得一個正方形。

而且,若是用init方法來初始化square的話,咱們也能夠獲得一個默認的正方形。由於在rectangle類裏覆寫了init方法,而這個init方法又調用了initWithWidth: andHeight:方法,而且square類又覆寫了initWithWidth: andHeight:方法,因此咱們仍然能夠獲得一個正方形。

並且,爲了讓square的init方法獲得一個默認的正方形,咱們也能夠覆寫它本身的初始化方法:

- (id)init{
    return [self initWithDimension:5.0f];
}

複製代碼

咱們作個總結:

由於子類的全能初始化方法(initWithDimension:)和其父類的初始化方法並不一樣,因此咱們須要在子類裏覆寫initWithWidth: andHeight:方法。

還差一點:initWithCoder:的初始化

有時,須要定義兩種全能初始化方法,由於對象有可能有兩種徹底不一樣的建立方式,例如initWithCoder:方法。

咱們仍然須要調用超類的初始化方法:

在rectangle類:

// Initializer from NSCoding
- (id)initWithCoder:(NSCoder*)decoder {

     // Call through to super's designated initializer if ((self = [super init])) { _width = [decoder decodeFloatForKey:@"width"]; _height = [decoder decodeFloatForKey:@"height"]; } return self; } 複製代碼

在square類:

// Initializer from NSCoding
- (id)initWithCoder:(NSCoder*)decoder {

 // Call through to super's designated initializer if ((self = [super initWithCoder:decoder])) { // EOCSquare's specific initializer
    }
     return self;
}
複製代碼

每一個子類的全能初始化方法都應該調用其超類的對應方法,並逐層向上。在調用了超類的初始化方法後,再執行與本類相關的方法。

第17條:實現description方法

在打印咱們本身定義的類的實例對象時,在控制檯輸出的結果每每是這樣的:

object = <EOCPerson: 0x7fd9a1600600>
複製代碼

這裏只包含了類名和內存地址,它的信息顯然是不具體的,遠達不到調試的要求。

**可是!**若是在咱們本身定義的類覆寫description方法,咱們就能夠在打印這個類的實例時輸出咱們想要的信息。

例如:

- (NSString*)description {
     return [NSString stringWithFormat:@"<%@: %p, %@ %@>", [self class], self, firstName, lastName];
}

複製代碼

在這裏,顯示了內存地址,還有該類的全部屬性。

並且,若是咱們將這些屬性值放在字典裏打印,則更具備可讀性:

- (NSString*)description {

     return [NSString stringWithFormat:@"<%@: %p, %@>",[self class],self,
   
    @{    @"title":_title,
       @"latitude":@(_latitude),
      @"longitude":@(_longitude)}
    ];
}
複製代碼

輸出結果:

location = <EOCLocation: 0x7f98f2e01d20, {

    latitude = "51.506";
   longitude = 0;
       title = London;
}>
複製代碼

咱們能夠看到,經過重寫description方法可讓咱們更加了解對象的狀況,便於後期的調試,節省開發時間。

第28條:經過協議提供匿名對象

匿名對象(Annonymous object),能夠理解爲「沒有名字的對象」。有時咱們用協議來提供匿名對象,目的在於說明它僅僅表示「聽從某個協議的對象」,而不是「屬於某個類的對象」。

它的表示方法爲:id<protocol>。 經過協議提供匿名對象的主要使用場景有兩個:

  • 做爲屬性
  • 做爲方法參數

1. 匿名對象做爲屬性

在設定某個類爲本身的代理屬性時,能夠不聲明代理的類,而是用id,由於成爲代理的終點並非某個類的實例,而是遵循了某個協議

舉個 :

@property (nonatomic, weak) id <EOCDelegate> delegate;
複製代碼

在這裏使用匿名對象的緣由有兩個:

  1. 未來可能會有不少不一樣類的實例對象做爲該類的代理。
  2. 咱們不想指明具體要使用哪一個類來做爲這個類的代理。

也就是說,能做爲該類的代理的條件只有一個:它聽從了 協議。

2. 匿名對象做爲方法參數

有時,咱們不會在乎方法裏某個參數的具體類型,而是遵循了某種協議,這個時候就可使用匿名對象來做爲方法參數。

舉個 :

- (void)setObject:(id)object forKey:(id<NSCopying>)key;
複製代碼

這個方法是NSDictionary的設值方法,它的參數只要聽從了協議,就能夠做爲參數傳進去,做爲NSDictionary的鍵。

第32條:編寫「異常安全代碼」時留意內存管理問題

在發生異常時的內存管理須要仔細考慮內存管理的問題:

在try塊中,若是先保留了某個對象,而後在釋放它以前又拋出了異常,那麼除非在catch塊中能處理此問題,不然對象所佔內存就將泄漏。

在MRC環境下:

@try {
     EOCSomeClass *object = [[EOCSomeClass alloc] init];
      [object doSomethingThatMayThrow];
      [object release];

}


@catch (...) {
         NSLog(@"Whoops, there was an error. Oh well...");
}

複製代碼

這裏,咱們用release方法釋放了try中的對象,可是這樣作仍然有問題:若是在doSomthingThatMayThrow方法中拋出了異常了呢?

這樣就沒法執行release方法了。

解決辦法是使用@finnaly塊,不管是否拋出異常,其中的代碼都能運行:

EOCSomeClass *object;
@try {
    object = [[EOCSomeClass alloc] init];
    [object doSomethingThatMayThrow];
}



@catch (...) {
     NSLog(@"Whoops, there was an error. Oh well...");
}

@finally {
    [object release];
}

複製代碼

在ARC環境下呢?

@try {
     EOCSomeClass *object = [[EOCSomeClass alloc] init];
     [object doSomethingThatMayThrow];
}



@catch (...) {
 NSLog(@"Whoops, there was an error. Oh well...");
}

複製代碼

這時,咱們沒法手動使用release方法了,解決辦法是使用:-fobjc-arc-exceptions 標誌來加入清理代碼,不過會致使應用程序變大,並且會下降運行效率。

第33條:以弱引用避免保留環

對象之間都用強指針引用對方的話會形成保留環。

兩個對象的保留環:

兩個對象都有一個對方的實例來做爲本身的屬性:

@interface EOCClassA : NSObject
@property (nonatomic, strong) EOCClassB *other;
@end


@interface EOCClassB : NSObject
@property (nonatomic, strong) EOCClassA *other;
@end

複製代碼

兩個對象的保留環

兩個對象都有指向對方的強指針,這樣會致使這兩個屬性裏的對象沒法被釋放掉。

多個對象的保留環:

若是保留環鏈接了多個對象,而這裏其中一個對象被外界引用,那麼當這個引用被移除後,整個保留環就泄漏了。

多個對象的保留環:孤島

解決方案是使用弱引用:

//EOCClassB.m
//第一種弱引用:unsafe_unretained
@property (nonatomic, unsafe_unretained) EOCClassA *other;


//第二種弱引用:weak
@property (nonatomic, weak) EOCClassA *other;

複製代碼

這兩種弱引用有什麼區別呢?

unsafe_unretained:當指向EOCClassA實例的引用移除後,unsafe_unretained屬性仍然指向那個已經回收的實例,

而weak指向nil:

unsafe_unretained 和 weak的區別

顯然,用weak字段應該是更安全的,由於再也不使用的對象按理說應該設置爲nil,而不該該產生依賴。

第34條:以「自動釋放池快」下降內存峯值


釋放對象的兩種方式:

  • 調用release:保留計數遞減
  • 調用autorelease將其加入自動釋放池中。在未來清空自動釋放池時,系統會向其中的對象發送release消息。

內存峯值(high-memory waterline)是指應用程序在某個限定時段內的最大內存用量(highest memory footprint)。新增的自動釋放池塊能夠減小這個峯值:

不用自動釋放池減小峯值:

for (int i = 0; i < 100000; i++) {

      [self doSomethingWithInt:i];

}

複製代碼

在這裏,doSomethingWithInt:方法可能會建立臨時對象。隨着循環次數的增長,臨時對象的數量也會飆升,而只有在整個for循環結束後,這些臨時對象纔會得意釋放。

這種狀況是不理想的,尤爲在咱們沒法控制循環長度的狀況下,咱們會不斷佔用內存並忽然釋放掉它們。

所以,咱們須要用自動釋放池來下降這種突兀的變化:

NSArray *databaseRecords = /* ... */;
NSMutableArray *people = [NSMutableArray new];
for (NSDictionary *record in databaseRecords) {
     @autoreleasepool {
             EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
            [people addObject:person];
      }
}
複製代碼

這樣一來,每次循環結束,咱們都會將臨時對象放在這個池裏面,而不是線程的主池裏面。

第35條:用「殭屍對象」調試內存管理問題

某個對象被回收後,再向它發送消息是不安全的,這並不必定會引發程序崩潰。

若是程序沒有崩潰,多是由於:

  • 該內存的部分原數據沒有被覆寫。
  • 該內存剛好被另外一個對象佔據,而這個對象能夠應答這個方法。

若是被回收的對象佔用的原內存被新的對象佔據,那麼收到消息的對象就不會是咱們預想的那個對象。在這樣的狀況下,若是這個對象沒法響應那個方法的話,程序依舊會崩潰。

所以,咱們但願能夠經過一種方法捕捉到對象被釋放後收到消息的狀況

這種方法就是利用殭屍對象!

Cocoa提供了「殭屍對象」的功能。若是開啓了這個功能,運行期系統會把全部已經回收的實例轉化成特殊的「殭屍對象」(經過修改isa指針,令其指向特殊的殭屍類),而不會真正回收它們,並且它們所佔據的核心內存將沒法被重用,這樣也就避免了覆寫的狀況。

在殭屍對象收到消息後,會拋出異常,它會說明發送過來的消息,也會描述回收以前的那個對象。

第38條:爲經常使用的塊類型建立typedef

若是咱們須要重複建立某種塊(相同參數,返回值)的變量,咱們就能夠經過typedef來給某一種塊定義屬於它本身的新類型

例如:

int (^variableName)(BOOL flag, int value) =^(BOOL flag, int value){
     // Implementation
     return someInt;
}

複製代碼

這個塊有一個bool參數和一個int參數,並返回int類型。咱們能夠給它定義類型:

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

再次定義的時候,就能夠經過簡單的賦值來實現:

EOCSomeBlock block = ^(BOOL flag, int value){
     // Implementation
};

複製代碼

定義做爲參數的塊:

- (void)startWithCompletionHandler: (void(^)(NSData *data, NSError *error))completion;

複製代碼

這裏的塊有一個NSData參數,一個NSError參數並無返回值

typedef void(^EOCCompletionHandler)(NSData *data, NSError *error);
- (void)startWithCompletionHandler:(EOCCompletionHandler)completion;」

複製代碼

經過typedef定義塊簽名的好處是:若是要某種塊增長參數,那麼只修改定義簽名的那行代碼便可。

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

下載網絡數據時,若是使用代理方法,會使得代碼分佈不緊湊,並且若是有多個下載任務的話,還要在回調的代理中判斷當前請求的類型。可是若是使用block的話,就可讓網絡下載的代碼和回調處理的代碼寫在一塊兒,這樣就能夠同時解決上面的兩個問題:

用代理下載:

- (void)fetchFooData {

     NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/foo.dat"];
    _fooFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    _fooFetcher.delegate = self;
    [_fooFetcher start];

}

- (void)fetchBarData {

     NSURL *url = [[NSURL alloc] initWithString: @"http://www.example.com/bar.dat"];
    _barFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    _barFetcher.delegate = self;
    [_barFetcher start];

}

- (void)networkFetcher:(EOCNetworkFetcher*)networkFetcher didFinishWithData:(NSData*)data
{   //判斷下載器類型
     if (networkFetcher == _fooFetcher) {
        _fetchedFooData = data;
        _fooFetcher = nil;

    } else if (networkFetcher == _barFetcher) {
        _fetchedBarData = data;
        _barFetcher = nil;
    }
}
複製代碼

用block下載:

- (void)fetchFooData {

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

}



- (void)fetchBarData {

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

}

複製代碼

還能夠將處理成功的代碼放在一個塊裏,處理失敗的代碼放在另外一個塊中:

#import <Foundation/Foundation.h>

@class EOCNetworkFetcher;
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



EOCNetworkFetcher *fetcher =[[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHander:^(NSData *data){
     // Handle success
}

 failureHandler:^(NSError *error){
 // Handle failure
}];



複製代碼

這樣寫的好處是,咱們能夠將處理成功和失敗的代碼分開來寫,看上去更加清晰。

咱們還能夠將 成功和失敗的代碼都放在同一個塊裏:

#import <Foundation/Foundation.h>


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

@interface EOCNetworkFetcher : NSObject

- (id)initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler:

(EOCNetworkFetcherCompletionHandler)completion;

@end



EOCNetworkFetcher *fetcher =[[EOCNetworkFetcher alloc] initWithURL:url];

[fetcher startWithCompletionHander:

^(NSData *data, NSError *error){

if (error) {

     // Handle failure

} else {

     // Handle success

}
}];

複製代碼

這樣作的好處是,若是及時下載失敗或中斷了,咱們仍然能夠取到當前所下載的data。並且,若是在需求上指出:下載成功後獲得的數據不多,也視爲失敗,那麼單一塊的寫法就很適用,由於它能夠取得數據後(成功)再判斷其是不是下載成功的。

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

若是塊捕獲的對象直接或間接地保留了塊自己,那麼就須要當心保留環問題:

@implementation EOCClass {

     EOCNetworkFetcher *_networkFetcher;
     NSData *_fetchedData;

}


- (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);
            _fetchedData = data;

    }];

}

複製代碼

在這裏出現了保留環:塊要設置_fetchedData變量,就須要捕獲self變量。而self(EOCClass實例)經過實例變量保留了獲取器_networkFetcher,而_networkFetcher又保留了塊。

解決方案是:在塊中取得了data後,將_networkFetcher設爲nil。

- (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);
            _fetchedData = data;
            _networkFetcher = nil;

    }];

}

複製代碼

第41條:多用派發隊列,少用同步鎖

多個線程執行同一份代碼時,極可能會形成數據不一樣步。做者建議使用GCD來爲代碼加鎖的方式解決這個問題。

方案一:使用串行同步隊列來將讀寫操做都安排到同一個隊列裏:

_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NULL);

//讀取字符串
- (NSString*)someString {

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

}

//設置字符串
- (void)setSomeString:(NSString*)someString {

     dispatch_sync(_syncQueue, ^{
        _someString = someString;
    });
}

複製代碼

這樣一來,讀寫操做都在串行隊列進行,就不容易出錯。

可是,還有一種方法可讓性能更高:

方案二:將寫操做放入柵欄快中,讓他們單獨執行;將讀取操做併發執行。

_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

//讀取字符串
- (NSString*)someString {

     __block NSString *localSomeString;
     dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
     return localSomeString;
}
複製代碼
//設置字符串
- (void)setSomeString:(NSString*)someString {

     dispatch_barrier_async(_syncQueue, ^{
        _someString = someString;
    });

}

複製代碼

顯然,數據的正確性主要取決於寫入操做,那麼只要保證寫入時,線程是安全的,那麼即使讀取操做是併發的,也能夠保證數據是同步的。

這裏的dispatch_barrier_async方法使得操做放在了同步隊列裏「有序進行」,保證了寫入操做的任務是在串行隊列裏。

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

在iOS開發中,有時會使用performSelector來執行某個方法,可是performSelector系列的方法能處理的選擇子很侷限:

  • 它沒法處理帶有多個參數的選擇子。
  • 返回值只能是void或者對象類型。

可是若是將方法放在塊中,經過GCD來操做就能很好地解決這些問題。尤爲是咱們若是想要讓一個任務在另外一個線程上執行,最好應該將任務放到塊裏,交給GCD來實現,而不是經過performSelector方法。

舉幾個 來比較這兩種方案:

1. 延後執行某個任務的方法:

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


// 使用 dispatch_after
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^(void){
    [self doSomething];
});

複製代碼

2. 將任務放在主線程執行:

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


// 使用 dispatch_async
// (or if waitUntilDone is YES, then dispatch_sync)
dispatch_async(dispatch_get_main_queue(), ^{
        [self doSomething];
});

複製代碼

注意: 若是waitUntilDone的參數是Yes,那麼就對應GCD的dispatch_sync方法。 咱們能夠看到,使用GCD的方式能夠將線程操做代碼和方法調用代碼寫在同一處,一目瞭然;並且徹底不受調用方法的選擇子和方法參數個數的限制。

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

除了GCD,操做隊列(NSOperationQueue)也是解決多線程任務管理問題的一個方案。對於不一樣的環境,咱們要採起不一樣的策略來解決問題:有時候使用GCD好些,有時則是使用操做隊列更加合理。

使用NSOperation和NSOperationQueue的優勢:

  1. 能夠取消操做:在運行任務前,能夠在NSOperation對象調用cancel方法,標明此任務不須要執行。可是GCD隊列是沒法取消的,由於它遵循「安排好以後就無論了(fire and forget)」的原則。
  2. 能夠指定操做間的依賴關係:例如從服務器下載並處理文件的動做能夠用操做來表示。而在處理其餘文件以前必須先下載「清單文件」。然後續的下載工做,都要依賴於先下載的清單文件這一操做。
  3. 監控NSOperation對象的屬性:能夠經過KVO來監聽NSOperation的屬性:能夠經過isCancelled屬性來判斷任務是否已取消;經過isFinished屬性來判斷任務是否已經完成。
  4. 能夠指定操做的優先級:操做的優先級表示此操做與隊列中其餘操做之間的優先關係,咱們能夠指定它。

第44條:經過Dispath Group機制,根據系統資源情況來執行任務

有時須要等待多個並行任務結束的那一刻執行某個任務,這個時候就可使用dispath group函數來實現這個需求:

經過dispath group函數,能夠把併發執行的多個任務合爲一組,因而調用者就能夠知道這些任務什麼時候才能所有執行完畢。

//一個優先級低的併發隊列
dispatch_queue_t lowPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);

//一個優先級高的併發隊列
dispatch_queue_t highPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

//建立dispatch_group
dispatch_group_t dispatchGroup = dispatch_group_create();

//將優先級低的隊列放入dispatch_group
for (id object in lowPriorityObjects) {
 dispatch_group_async(dispatchGroup,lowPriorityQueue,^{ [object performTask]; });
}

//將優先級高的隊列放入dispatch_group
for (id object in highPriorityObjects) {
 dispatch_group_async(dispatchGroup,highPriorityQueue,^{ [object performTask]; });
}

//dispatch_group裏的任務都結束後調用塊中的代碼
dispatch_queue_t notifyQueue = dispatch_get_main_queue();
dispatch_group_notify(dispatchGroup,notifyQueue,^{
     // Continue processing after completing tasks
});



複製代碼

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

有時咱們可能只須要將某段代碼執行一次,這時能夠經過dispatch_once函數來解決。

dispatch_once函數比較重要的使用例子是單例模式: 咱們在建立單例模式的實例時,可使用dispatch_once函數來令初始化代碼只執行一次,而且內部是線程安全的。

並且,對於執行一次的block來講,每次調用函數時傳入的標記都必須徹底相同,一般標記變量聲明在static或global做用域裏。

+ (id)sharedInstance {

     static EOCClass *sharedInstance = nil;
     static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
            sharedInstance = [[self alloc] init];
    });
     return sharedInstance;
}

複製代碼

咱們能夠這麼理解:在dispatch_once塊中的代碼在程序啓動到終止的過程裏,只要運行了一次後,就給本身加上了註釋符號,再也不存在了。

第49條:對自定義其內存管理語義的collection使用無縫橋接

經過無縫橋接技術,能夠再Foundation框架中的OC對象和CoreFoundation框架中的C語言數據結構之間來回轉換。

建立CoreFoundation中的collection時,能夠指定如何處理其中的元素。而後利用無縫橋接技術,能夠將其轉換爲OCcollection。

簡單的無縫橋接演示:

NSArray *anNSArray = @[@1, @2, @3, @4, @5];
CFArrayRef aCFArray = (__bridge CFArrayRef)anNSArray;
NSLog(@"Size of array = %li", CFArrayGetCount(aCFArray));

複製代碼

這裏,__bridge表示ARC仍然具有這個OC對象的全部權。CFArrayGetCount用來獲取數組的長高度。

爲何要使用無縫橋接技術呢?由於有些OC對象的特性是其對應的CF數據結構不具有的,反之亦然。所以咱們須要經過無縫橋接技術來讓這二者進行功能上的「互補」。

最後的話

終於總結完了,仍是有個別知識點理解得不是很透徹,須要反覆閱讀和理解消化。但願各位小夥伴多多提出寶貴意見,交流學習~

本文已同步到我的博客:傳送門

---------------------------- 2018年7月17日更新 ----------------------------

注意注意!!!

筆者在近期開通了我的公衆號,主要分享編程,讀書筆記,思考類的文章。

  • 編程類文章:包括筆者之前發佈的精選技術文章,以及後續發佈的技術文章(以原創爲主),而且逐漸脫離 iOS 的內容,將側重點會轉移到提升編程能力的方向上。
  • 讀書筆記類文章:分享編程類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術上生活上的思考。

由於公衆號天天發佈的消息數有限制,因此到目前爲止尚未將全部過去的精選文章都發布在公衆號上,後續會逐步發佈的。

並且由於各大博客平臺的各類限制,後面還會在公衆號上發佈一些短小精幹,以小見大的乾貨文章哦~

掃下方的公衆號二維碼並點擊關注,期待與您的共同成長~

公衆號:程序員維他命
相關文章
相關標籤/搜索