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


簡書博客已經(jīng)暫停更新,想看更多技術(shù)博客請到:


《Effective Objective-C 編寫高質(zhì)量iOS與OS X代碼的52個有效方法》

《Effective Objective-C 》超級干貨三部曲系列迎來了最后一篇:技巧篇,這一篇總結(jié)匯總了這本書中一些用來解決問題的偏向“設(shè)計模式”的知識點。

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

三部曲分布圖

前兩篇傳送門:
《Effective Objective-C 》超級干貨三部曲(一):概念篇
《Effective Objective-C 》超級干貨三部曲(二):規(guī)范篇

第9條 以“類族模式“隱藏實現(xiàn)細(xì)節(jié)


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

舉個?? :

+ (UIButton *)buttonWithType:(UIButtonType)type;

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

為什么要這么做呢?

筆者認(rèn)為這么做的原因是為了“弱化”子類的具體類型,讓開發(fā)者無需關(guān)心創(chuàng)建出來的子類具體屬于哪個類。(這里覺得還有點什么,但是還沒有想到,歡迎補(bǔ)充!)

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

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 {
 // 需要子類來實現(xiàn)
}



@end

我們可以看到,將EOCEmployee作為抽象基類,這個抽象基類有一個初始化方法,通過這個方法,我們可以得到多種基于這個抽象基類的實體子類:

2. 實體子類(concrete subclass):


@interface EOCEmployeeDeveloper : EOCEmployee
@end

@implementation EOCEmployeeDeveloper

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

@end

注意:
如果對象所屬的類位于某個類族中,那么在查詢類型信息時就要小心。因為類族中的實體子類并不與其基類屬于同一個類。

第10條:在既有類中使用關(guān)聯(lián)對象存放自定義數(shù)據(jù)


我們可以通“關(guān)聯(lián)對象”機(jī)制來把兩個對象連接起來。這樣我們就可以從某個對象中獲取相應(yīng)的關(guān)聯(lián)對象的值。

先看一下關(guān)聯(lián)對象的語法:

1. 為某個對象設(shè)置關(guān)聯(lián)對象的值:

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

這里,第一個參數(shù)是主對象,第二個參數(shù)是鍵,第三個參數(shù)是關(guān)聯(lián)的對象,第四個參數(shù)是存儲策略:是枚舉,定義了內(nèi)存管理語義。

2. 根據(jù)給定的鍵從某對象中獲取相應(yīng)的關(guān)聯(lián)對象值:

id objc_getAssociatedObject(id object, void *key)

3. 移除指定對象的關(guān)聯(lián)對象:

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關(guān)聯(lián)在了一起
         objc_setAssociatedObject(alert,EOCMyAlertViewKey,block, OBJC_ASSOCIATION_COPY);
         [alert show];
}

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

第13條:用“方法調(diào)配技術(shù)”調(diào)試“黑盒方法”


與選擇子名稱相對應(yīng)的方法是可以在運(yùn)行期被改變的,所以,我們可以不用通過繼承類并覆寫方法就能改變這個類本身的功能。

那么如何在運(yùn)行期改變選擇子對應(yīng)的方法呢?
答:通過操縱類的方法列表的IMP指針

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

類的方法列表會把選擇子的名稱映射到相關(guān)的方法實現(xiàn)上,使得“動態(tài)消息派發(fā)系統(tǒng)”能夠據(jù)此找到應(yīng)該調(diào)用的方法。這些方法均以函數(shù)指針的形式來表示,這些指針叫做IMP。例如NSString類的選擇子列表:

類方法表的映射

有了這張表,OC的運(yùn)行期系統(tǒng)提供的幾個方法就能操縱它。開發(fā)者可以向其中增加選擇子,也可以改變某選擇子對應(yīng)的方法實現(xiàn),也可以交換兩個選擇子所映射到的指針以達(dá)到交換方法實現(xiàn)的目的。

舉個 :交換lowercaseStringuppercaseString方法的實現(xiàn):


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

method_exchangeImplementations(originalMethod, swappedMethod);

這樣一來,類方法表的映射關(guān)系就變成了下圖:

