Objective-C項(xiàng)目規(guī)范

原著:https://cloud.tencent.com/developer/article/1199334,有部分修改

代碼是寫給人看的,別人看不懂的代碼就像是生銹的機(jī)器

[TOC]

.h文件中只定義確實(shí)需要暴露的屬性

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

@interface Person : NSObject

@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;

@end

在頭文件中盡量少的引用其他文件

類A需要將類B的實(shí)例變量作為它公共API的屬性。這個(gè)時(shí)候,我們不應(yīng)該引入類B的頭文件,而應(yīng)該使用向前聲明(forward declaring)使用class關(guān)鍵字,并且在A的實(shí)現(xiàn)文件引用B的頭文件。

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

@class Employer;

@interface Person : NSObject

@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) Employer *employer;//將Employer作為屬性

@end

// Person.m
#import "Employer.h"

這樣做有什么優(yōu)點(diǎn)呢:

不在A的頭文件中引入B的頭文件,就不會(huì)一并引入B的全部內(nèi)容,這樣就減少了編譯時(shí)間。
可以避免循環(huán)引用:因?yàn)槿绻麅蓚€(gè)類在自己的頭文件中都引入了對方的頭文件,那么就會(huì)導(dǎo)致其中一個(gè)類無法被正確編譯。
但是個(gè)別的時(shí)候,必須在頭文件中引入其他類的頭文件:

主要有兩種情況:
該類繼承于某個(gè)類,則應(yīng)該引入父類的頭文件。
該類遵從某個(gè)協(xié)議,則應(yīng)該引入該協(xié)議的頭文件。而且最好將協(xié)議單獨(dú)放在一個(gè)頭文件中。

多用字面量語法,少用與之等價(jià)的方法**

1. 聲明時(shí)的字面量語法:

在聲明NSNumber,NSArray,NSDictionary時(shí),應(yīng)該盡量使用簡潔字面量語法。
NSString *cat = animals[0];
NSString *iphone = dict[@"phone"];

2. 集合類取下標(biāo)的字面量語法:

NSArray,NSDictionary,NSMutableArray,NSMutableDictionary 的取下標(biāo)操作也應(yīng)該盡量使用字面量語法。

使用字面量語法的優(yōu)點(diǎn):

  1. 代碼看起來更加簡潔。
  2. 如果存在nil值,則會(huì)立即拋出異常。如果在不用字面量語法定義數(shù)組的情況下,如果數(shù)組內(nèi)部存在nil,則系統(tǒng)會(huì)將其設(shè)為數(shù)組最后一個(gè)元素并終止。所以當(dāng)這個(gè)nil不是最后一個(gè)元素的話,就會(huì)出現(xiàn)難以排查的錯(cuò)誤。

注意: 字面量語法創(chuàng)建出來的字符串,數(shù)組,字典對象都是不可變的。

多用類型常量,少用#define預(yù)處理命令

在OC中,定義常量通常使用預(yù)處理命令,但是并不建議使用它,而是使用類型常量的方法。 首先比較一下這兩種方法的區(qū)別:

  • 預(yù)處理命令:簡單的文本替換,不包括類型信息,并且可被任意修改。
  • 類型常量:包括類型信息,并且可以設(shè)置其使用范圍,而且不可被修改。

我們可以看出來,使用預(yù)處理雖然能達(dá)到替換文本的目的,但是本身還是有局限性的:不具備類型 + 可以被任意修改,總之給人一種不安全的感覺。

知道了它們的長短處,我們再來簡單看一下它們的具體使用方法:

預(yù)處理命令:

#define W_LABEL (W_SCREEN - 2*GAP)

這里,(W_SCREEN - 2*GAP)替換了W_LABEL,它不具備W_LABEL的類型信息。而且要注意一下:如果替換式中存在運(yùn)算符號,以筆者的經(jīng)驗(yàn)最好用括號括起來,不然容易出現(xiàn)錯(cuò)誤(有體會(huì))。

類型常量:

static const NSTimeIntervalDuration = 0.3;

這里: const 將其設(shè)置為常量,不可更改。 static意味著該變量僅僅在定義此變量的編譯單元中可見。如果不聲明static,編譯器會(huì)為它創(chuàng)建一個(gè)外部符號(external symbol)。我們來看一下對外公開的常量的聲明方法:

對外公開某個(gè)常量:

如果我們需要發(fā)送通知,那么就需要在不同的地方拿到通知的“頻道”字符串,那么顯然這個(gè)字符串是不能被輕易更改,而且可以在不同的地方獲取。這個(gè)時(shí)候就需要定義一個(gè)外界可見的字符串常量。

//header file
extern NSString *const NotificationString;

//implementation file
NSString *const  NotificationString = @"Finish Download";

