原著: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):
- 代碼看起來更加簡潔。
- 如果存在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è)特例:
- 初始化方法和dealloc方法中,需要直接訪問實(shí)例變量來進(jìn)行設(shè)置屬性操作。因?yàn)槿绻谶@里沒有繞過set方法,就有可能觸發(fā)其他不必要的操作。
- 惰性初始化(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ù)源流向類
普通的委托 | 信息源
就好比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ì)列的層級呢?
隊(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孤立出來,形成“孤島”:
孤立了類和它的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ì)失效。