交換兩個方法

這時,如果我們調(diào)用lowercaseString方法就會實際調(diào)用uppercaseString的方法,反之亦然。

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

它的實現(xiàn)原理是:先通過分類增加一個新方法,然后將這個新方法和要增加功能的舊方法替換(舊方法名 對應(yīng)新方法的實現(xiàn)),這樣一來,如果我們調(diào)用了舊方法,就會實現(xiàn)新方法了。

不知道這么說是否抽象。還是舉個 :

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

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

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


@implementation NSString (EOCMyAdditions)

- (NSString*)eoc_myLowercaseString {
     NSString *lowercase = [self eoc_myLowercaseString];//eoc_myLowercaseString方法會在將來方法調(diào)換后執(zhí)行l(wèi)owercaseString的方法
     NSLog(@"%@ => %@", self, lowercase);//輸出語句,便于調(diào)試
     return lowercase;
}
@end

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

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

method_exchangeImplementations(originalMethod, swappedMethod);

這樣一來,我們?nèi)绻粨Q了lowercaseStringeoc_myLowercaseString的方法實現(xiàn),那么在調(diào)用原來的lowercaseString方法后就可以輸出新增的語句了。

“NSString *string = @"ThIs iS tHe StRiNg";
NSString *lowercaseString = [string lowercaseString];
// Output: ThIs iS tHe StRiNg => this is the string”

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


有時,由于要實現(xiàn)各種設(shè)計需求,一個類可以有多個創(chuàng)建實例的初始化方法。我們應(yīng)該選定其中一個作為全能初始化方法,令其他初始化方法都來調(diào)用它。

注意

  • 只有在這個全能初始化方法里面才能存儲內(nèi)部數(shù)據(jù)。這樣一來,當(dāng)?shù)讓訑?shù)據(jù)存儲機(jī)制改變時,只需修改此方法的代碼就好,無需改動其他初始化方法。
  • 全能初始化方法是所有初始化方法里參數(shù)最多的一個,因為它使用了盡可能多的初始化所需要的參數(shù),以便其他的方法來調(diào)用自己。
  • 在我們擁有了一個全能初始化方法后,最好還是要覆寫init方法來設(shè)置默認(rèn)值。
//全能初始化方法
- (id)initWithWidth:(float)width andHeight:(float)height
{
     if ((self = [super init])) {
        _width = width;
        _height = height;
    }
    return self;
}

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

現(xiàn)在,我們要創(chuàng)造一個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];
}

這樣一來,當(dāng)square用initWithWidth: andHeight:方法初始化時,就會得到一個正方形。

并且,如果用init方法來初始化square的話,我們也可以得到一個默認(rèn)的正方形。因為在rectangle類里覆寫了init方法,而這個init方法又調(diào)用了initWithWidth: andHeight:方法,并且square類又覆寫了initWithWidth: andHeight:方法,所以我們?nèi)匀豢梢缘玫揭粋€正方形。

而且,為了讓square的init方法得到一個默認(rèn)的正方形,我們也可以覆寫它自己的初始化方法:

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

我們做個總結(jié):

因為子類的全能初始化方法(initWithDimension:)和其父類的初始化方法并不同,所以我們需要在子類里覆寫initWithWidth: andHeight:方法。

還差一點:initWithCoder:的初始化

有時,需要定義兩種全能初始化方法,因為對象有可能有兩種完全不同的創(chuàng)建方式,例如initWithCoder:方法。

我們?nèi)匀恍枰{(diào)用超類的初始化方法:

在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;
}

每個子類的全能初始化方法都應(yīng)該調(diào)用其超類的對應(yīng)方法,并逐層向上。在調(diào)用了超類的初始化方法后,再執(zhí)行與本類相關(guān)的方法。

第17條:實現(xiàn)description方法


在打印我們自己定義的類的實例對象時,在控制臺輸出的結(jié)果往往是這樣的:

object = <EOCPerson: 0x7fd9a1600600>

這里只包含了類名和內(nèi)存地址,它的信息顯然是不具體的,遠(yuǎn)達(dá)不到調(diào)試的要求。