這里NSString *const NotificationString是指針常量。 extern關(guān)鍵字告訴編譯器,在全局符號表中將會(huì)有一個(gè)名叫NotificationString的符號。

我們通常在頭文件聲明常量,在其實(shí)現(xiàn)文件里定義該常量。由實(shí)現(xiàn)文件生成目標(biāo)文件時(shí),編譯器會(huì)在“數(shù)據(jù)段”為字符串分配存儲(chǔ)空間。

最后注意一下公開和非公開的常量的命名規(guī)范:

公開的常量:常量的名字最好用與之相關(guān)的類名做前綴。 非公開的常量:局限于某個(gè)編譯單元(tanslation unit,實(shí)現(xiàn)文件 implementation file)內(nèi),在簽名加上字母k。

用枚舉表示狀態(tài),選項(xiàng),狀態(tài)碼

我們經(jīng)常需要給類定義幾個(gè)狀態(tài),這些狀態(tài)碼可以用枚舉來管理。下面是關(guān)于網(wǎng)絡(luò)連接狀態(tài)的狀態(tài)碼枚舉:

typedef NS_ENUM(NSUInteger, EOCConnectionState) {
  EOCConnectionStateDisconnected,
  EOCConnectionStateConnecting,
  EOCConnectionStateConnected,
};

需要注意的一點(diǎn)是: 在枚舉類型的switch語句中不要實(shí)現(xiàn)default分支。它的好處是,當(dāng)我們給枚舉增加成員時(shí),編譯器就會(huì)提示開發(fā)者:switch語句并未處理所有的枚舉。對此,筆者有個(gè)教訓(xùn),又一次在switch語句中將“默認(rèn)分支”設(shè)置為枚舉中的第一項(xiàng),自以為這樣寫可以讓程序更健壯,結(jié)果后來導(dǎo)致了嚴(yán)重的崩潰。

在對象內(nèi)部盡量直接訪問實(shí)例變量

關(guān)于實(shí)例變量的訪問,可以直接訪問,也可以通過屬性的方式(點(diǎn)語法)來訪問。書中作者建議在讀取實(shí)例變量時(shí)采用直接訪問的形式,而在設(shè)置實(shí)例變量的時(shí)候通過屬性來做。

直接訪問屬性的特點(diǎn):

  • 繞過set,get語義,速度快;

通過屬性訪問屬性的特點(diǎn):

  • 不會(huì)繞過屬性定義的內(nèi)存管理語義
  • 有助于打斷點(diǎn)排查錯(cuò)誤
  • 可以觸發(fā)KVO

因此,有個(gè)關(guān)于折中的方案:

設(shè)置屬性:通過屬性 讀取屬性:直接訪問

不過有兩個(gè)特例:

  1. 初始化方法和dealloc方法中,需要直接訪問實(shí)例變量來進(jìn)行設(shè)置屬性操作。因?yàn)槿绻谶@里沒有繞過set方法,就有可能觸發(fā)其他不必要的操作。
  2. 惰性初始化(lazy initialization)的屬性,必須通過屬性來讀取數(shù)據(jù)。因?yàn)槎栊猿跏蓟峭ㄟ^重寫get方法來初始化實(shí)例變量的,如果不通過屬性來讀取該實(shí)例變量,那么這個(gè)實(shí)例變量就永遠(yuǎn)不會(huì)被初始化。

用前綴 避免命名空間沖突

Apple宣稱其保留使用所有"兩字母前綴"的權(quán)利,所以我們選用的前綴應(yīng)該是三個(gè)字母的。 而且,如果自己開發(fā)的程序使用到了第三方庫,也應(yīng)該加上前綴。

盡量使用不可變對象

書中作者建議盡量把對外公布出來的屬性設(shè)置為只讀,在實(shí)現(xiàn)文件內(nèi)部設(shè)為讀寫。具體做法是:

在頭文件中,設(shè)置對象屬性為readonly,在實(shí)現(xiàn)文件中設(shè)置為readwrite。這樣一來,在外部就只能讀取該數(shù)據(jù),而不能修改它,使得這個(gè)類的實(shí)例所持有的數(shù)據(jù)更加安全。

而且,對于集合類的對象,更應(yīng)該仔細(xì)考慮是否可以將其設(shè)為可變的。

如果在公開部分只能設(shè)置其為只讀屬性,那么就在非公開部分存儲(chǔ)一個(gè)可變型。這樣一來,當(dāng)在外部獲取這個(gè)屬性時(shí),獲取的只是內(nèi)部可變型的一個(gè)不可變版本,例如:

在公共API中:

@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSSet *friends //向外公開的不可變集合

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName;
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;

@end

