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

J_Knight_發表於2018-01-12

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

不知道筆者所說的三部曲的童鞋們可以看一下這張圖:

三部曲分佈圖

前兩篇傳送門:

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

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

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

在iOS開發中,我們也會使用“類族”(class cluster)這一設計模式,通過“抽象基類”來例項化不同的實體子類。

舉個? :

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

在這裡,我們只需要輸入不同的按鈕型別(UIButtonType)就可以得到不同的UIButton的子類。在OC框架中普遍使用這一設計模式。

為什麼要這麼做呢?

筆者認為這麼做的原因是為了“弱化”子類的具體型別,讓開發者無需關心建立出來的子類具體屬於哪個類。(這裡覺得還有點什麼,但是還沒有想到,歡迎補充!)

我們可以看一個具體的例子: 對於“員工”這個類,可以有各種不同的“子型別”:開發員工,設計員工和財政員工。這些“實體類”可以由“員工”這個抽象基類來獲得:

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 的內容,將側重點會轉移到提高程式設計能力的方向上。
  • 讀書筆記類文章:分享程式設計類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術上生活上的思考。

因為公眾號每天釋出的訊息數有限制,所以到目前為止還沒有將所有過去的精選文章都發布在公眾號上,後續會逐步釋出的。

而且因為各大部落格平臺的各種限制,後面還會在公眾號上釋出一些短小精幹,以小見大的乾貨文章哦~

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

公眾號:程式設計師維他命

相關文章