但是!如果在我們自己定義的類覆寫description方法,我們就可以在打印這個類的實例時輸出我們想要的信息。

例如:


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

在這里,顯示了內(nèi)存地址,還有該類的所有屬性。

而且,如果我們將這些屬性值放在字典里打印,則更具有可讀性:

- (NSString*)description {

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

輸出結(jié)果:

location = <EOCLocation: 0x7f98f2e01d20, {

    latitude = "51.506";
   longitude = 0;
       title = London;
}>

我們可以看到,通過重寫description方法可以讓我們更加了解對象的情況,便于后期的調(diào)試,節(jié)省開發(fā)時間。

第28條:通過協(xié)議提供匿名對象


匿名對象(Annonymous object),可以理解為“沒有名字的對象”。有時我們用協(xié)議來提供匿名對象,目的在于說明它僅僅表示“遵從某個協(xié)議的對象”,而不是“屬于某個類的對象”。

它的表示方法為:id<protocol>
通過協(xié)議提供匿名對象的主要使用場景有兩個:

  • 作為屬性
  • 作為方法參數(shù)

1. 匿名對象作為屬性

在設(shè)定某個類為自己的代理屬性時,可以不聲明代理的類,而是用id<protocol>,因為成為代理的終點并不是某個類的實例,而是遵循了某個協(xié)議

舉個 :

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

在這里使用匿名對象的原因有兩個:

  1. 將來可能會有很多不同類的實例對象作為該類的代理。
  2. 我們不想指明具體要使用哪個類來作為這個類的代理。

也就是說,能作為該類的代理的條件只有一個:它遵從了 <EOCDelegate>協(xié)議。

2. 匿名對象作為方法參數(shù)

有時,我們不會在意方法里某個參數(shù)的具體類型,而是遵循了某種協(xié)議,這個時候就可以使用匿名對象來作為方法參數(shù)。

舉個 :

- (void)setObject:(id)object forKey:(id<NSCopying>)key;

這個方法是NSDictionary的設(shè)值方法,它的參數(shù)只要遵從了<NSCopying>協(xié)議,就可以作為參數(shù)傳進(jìn)去,作為NSDictionary的鍵。

第32條:編寫“異常安全代碼”時留意內(nèi)存管理問題


在發(fā)生異常時的內(nèi)存管理需要仔細(xì)考慮內(nèi)存管理的問題:

在try塊中,如果先保留了某個對象,然后在釋放它之前又拋出了異常,那么除非在catch塊中能處理此問題,否則對象所占內(nèi)存就將泄漏。

在MRC環(huán)境下:


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

}


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

這里,我們用release方法釋放了try中的對象,但是這樣做仍然有問題:如果在doSomthingThatMayThrow方法中拋出了異常了呢?

這樣就無法執(zhí)行release方法了。

解決辦法是使用@finnaly塊,無論是否拋出異常,其中的代碼都能運(yùn)行:


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



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

@finally {
    [object release];
}

在ARC環(huán)境下呢?

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



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

這時,我們無法手動使用release方法了,解決辦法是使用:-fobjc-arc-exceptions 標(biāo)志來加入清理代碼,不過會導(dǎo)致應(yīng)用程序變大,而且會降低運(yùn)行效率。

第33條:以弱引用避免保留環(huán)


對象之間都用強(qiáng)指針引用對方的話會造成保留環(huán)。

兩個對象的保留環(huán):

兩個對象都有一個對方的實例來作為自己的屬性:


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


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

兩個對象的保留環(huán)

兩個對象都有指向?qū)Ψ降膹?qiáng)指針,這樣會導(dǎo)致這兩個屬性里的對象無法被釋放掉。

多個對象的保留環(huán):

如果保留環(huán)連接了多個對象,而這里其中一個對象被外界引用,那么當(dāng)這個引用被移除后,整個保留環(huán)就泄漏了。

多個對象的保留環(huán):孤島

解決方案是使用弱引用:


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


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

這兩種弱引用有什么區(qū)別呢?