在這里,我們將friends屬性設(shè)置為不可變的set。然后,提供了來增加和刪除這個(gè)set里的元素的公共接口。

在實(shí)現(xiàn)文件里:

interface EOCPerson ()

@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;

@end

@implementation EOCPerson {
    NSMutableSet *_internalFriends;  //實(shí)現(xiàn)文件里的可變集合
}

- (NSSet*)friends {
    return [_internalFriends copy]; //get方法返回的永遠(yuǎn)是可變set的不可變型
}

- (void)addFriend:(EOCPerson*)person {
   [_internalFriends addObject:person]; //在外部增加集合元素的操作
   //do something when add element
}

- (void)removeFriend:(EOCPerson*)person {
   [_internalFriends removeObject:person]; //在外部移除元素的操作
   //do something when remove element
}

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName {

    if ((self = [super init])) {
       _firstName = firstName;
       _lastName = lastName;
       _internalFriends = [NSMutableSet new];
   }
return self;
}

我們可以看到,在實(shí)現(xiàn)文件里,保存一個(gè)可變set來記錄外部的增刪操作。

這里最重要的代碼是:

- (NSSet*)friends {
 return [_internalFriends copy];
}

這個(gè)是friends屬性的獲取方法:它將當(dāng)前保存的可變set復(fù)制了一不可變的set并返回。因此,外部讀取到的set都將是不可變的版本。

等一下,有個(gè)疑問:

在公共接口設(shè)置不可變set 和 將增刪的代碼放在公共接口中是否矛盾的?

答案:并不矛盾!

因?yàn)槿绻麑riends屬性設(shè)置為可變的,那么外部就可以隨便更改set集合里的數(shù)據(jù),這里的更改,僅僅是底層數(shù)據(jù)的更改,并不伴隨其他任何操作。 然而有時(shí),我們需要在更改set數(shù)據(jù)的同時(shí)要執(zhí)行隱秘在實(shí)現(xiàn)文件里的其他工作,那么如果在外部隨意更改這個(gè)屬性的話,顯然是達(dá)不到這種需求的。

因此,我們需要提供給外界我們定制的增刪的方法,并不讓外部”自行“增刪。

使用清晰而協(xié)調(diào)的命名方式

在給OC的方法取名字的時(shí)候要充分利用OC方法的命名優(yōu)勢,取一個(gè)語義清晰的方法名!什么叫語義清晰呢?就是說讀起來像是一句話一樣。

我們看一個(gè)例子:

先看名字取得不好的:

//方法定義
- (id)initWithSize:(float)width :(float)height;

//方法調(diào)用
EOCRectangle *aRectangle =[[EOCRectangle alloc] initWithSize:5.0f :10.0f];

這里定義了Rectangle的初始化方法。雖然直觀上可以知道這個(gè)方法通過傳入的兩個(gè)參數(shù)來組成矩形的size,但是我們并不知道哪個(gè)是矩形的寬,哪個(gè)是矩形的高。 來看一下正確的? :

//方法定義
- (id)initWithWidth:(float)width height:(float)height;

//方法調(diào)用
EOCRectangle *aRectangle =[[EOCRectangle alloc] initWithWidth:5.0f height:10.0f];

這個(gè)方法名就很好的詮釋了該方法的意圖:這個(gè)類的初始化是需要寬度和高度的。而且,哪個(gè)參數(shù)是高度,哪個(gè)參數(shù)是寬度,看得人一清二楚。永遠(yuǎn)要記得:代碼是給人看的。

筆者自己總結(jié)的方法命名規(guī)則:

每個(gè)冒號左邊的方法部分最好與右邊的參數(shù)名一致。

對于返回值是布爾值的方法,我們也要注意命名的規(guī)范:

  • 獲取”是否“的布爾值,應(yīng)該增加“is”前綴:
- isEqualToString:

獲取“是否有”的布爾值,應(yīng)該增加“has”前綴:

- hasPrefix:

為私有方法名加前綴

建議在實(shí)現(xiàn)文件里將非公開的方法都加上前綴,便于調(diào)試,而且這樣一來也很容易區(qū)分哪些是公共方法,哪些是私有方法。因?yàn)橥卜椒ㄊ遣槐阌谌我庑薷牡摹?/p>

在這里,作者舉了個(gè)例子:

#import <Foundation/Foundation.h>

@interface EOCObject : NSObject

- (void)publicMethod;

@end


@implementation EOCObject

- (void)publicMethod {
 /* ... */
}

- (void)p_privateMethod {
 /* ... */
}

@end

注意: 不要用下劃線來區(qū)分私有方法和公共方法,因?yàn)闀?huì)和蘋果公司的API重復(fù)。

通過委托與數(shù)據(jù)源協(xié)議進(jìn)行對象間通信

如果給委托對象發(fā)送消息,那么必須提前判斷該委托對象是否實(shí)現(xiàn)了該消息:

NSData *data = /* data obtained from network */;

if ([_delegate respondsToSelector: @selector(networkFetcher:didReceiveData:)])
{
        [_delegate networkFetcher:self didReceiveData:data];
}

而且,最好再加上一個(gè)判斷:判斷委托對象是否存在

NSData *data = /* data obtained from network */;

if ( (_delegate) && ([_delegate respondsToSelector: @selector(networkFetcher:didReceiveData:)]))
{
        [_delegate networkFetcher:self didReceiveData:data];
}

對于代理模式,在iOS中分為兩種:

  • 普通的委托模式:信息從類流向委托者
  • 信息源模式:信息從數(shù)據(jù)源流向類
image

普通的委托 | 信息源

就好比tableview告訴它的代理(delegate)“我被點(diǎn)擊了”;而它的數(shù)據(jù)源(data Source)告訴它“你有這些數(shù)據(jù)”。仔細(xì)回味一下,這兩個(gè)信息的傳遞方向是相反的。

將類的實(shí)現(xiàn)代碼分散到便于管理的數(shù)個(gè)分類中

通常一個(gè)類會(huì)有很多方法,而這些方法往往可以用某種特有的邏輯來分組。我們可以利用OC的分類機(jī)制,將類的這些方法按一定的邏輯劃入幾個(gè)分區(qū)中。

例子:

無分類的類:

#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName;

/* Friendship methods */
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;


/* Work methods */
- (void)performDaysWork;
- (void)takeVacationFromWork;


/* Play methods */
- (void)goToTheCinema;
- (void)goToSportsGame;


@end

分類之后:

#import <Foundation/Foundation.h>


@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;



- (id)initWithFirstName:(NSString*)firstName

lastName:(NSString*)lastName;

@end



@interface EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;

@end



@interface EOCPerson (Work)

- (void)performDaysWork;
- (void)takeVacationFromWork;

@end



@interface EOCPerson (Play)

- (void)goToTheCinema;
- (void)goToSportsGame;

@end

其中,F(xiàn)riendShip分類的實(shí)現(xiàn)代碼可以這么寫:

注意:在新建分類文件時(shí),一定要引入被分類的類文件。

通過分類機(jī)制,可以把類代碼分成很多個(gè)易于管理的功能區(qū),同時(shí)也便于調(diào)試。因?yàn)榉诸惖姆椒Q會(huì)包含分類的名稱,可以馬上看到該方法屬于哪個(gè)分類中。

利用這一點(diǎn),我們可以創(chuàng)建名為Private的分類,將所有私有方法都放在該類里。這樣一來,我們就可以根據(jù)private一詞的出現(xiàn)位置來判斷調(diào)用的合理性,這也是一種編寫“自我描述式代碼(self-documenting)”的辦法。

總是為第三方類的分類名稱加前綴

分類機(jī)制雖然強(qiáng)大,但是如果分類里的方法與原來的方法名稱一致,那么分類的方法就會(huì)覆蓋掉原來的方法,而且總是以最后一次被覆蓋為基準(zhǔn)。

因此,我們應(yīng)該以命名空間來區(qū)別各個(gè)分類的名稱與其中定義的方法。在OC里的做法就是給這些方法加上某個(gè)共用的前綴。例如:

@interface NSString (ABC_HTTP)

// Encode a string with URL encoding
- (NSString*)abc_urlEncodedString;

// Decode a URL encoded string
- (NSString*)abc_urlDecodedString;

@end

因此,如果我們想給第三方庫或者iOS框架里的類添加分類時(shí),最好將分類名和方法名加上前綴。

勿在分類中聲明屬性

除了實(shí)現(xiàn)文件里的class-continuation分類中可以聲明屬性外,其他分類無法向類中新增實(shí)例變量。 因此,類所封裝的全部數(shù)據(jù)都應(yīng)該定義在主接口中,這里是唯一能夠定義實(shí)例變量的地方。

關(guān)于分類,需要強(qiáng)調(diào)一點(diǎn):

分類機(jī)制,目標(biāo)在于擴(kuò)展類的功能,而不是封裝數(shù)據(jù)。

使用class-continuation分類 隱藏實(shí)現(xiàn)細(xì)節(jié)

通常,我們需要減少在公共接口中向外暴露的部分(包括屬性和方法),而因此帶給我們的局限性可以利用class-continuation分類的特性來補(bǔ)償:

  • 可以在class-continuation分類中增加實(shí)例變量。
  • 可以在class-continuation分類中將公共接口的只讀屬性設(shè)置為讀寫。
  • 可以在class-continuation分類中遵循協(xié)議,使其不為人知。