unsafe_unretained:當(dāng)指向EOCClassA實例的引用移除后,unsafe_unretained屬性仍然指向那個已經(jīng)回收的實例,

而weak指向nil:

unsafe_unretained 和 weak的區(qū)別

顯然,用weak字段應(yīng)該是更安全的,因為不再使用的對象按理說應(yīng)該設(shè)置為nil,而不應(yīng)該產(chǎn)生依賴。

第34條:以“自動釋放池快”降低內(nèi)存峰值


釋放對象的兩種方式:

  • 調(diào)用release:保留計數(shù)遞減
  • 調(diào)用autorelease將其加入自動釋放池中。在將來清空自動釋放池時,系統(tǒng)會向其中的對象發(fā)送release消息。

內(nèi)存峰值(high-memory waterline)是指應(yīng)用程序在某個限定時段內(nèi)的最大內(nèi)存用量(highest memory footprint)。新增的自動釋放池塊可以減少這個峰值:

不用自動釋放池減少峰值:


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

      [self doSomethingWithInt:i];

}

在這里,doSomethingWithInt:方法可能會創(chuàng)建臨時對象。隨著循環(huán)次數(shù)的增加,臨時對象的數(shù)量也會飆升,而只有在整個for循環(huán)結(jié)束后,這些臨時對象才會得意釋放。

這種情況是不理想的,尤其在我們無法控制循環(huán)長度的情況下,我們會不斷占用內(nèi)存并突然釋放掉它們。

因此,我們需要用自動釋放池來降低這種突兀的變化:


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

這樣一來,每次循環(huán)結(jié)束,我們都會將臨時對象放在這個池里面,而不是線程的主池里面。

第35條:用“僵尸對象”調(diào)試內(nèi)存管理問題


某個對象被回收后,再向它發(fā)送消息是不安全的,這并不一定會引起程序崩潰。

如果程序沒有崩潰,可能是因為:

  • 該內(nèi)存的部分原數(shù)據(jù)沒有被覆寫。
  • 該內(nèi)存恰好被另一個對象占據(jù),而這個對象可以應(yīng)答這個方法。

如果被回收的對象占用的原內(nèi)存被新的對象占據(jù),那么收到消息的對象就不會是我們預(yù)想的那個對象。在這樣的情況下,如果這個對象無法響應(yīng)那個方法的話,程序依舊會崩潰。

因此,我們希望可以通過一種方法捕捉到對象被釋放后收到消息的情況

這種方法就是利用僵尸對象!

Cocoa提供了“僵尸對象”的功能。如果開啟了這個功能,運(yùn)行期系統(tǒng)會把所有已經(jīng)回收的實例轉(zhuǎn)化成特殊的“僵尸對象”(通過修改isa指針,令其指向特殊的僵尸類),而不會真正回收它們,而且它們所占據(jù)的核心內(nèi)存將無法被重用,這樣也就避免了覆寫的情況。

在僵尸對象收到消息后,會拋出異常,它會說明發(fā)送過來的消息,也會描述回收之前的那個對象。

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


如果我們需要重復(fù)創(chuàng)建某種塊(相同參數(shù),返回值)的變量,我們就可以通過typedef來給某一種塊定義屬于它自己的新類型

例如:

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

這個塊有一個bool參數(shù)和一個int參數(shù),并返回int類型。我們可以給它定義類型:

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

再次定義的時候,就可以通過簡單的賦值來實現(xiàn):

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

定義作為參數(shù)的塊:

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

這里的塊有一個NSData參數(shù),一個NSError參數(shù)并沒有返回值

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

通過typedef定義塊簽名的好處是:如果要某種塊增加參數(shù),那么只修改定義簽名的那行代碼即可。

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


下載網(wǎng)絡(luò)數(shù)據(jù)時,如果使用代理方法,會使得代碼分布不緊湊,而且如果有多個下載任務(wù)的話,還要在回調(diào)的代理中判斷當(dāng)前請求的類型。但是如果使用block的話,就可以讓網(wǎng)絡(luò)下載的代碼和回調(diào)處理的代碼寫在一起,這樣就可以同時解決上面的兩個問題:

用代理下載:

- (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;
    }
}

用塊下載:


- (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

}
}];

這樣做的好處是,如果及時下載失敗或中斷了,我們?nèi)匀豢梢匀〉疆?dāng)前所下載的data。而且,如果在需求上指出:下載成功后得到的數(shù)據(jù)很少,也視為失敗,那么單一塊的寫法就很適用,因為它可以取得數(shù)據(jù)后(成功)再判斷其是否是下載成功的。

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


如果塊捕獲的對象直接或間接地保留了塊本身,那么就需要小心保留環(huán)問題:

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

    }];

}

在這里出現(xiàn)了保留環(huán):塊要設(shè)置_fetchedData變量,就需要捕獲self變量。而self(EOCClass實例)通過實例變量保留了獲取器_networkFetcher,而_networkFetcher又保留了塊。

解決方案是:在塊中取得了data后,將_networkFetcher設(shè)為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條:多用派發(fā)隊列,少用同步鎖


多個線程執(zhí)行同一份代碼時,很可能會造成數(shù)據(jù)不同步。作者建議使用GCD來為代碼加鎖的方式解決這個問題。

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

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

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

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

}

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

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

這樣一來,讀寫操作都在串行隊列進(jìn)行,就不容易出錯。

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

方案二:將寫操作放入柵欄快中,讓他們單獨執(zhí)行;將讀取操作并發(fā)執(zhí)行。

_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

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

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

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

}

顯然,數(shù)據(jù)的正確性主要取決于寫入操作,那么只要保證寫入時,線程是安全的,那么即便讀取操作是并發(fā)的,也可以保證數(shù)據(jù)是同步的。

這里的dispatch_barrier_async方法使得操作放在了同步隊列里“有序進(jìn)行”,保證了寫入操作的任務(wù)是在串行隊列里。

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


在iOS開發(fā)中,有時會使用performSelector來執(zhí)行某個方法,但是performSelector系列的方法能處理的選擇子很局限:

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

但是如果將方法放在塊中,通過GCD來操作就能很好地解決這些問題。尤其是我們?nèi)绻胍屢粋€任務(wù)在另一個線程上執(zhí)行,最好應(yīng)該將任務(wù)放到塊里,交給GCD來實現(xiàn),而不是通過performSelector方法。

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

1. 延后執(zhí)行某個任務(wù)的方法:


// 使用 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. 將任務(wù)放在主線程執(zhí)行:

// 使用 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的參數(shù)是Yes,那么就對應(yīng)GCD的dispatch_sync方法。
我們可以看到,使用GCD的方式可以將線程操作代碼和方法調(diào)用代碼寫在同一處,一目了然;而且完全不受調(diào)用方法的選擇子和方法參數(shù)個數(shù)的限制。

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


除了GCD,操作隊列(NSOperationQueue)也是解決多線程任務(wù)管理問題的一個方案。對于不同的環(huán)境,我們要采取不同的策略來解決問題:有時候使用GCD好些,有時則是使用操作隊列更加合理。

使用NSOperation和NSOperationQueue的優(yōu)點:

  1. 可以取消操作:在運(yùn)行任務(wù)前,可以在NSOperation對象調(diào)用cancel方法,標(biāo)明此任務(wù)不需要執(zhí)行。但是GCD隊列是無法取消的,因為它遵循“安排好之后就不管了(fire and forget)”的原則。
  2. 可以指定操作間的依賴關(guān)系:例如從服務(wù)器下載并處理文件的動作可以用操作來表示。而在處理其他文件之前必須先下載“清單文件”。而后續(xù)的下載工作,都要依賴于先下載的清單文件這一操作。
  3. 監(jiān)控NSOperation對象的屬性:可以通過KVO來監(jiān)聽NSOperation的屬性:可以通過isCancelled屬性來判斷任務(wù)是否已取消;通過isFinished屬性來判斷任務(wù)是否已經(jīng)完成。
  4. 可以指定操作的優(yōu)先級:操作的優(yōu)先級表示此操作與隊列中其他操作之間的優(yōu)先關(guān)系,我們可以指定它。

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