在dealloc方法中只釋放引用并解除監(jiān)聽**

永遠(yuǎn)不要自己調(diào)用dealloc方法,運(yùn)行期系統(tǒng)會(huì)在適當(dāng)?shù)臅r(shí)候調(diào)用它。根據(jù)性能需求我們有時(shí)需要在dealloc方法中做一些操作。那么我們可以在dealloc方法里做什么呢?

  • 釋放對象所擁有的所有引用,不過ARC會(huì)自動(dòng)添加這些釋放代碼,可以不必操心。
  • 而且對象擁有的其他非OC對象也要釋放(CoreFoundation對象就必須手動(dòng)釋放)
  • 釋放原來的觀測行為:注銷通知。如果沒有及時(shí)注銷,就會(huì)向其發(fā)送通知,使得程序崩潰。

舉個(gè)簡單的? :

- (void)dealloc {

     CFRelease(coreFoundationObject);
    [[NSNotificationCenter defaultCenter] removeObserver:self];

}

尤其注意:在dealloc方法中不應(yīng)該調(diào)用其他的方法,因?yàn)槿绻@些方法是異步的,并且回調(diào)中還要使用當(dāng)前對象,那么很有可能當(dāng)前對象已經(jīng)被釋放了,會(huì)導(dǎo)致崩潰。 并且在dealloc方法中也不能調(diào)用屬性的存取方法,因?yàn)楹苡锌赡茉谶@些方法里還有其他操作。而且這個(gè)屬性還有可能處于鍵值觀察狀態(tài),該屬性的觀察者可能會(huì)在屬性改變時(shí)保留或者使用這個(gè)即將回收的對象。

不要使用retainCount

在非ARC得環(huán)境下使用retainCount可以返回當(dāng)前對象的引用計(jì)數(shù),但是在ARC環(huán)境下調(diào)用會(huì)報(bào)錯(cuò),因?yàn)樵摲椒ㄒ呀?jīng)被廢棄了 。

它被廢棄的原因是因?yàn)樗祷氐囊糜?jì)數(shù)只能反映對象某一時(shí)刻的引用計(jì)數(shù),而無法“預(yù)知”對象將來引用計(jì)數(shù)的變化(比如對象當(dāng)前處于自動(dòng)釋放池中,那么將來就會(huì)自動(dòng)遞減引用計(jì)數(shù))。

不要使用dispatch_get_current_queue

我們無法用某個(gè)隊(duì)列來描述“當(dāng)前隊(duì)列”這一屬性,因?yàn)榕砂l(fā)隊(duì)列是按照層級來組織的。

那么什么是隊(duì)列的層級呢?

image

隊(duì)列的層及分布

安排在某條隊(duì)列中的快,會(huì)在其上層隊(duì)列中執(zhí)行,而層級地位最高的那個(gè)隊(duì)列總是全局并發(fā)隊(duì)列。

在這里,B,C中的塊會(huì)在A里執(zhí)行。但是D中的塊,可能與A里的塊并行,因?yàn)锳和D的目標(biāo)隊(duì)列是并發(fā)隊(duì)列。

正因?yàn)橛辛诉@種層級關(guān)系,所以檢查當(dāng)前隊(duì)列是并發(fā)的還是非并發(fā)的就不會(huì)總是很準(zhǔn)確。

多用塊枚舉,少用for循環(huán)**

當(dāng)遍歷集合元素時(shí),建議使用塊枚舉,因?yàn)橄鄬τ趥鹘y(tǒng)的for循環(huán),它更加高效,而且簡潔,還能獲取到用傳統(tǒng)的for循環(huán)無法提供的值:

我們首先看一下傳統(tǒng)的遍歷:

NSArray *anArray = /* ... */;
for (int i = 0; i < anArray.count; i++) {
   id object = anArray[i];
   // Do something with 'object'
}



// Dictionary
NSDictionary *aDictionary = /* ... */;
NSArray *keys = [aDictionary allKeys];
for (int i = 0; i < keys.count; i++) {
   id key = keys[i];
   id value = aDictionary[key];
   // Do something with 'key' and 'value'
}


// Set
NSSet *aSet = /* ... */;
NSArray *objects = [aSet allObjects];
for (int i = 0; i < objects.count; i++) {
   id object = objects[i];
   // Do something with 'object'

}

傳統(tǒng)的for遍歷

NSArray *anArray = /* ... */;
for (id object in anArray) {
 // Do something with 'object'
}

// Dictionary
NSDictionary *aDictionary = /* ... */;
for (id key in aDictionary) {
 id value = aDictionary[key];
 // Do something with 'key' and 'value'

}


NSSet *aSet = /* ... */;
for (id object in aSet) {
 // Do something with 'object'
}

我們可以看到,在遍歷NSDictionary,和NSet時(shí),我們又新創(chuàng)建了一個(gè)數(shù)組。雖然遍歷的目的達(dá)成了,但是卻加大了系統(tǒng)的開銷。

利用快速遍歷:

NSArray *anArray = /* ... */;
for (id object in anArray) {
 // Do something with 'object'
}

// Dictionary
NSDictionary *aDictionary = /* ... */;
for (id key in aDictionary) {
 id value = aDictionary[key];
 // Do something with 'key' and 'value'

}


NSSet *aSet = /* ... */;
for (id object in aSet) {
 // Do something with 'object'
}

這種快速遍歷的方法要比傳統(tǒng)的遍歷方法更加簡潔易懂,但是缺點(diǎn)是無法方便獲取元素的下標(biāo)。

利用基于block的遍歷:

NSArray anArray = / ... */;
[anArray enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL *stop){

// Do something with 'object'
if (shouldStop) {
*stop = YES; //使迭代停止
}

}];

“// Dictionary
NSDictionary aDictionary = / ... */;
[aDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id object, BOOL *stop){
// Do something with 'key' and 'object'
if (shouldStop) {
*stop = YES;
}
}];

// Set
NSSet aSet = / ... */;
[aSet enumerateObjectsUsingBlock:^(id object, BOOL *stop){
// Do something with 'object'
if (shouldStop) {
*stop = YES;
}
我們可以看到,在使用塊進(jìn)行快速枚舉的時(shí)候,我們可以不創(chuàng)建臨時(shí)數(shù)組。雖然語法上沒有快速枚舉簡潔,但是我們可以獲得數(shù)組元素對應(yīng)的序號,字典元素對應(yīng)的鍵值,而且,我們還可以隨時(shí)令遍歷終止。

利用快速枚舉和塊的枚舉還有一個(gè)優(yōu)點(diǎn):能夠修改塊的方法簽名

for (NSString *key in aDictionary) {
         NSString *object = (NSString*)aDictionary[key];
        // Do something with 'key' and 'object'
}
NSDictionary *aDictionary = /* ... */;

    [aDictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop){

             // Do something with 'key' and 'obj'

}];

如果我們可以知道集合里的元素類型,就可以修改簽名。這樣做的好處是:可以讓編譯期檢查該元素是否可以實(shí)現(xiàn)我們想調(diào)用的方法,如果不能實(shí)現(xiàn),就做另外的處理。這樣一來,程序就能變得更加安全。

構(gòu)建緩存時(shí)選用NSCache 而非NSDictionary**

如果我們緩存使用得當(dāng),那么應(yīng)用程序的響應(yīng)速度就會(huì)提高。只有那種“重新計(jì)算起來很費(fèi)事的數(shù)據(jù),才值得放入緩存”,比如那些需要從網(wǎng)絡(luò)獲取或從磁盤讀取的數(shù)據(jù)。

在構(gòu)建緩存的時(shí)候很多人習(xí)慣用NSDictionary或者NSMutableDictionary,但是作者建議大家使用NSCache,它作為管理緩存的類,有很多特點(diǎn)要優(yōu)于字典,因?yàn)樗緛砭褪菫榱斯芾砭彺娑O(shè)計(jì)的。

NSCache優(yōu)于NSDictionary的幾點(diǎn):

  • 當(dāng)系統(tǒng)資源將要耗盡時(shí),NSCache具備自動(dòng)刪減緩沖的功能。并且還會(huì)先刪減“最久未使用”的對象。
  • NSCache不拷貝鍵,而是保留鍵。因?yàn)椴⒉皇撬械逆I都遵從拷貝協(xié)議(字典的鍵是必須要支持拷貝協(xié)議的,有局限性)。
  • NSCache是線程安全的:不編寫加鎖代碼的前提下,多個(gè)線程可以同時(shí)訪問NSCache。

關(guān)于操控NSCache刪減內(nèi)容的時(shí)機(jī)

開發(fā)者可以通過兩個(gè)尺度來調(diào)整這個(gè)時(shí)機(jī):

  • 緩存中的對象總數(shù).
  • 將對象加入緩存時(shí),為其指定開銷值。

對于開銷值,只有在能很快計(jì)算出開銷值的情況下,才應(yīng)該考慮采用這個(gè)尺度,不然反而會(huì)加大系統(tǒng)的開銷。

下面我們來看一下緩存的用法:緩存網(wǎng)絡(luò)下載的數(shù)據(jù)

// Network fetcher class
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);

@interface EOCNetworkFetcher : NSObject

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

@end