有時需要等待多個并行任務(wù)結(jié)束的那一刻執(zhí)行某個任務(wù),這個時候就可以使用dispath group函數(shù)來實現(xiàn)這個需求:

通過dispath group函數(shù),可以把并發(fā)執(zhí)行的多個任務(wù)合為一組,于是調(diào)用者就可以知道這些任務(wù)何時才能全部執(zhí)行完畢。


//一個優(yōu)先級低的并發(fā)隊列
dispatch_queue_t lowPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);

//一個優(yōu)先級高的并發(fā)隊列
dispatch_queue_t highPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

//創(chuàng)建dispatch_group
dispatch_group_t dispatchGroup = dispatch_group_create();

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

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

//dispatch_group里的任務(wù)都結(jié)束后調(diào)用塊中的代碼
dispatch_queue_t notifyQueue = dispatch_get_main_queue();
dispatch_group_notify(dispatchGroup,notifyQueue,^{
     // Continue processing after completing tasks
});



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


有時我們可能只需要將某段代碼執(zhí)行一次,這時可以通過dispatch_once函數(shù)來解決。

dispatch_once函數(shù)比較重要的使用例子是單例模式:
我們在創(chuàng)建單例模式的實例時,可以使用dispatch_once函數(shù)來令初始化代碼只執(zhí)行一次,并且內(nèi)部是線程安全的。

而且,對于執(zhí)行一次的block來說,每次調(diào)用函數(shù)時傳入的標(biāo)記都必須完全相同,通常標(biāo)記變量聲明在static或global作用域里。


+ (id)sharedInstance {

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

我們可以這么理解:在dispatch_once塊中的代碼在程序啟動到終止的過程里,只要運(yùn)行了一次后,就給自己加上了注釋符號,不再存在了。

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


通過無縫橋接技術(shù),可以再Foundation框架中的OC對象和CoreFoundation框架中的C語言數(shù)據(jù)結(jié)構(gòu)之間來回轉(zhuǎn)換。

創(chuàng)建CoreFoundation中的collection時,可以指定如何處理其中的元素。然后利用無縫橋接技術(shù),可以將其轉(zhuǎn)換為OCcollection。

簡單的無縫橋接演示:

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

這里,__bridge表示ARC仍然具備這個OC對象的所有權(quán)。CFArrayGetCount用來獲取數(shù)組的長高度。

為什么要使用無縫橋接技術(shù)呢?因為有些OC對象的特性是其對應(yīng)的CF數(shù)據(jù)結(jié)構(gòu)不具備的,反之亦然。因此我們需要通過無縫橋接技術(shù)來讓這兩者進(jìn)行功能上的“互補(bǔ)”。

最后的話


終于總結(jié)完了,還是有個別知識點理解得不是很透徹,需要反復(fù)閱讀和理解消化。希望各位小伙伴多多提出寶貴意見,交流學(xué)習(xí)~

本文已同步到個人博客:傳送門,歡迎常來^^


本文已在版權(quán)印備案,如需轉(zhuǎn)載請訪問版權(quán)印。48422928

獲取授權(quán)

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

注意注意!!!

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

  • 編程類文章:包括筆者以前發(fā)布的精選技術(shù)文章,以及后續(xù)發(fā)布的技術(shù)文章(以原創(chuàng)為主),并且逐漸脫離 iOS 的內(nèi)容,將側(cè)重點會轉(zhuǎn)移到提高編程能力的方向上。
  • 讀書筆記類文章:分享編程類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術(shù)上生活上的思考。

因為公眾號每天發(fā)布的消息數(shù)有限制,所以到目前為止還沒有將所有過去的精選文章都發(fā)布在公眾號上,后續(xù)會逐步發(fā)布的。

而且因為各大博客平臺的各種限制,后面還會在公眾號上發(fā)布一些短小精干,以小見大的干貨文章哦~

掃下方的公眾號二維碼并點擊關(guān)注,期待與您的共同成長~

公眾號:程序員維他命
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內(nèi)容