// Class that uses the network fetcher and caches results
@interface EOCClass : NSObject
@end

@implementation EOCClass {
     NSCache *_cache;
}

- (id)init {

     if ((self = [super init])) {
    _cache = [NSCache new];

     // Cache a maximum of 100 URLs
    _cache.countLimit = 100;


     /**
     * The size in bytes of data is used as the cost,
     * so this sets a cost limit of 5MB.
     */
    _cache.totalCostLimit = 5 * 1024 * 1024;
    }
 return self;
}



- (void)downloadDataForURL:(NSURL*)url { 

     NSData *cachedData = [_cache objectForKey:url];

     if (cachedData) {

         // Cache hit:存在緩存,讀取
        [self useData:cachedData];

    } else {

         // Cache miss:沒有緩存,下載
         EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];      

        [fetcher startWithCompletionHandler:^(NSData *data){
         [_cache setObject:data forKey:url cost:data.length];    
        [self useData:data];
        }];
    }
}
@end

在這里,我們使用URL作為緩存的key,將總對象數(shù)目設(shè)置為100,將開銷值設(shè)置為5MB。

NSPurgeableData

NSPurgeableData是NSMutableData的子類,把它和NSCache配合使用效果很好。

因?yàn)楫?dāng)系統(tǒng)資源緊張時(shí),可以把保存NSPurgeableData的那塊內(nèi)存釋放掉。

如果需要訪問某個(gè)NSPurgeableData對象,可以調(diào)用beginContentAccess方發(fā),告訴它現(xiàn)在還不應(yīng)該丟棄自己所占據(jù)的內(nèi)存。

在使用完之后,調(diào)用endContentAccess方法,告訴系統(tǒng)在必要時(shí)可以丟棄自己所占據(jù)的內(nèi)存。

- (void)downloadDataForURL:(NSURL*)url { 

      NSPurgeableData *cachedData = [_cache objectForKey:url];

      if (cachedData) {         

            // 如果存在緩存,需要調(diào)用beginContentAccess方法
            [cacheData beginContentAccess];

             // Use the cached data
            [self useData:cachedData];

             // 使用后,調(diào)用endContentAccess
            [cacheData endContentAccess];


        } else {

                 //沒有緩存
                 EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];    

                  [fetcher startWithCompletionHandler:^(NSData *data){

                         NSPurgeableData *purgeableData = [NSPurgeableData dataWithData:data];
                         [_cache setObject:purgeableData forKey:url cost:purgeableData.length];

                          // Don't need to beginContentAccess as it begins            
                          // with access already marked
                           // Use the retrieved data
                            [self useData:data];

                             // Mark that the data may be purged now
                            [purgeableData endContentAccess];

            }];
      }
}

上面這兩個(gè)方法類似于“引用計(jì)數(shù)”遞增遞減的操作,也就是說,只有當(dāng)“引用計(jì)數(shù)”為0的時(shí)候,才可以在將來刪去它所占的內(nèi)存。

注意: 在我們可以直接拿到purgeableData的情況下需要執(zhí)行beginContentAccess方法。然而,在創(chuàng)建purgeableData的情況下,是不需要執(zhí)行beginContentAccess,因?yàn)樵趧?chuàng)建了purgeableData之后,其引用計(jì)數(shù)會(huì)自動(dòng)+1;

精簡initialize 與 load的實(shí)現(xiàn)代碼

load方法

+(void)load;

每個(gè)類和分類在加入運(yùn)行期系統(tǒng)時(shí),都會(huì)調(diào)用load方法,而且僅僅調(diào)用一次,可能有些小伙伴習(xí)慣在這里調(diào)用一些方法,但是作者建議盡量不要在這個(gè)方法里調(diào)用其他方法,尤其是使用其他的類。原因是每個(gè)類載入程序庫的時(shí)機(jī)是不同的,如果該類調(diào)用了還未載入程序庫的類,就會(huì)很危險(xiǎn)。

initialize方法

+(void)initialize;

這個(gè)方法與load方法類似,區(qū)別是這個(gè)方法會(huì)在程序首次調(diào)用這個(gè)類的時(shí)候調(diào)用(惰性調(diào)用),而且只調(diào)用一次(絕對不能主動(dòng)使用代碼調(diào)用)。

值得注意的一點(diǎn)是,如果子類沒有實(shí)現(xiàn)它,它的超類卻實(shí)現(xiàn)了,那么就會(huì)運(yùn)行超類的代碼:這個(gè)情況往往很容易讓人忽視。

看一下? :

#import <Foundation/Foundation.h>

@interface EOCBaseClass : NSObject
@end

@implementation EOCBaseClass
+ (void)initialize {
 NSLog(@"%@ initialize", self);
}
@end

@interface EOCSubClass : EOCBaseClass
@end

@implementation EOCSubClass
@end

當(dāng)使用EOCSubClass類時(shí),控制臺(tái)會(huì)輸出兩次打印方法:

EOCBaseClass initialize
EOCSubClass initialize

因?yàn)樽宇怑OCSubClass并沒有覆寫initialize方法,那么自然會(huì)調(diào)用其父類EOCBaseClass的方法。 解決方案是通過檢測類的類型的方法:

+ (void)initialize {
   if (self == [EOCBaseClass class]) {
       NSLog(@"%@ initialized", self);
    }
}

這樣一來,EOCBaseClass的子類EOCSubClass就無法再調(diào)用initialize方法了。 我們可以察覺到,如果在這個(gè)方法里執(zhí)行過多的操作的話,會(huì)使得程序難以維護(hù),也可能引起其他的bug。因此,在initialize方法里,最好只是設(shè)置內(nèi)部的數(shù)據(jù),不要調(diào)用其他的方法,因?yàn)閷砜赡軙?huì)給這些方法添加其它的功能,那么會(huì)可能會(huì)引起難以排查的bug。

別忘了NSTimer會(huì)保留其目標(biāo)對象

在使用NSTimer的時(shí)候,NSTimer會(huì)生成指向其使用者的引用,而其使用者如果也引用了NSTimer,那么就會(huì)生成保留環(huán)。

#import <Foundation/Foundation.h>

@interface EOCClass : NSObject
- (void)startPolling;
- (void)stopPolling;
@end


@implementation EOCClass {
     NSTimer *_pollTimer;
}


- (id)init {
     return [super init];
}


- (void)dealloc {
    [_pollTimer invalidate];
}


- (void)stopPolling {

    [_pollTimer invalidate];
    _pollTimer = nil;
}


- (void)startPolling {
   _pollTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
                                                 target:self
                                               selector:@selector(p_doPoll)
                                               userInfo:nil
                                                repeats:YES];
}

- (void)p_doPoll {
    // Poll the resource
}

@end

在這里,在EOCClass和_pollTimer之間形成了保留環(huán),如果不主動(dòng)調(diào)用stopPolling方法就無法打破這個(gè)保留環(huán)。像這種通過主動(dòng)調(diào)用方法來打破保留環(huán)的設(shè)計(jì)顯然是不好的。

而且,如果通過回收該類的方法來打破此保留環(huán)也是行不通的,因?yàn)闀?huì)將該類和NSTimer孤立出來,形成“孤島”:

image

孤立了類和它的NSTimer

這可能是一個(gè)極其危險(xiǎn)的情況,因?yàn)镹STimer沒有消失,它還有可能持續(xù)執(zhí)行一些任務(wù),不斷消耗系統(tǒng)資源。而且,如果任務(wù)涉及到下載,那么可能會(huì)更糟。。

那么如何解決呢? 通過“塊”來解決!

#import <Foundation/Foundation.h>

@interface NSTimer (EOCBlocksSupport)

+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                         block:(void(^)())block
                                         repeats:(BOOL)repeats;
@end



@implementation NSTimer (EOCBlocksSupport)

+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                         block:(void(^)())block
                                        repeats:(BOOL)repeats
{
             return [self scheduledTimerWithTimeInterval:interval
                                                  target:self
                                                selector:@selector(eoc_blockInvoke:)
                                                userInfo:[block copy]
                                                 repeats:repeats];

}


+ (void)eoc_blockInvoke:(NSTimer*)timer {
     void (^block)() = timer.userInfo;
         if (block) {
             block();
        }
}
@end

通過給NSTimer增加一個(gè)分類就可以解決:

- (void)startPolling {

         __weak EOCClass *weakSelf = self;    
         _pollTimer = [NSTimer eoc_scheduledTimerWithTimeInterval:5.0 block:^{

               EOCClass *strongSelf = weakSelf;
               [strongSelf p_doPoll];
          }

                                                          repeats:YES];
}

我們在NSTimer類里添加了方法,我們來看一下如何使用它:

在這里,創(chuàng)建了一個(gè)self的弱引用,然后讓塊捕獲了這個(gè)self變量,讓其在執(zhí)行期間存活。

一旦外界指向EOC類的最后一個(gè)引用消失,該類就會(huì)被釋放,被釋放的同時(shí),也會(huì)向NSTimer發(fā)送invalidate消息(因?yàn)樵谠擃惖膁ealloc方法中向NSTimer發(fā)送了invalidate消息)。

而且,即使在dealloc方法里沒有發(fā)送invalidate消息,因?yàn)閴K里的weakSelf會(huì)變成nil,所以NSTimer同樣會(huì)失效。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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