簡介:
如果你閱讀這本書,你可能已經牢牢掌握iOS開發的基礎,但這里有一些小特點和實踐是許多開發者並不熟悉的,甚至有數年經驗的開發者也是。在這一章里,你會學到一些很重要的開發技巧,但這仍遠遠不夠,你還需要積累更多的實踐來讓你的代碼更強力。
/*
本文翻譯自《iOS 7 Programming Pushing the Limits》一書的第三章“You May Not Know”,想體會原文精髓的朋友請支持原書正版。
——————(博客園、新浪微博)葛布林大帝
*/
目錄:
一. 最好的命名實踐
二. Property和實例變量(Ivar)的最佳實踐
三. 分類(Categories)
四. 關聯引用(Associative References)
五. Weak Collections
六. NSCache
七. NSURLComponents
八. CFStringTransform
九. instancetype
十. Base64 和 Percent編碼
十一. -[NSArray firstObject]
十二. 總結
十三. 更多閱讀
一、最好的命名實踐
在iOS開發里,命名規范極其重要。在下面的部分,我們將學習如何正確命名各種條目,以及為什么這樣命名。
1. 自動變量
Cocoa是動態類型的語言,你很容易對所使用的類型感到困惑。集合(數組、字典等等)沒有關聯它們的類型,所以這樣的意外很容易發生:
1 NSArray *dates = @[@”1/1/2000”]; 2 NSDate *firstDate = [dates firstObject];
編譯器沒有警告,但當你使用firstDate時,它很可能會報錯(an unknown selector exception)。錯誤是調用一個string dates數組。這個數組應該調用dateStrings,或者應該包含NSDate對象。這樣小心的命名將會避免很多令人頭痛的錯誤。
2. 方法
1)方法名應該清楚表明接收和返回的類型
例如,這個方法名是令人困惑的:
1 - (void)add; // 令人困惑
看起來add應該帶一些參數,但它沒有。難道它是增加一些默認對象?
這樣命名就清楚多了:
1 - (void)addEmptyRecord; 2 - (void)addRecord:(Record *)record;
現在addRecord:接收一個Record參數,看起來清楚多了。
2)對象的類型應符合名稱,如果類型和名稱不匹配,則容易弄混
這個例子展示了一個常見錯誤:
1 - (void)setURL:(NSString *)URL; // 錯誤的
這里錯誤是因為調用setURL時,應該接收一個NSURL,而不是一個NSString。如果你需要string,你需要增加一些指示讓它更明朗:
1 - (void)setURLString:(NSString *)string; 2 - (void)setURL:(NSURL *)URL;
這個規則不應過度使用。如果類型很明顯,別添加類型信息到變量上。一個叫做name的屬性就比叫做nameString的屬性更好。
3)方法名也有與內存管理和KVC相關的特定原則
雖然ARC使得其中的一些規則不再重要,但在ARC與非ARC進行交互時(包括Apple框架的非ARC代碼),不正確的命名規則仍會導致非常具有挑戰性的錯誤。
方法名應該永遠是小寫字母開頭,駝峰結構。
如果一個方法名以alloc、new、copy或者nutableCopy開頭,調用者擁有返回的對象。如果你的property的名字像newRecord這樣,這個規則可能會導致問題,請換一個名字。
get方法的開頭應該返回一個參照值,例如:
1 - (void)getPerson:(Person **)person;
不要使用get前綴作為property accessor的一部分,property name的getter應該為-name。
二、Property和實例變量(Ivar)的最佳實踐
Property應該代表一個對象的狀態,Getter應該沒有外部影響(它們可以具有內部影響,例如caching,但那些應該是調用者不可見的)。
避免直接訪問實例變量,使用accessor來代替。
在早期的ARC里,引起bug最常見的原因就是直接訪問實例變量。開發者沒有正確的retain和release實例變量,它們的應用就會崩潰或者內存泄露。由於ARC自動管理retain和release,一些開發者認為這個規則已經不再重要,但仍還有其他使用accessors的原因:
- KVO
- 也許使用accessor的最關鍵原因是,property可以被觀察到。如果你不使用accessor,你需要在每次修改property里的實例變量時調用willChangeValueForKey: 和 didChangeValueForKey: ,而accessor會在需要時自動調用這些方法。
- Side effects
- 在setter里,你或者你的子類可能包含side effects。通知可能被傳送、事件可能被注冊到NSUndoManager里,你不應該繞過這些side effects,除非它是必要的。
- Lazy instantiation
- 如果一個property被lazily instantiated,必須使用accessor來確保它的正確初始化。
- Locking
- 如果引進locking到一個property里來管理多線程代碼,直接訪問實例變量將違背你的lock,並可能導致程序崩潰。
- Consistency
- 在看到前面的內容后,有人可能會說應該只使用accessor,但這使得代碼很難維護。懷疑和解釋每一個直接訪問的實例變量,而不是記住哪些需要accessor哪些不需要,這樣使得代碼更容易審核、審閱和維護。Accessor,特別是synthesized accessors,已經在OC里被高度優化,它們值得使用。
這就是說,你不應該在這幾個地方使用accessor:
- Accessor內部
- 顯然,你不能在accessor內部使用自身。通常,你也不想在getter和setter內部使用它們自己(這可能創建無限循環),一個accessor應該訪問其自身的實例變量。
- Dealloc
- ARC極大地減少了dealloc,但它有時仍會出現。最好調用dealloc里的外部對象,該對象可能處於不一致的狀態,並很可能造成混淆。
- Initialization
- 類似dealloc,對象可能在初始化過程中處於不一致狀態,你不應該在此時銷毀通知或者其他的side effects。
三、 分類(Categories)
分類允許你在運行中的類里添加方法。任何類(甚至是由Apple提供的Cocoa類)都可以通過分類來拓展,這些新方法對類的所有實例都是可用的,分類聲明如下:
1 @interface NSMutableString (PTLCapitalize) 2 - (void)ptl_capitalize; 3 @end
PTLCapitalize是分類的名稱,注意這里沒有聲明任何實例變量。
分類不能聲明實例變量,也不能synthesize properties。
分類可以聲明properties,因為它只是聲明方法的另一種方式。
分類不能synthesize properties,因為這會創建一個實例變量。
1. +load
分類在運行時附加到類,這可能定義分類為動態加載,所以分類可以很晚添加(雖然你不能在iOS里編寫自己的動態庫,但系統框架是動態加載的,並且包括分類)。OC提供了一個名為 +load 的東西,在分類首次附加時運行。隨着 +initialize,你可以使用它來實現指定分類的設定,例如初始化靜態變量。你不能安全的在分類里使用+initialize,因為類可能已經實現它。如果有多個分類實現+initialize,那么運行一個沒有意義。
我希望你已經准備好要問一個顯而易見的問題:“如果分類不能使用+initialize,因為他們可能與其他分類沖突,那么多個分類實現+load呢?”這正是OC runtime神奇的地方之一, +load方法是runtime的特例,是每一個分類能實現它,並且所有的實現都運行。當然,你不應該嘗試手動調用+load。
四、關聯引用(Associative References)
關聯引用允許你附加key-value數據到任何對象。這個能力有多種用途,但最常用的是允許你的分類添加數據的property。
考慮一個Person類的情況,你想使用分類來添加一個叫做emailAddress的新property。也許你在其他程序里使用Person類,並且有時使用email address而有時不用,因此使用分類是可以避免開銷的很好解決方案。或者,你沒有自己的Person類,並且維護者不會為你添加property,你該如何解決這個問題?首先來看一下基礎的Person類:
1 @interface Person : NSObject 2 @property (nonatomic, readwrite, copy) NSString *name; 3 @end 4 5 @implementation Person 6 @end
現在你可以添加新的property了,在分類里使用關聯引用:
1 #import <objc/runtime.h> 2 @interface Person (EmailAddress) 3 @property (nonatomic, readwrite, copy) NSString *emailAddress; 4 @end 5 6 @implementation Person (EmailAddress) 7 static char emailAddressKey; 8 9 - (NSString *)emailAddress { 10 return objc_getAssociatedObject(self, &emailAddressKey); 11 } 12 13 - (void)setEmailAddress:(NSString *)emailAddress { 14 objc_setAssociatedObject(self, &emailAddressKey, emailAddress, OBJC_ASSOCIATION_COPY); 16 } 17 @end
注意關聯引用是基於key的內存地址,而不是它的值。emailAddressKey里存儲什么並不重要,它只需要有一個唯一、不變的地址,這就是為什么它通常使用未分配的static char作為key。
關聯引用有很好的內存管理,用以參照objc_setAssociatedObject的參數傳遞正確處理copy、assign或者retain。當相關的對象被deallocated,它們會released。這實際上意味着在另一個對象被銷毀時,你可以使用相關的對象進行追蹤,例如:
1 const char kWatcherKey; 2 3 @interface Watcher : NSObject 4 @end 5 6 #import <objc/runtime.h> 7 8 @implementation Watcher 9 - (void)dealloc { 10 NSLog(@"HEY! The thing I was watching is going away!"); 11 } 12 @end 13 ... 14 NSObject *something = [NSObject new];
15 objc_setAssociatedObject(something, &kWatcherKey, [Watcher new], OBJC_ASSOCIATION_RETAIN);
這種技術對於調試非常有用,同時也可用於非調試任務,例如執行清理。
使用關聯引用是附加相關對象到alert panel或者control的好方法,例如你可以附加一個“represented object”到alert panel,代碼如下:
1 ViewController.m (AssocRef) 2 id interestingObject = ...; 3 UIAlertView *alert = [[UIAlertView alloc] 4 initWithTitle:@"Alert" message:nil 5 delegate:self 6 cancelButtonTitle:@"OK" 7 otherButtonTitles:nil]; 8 objc_setAssociatedObject(alert, &kRepresentedObject, 9 interestingObject, 10 [alert show];
許多程序在調用里使用實例變量處理這個任務,但關聯引用更簡潔。對於那些熟悉Mac的開發者,這些代碼類似於representedObject
,但卻更靈活。
聯想引用的一個限制是,它們沒有與encodeWithCoder:整合,因此它們很難通過一個分類來序列化。
五、Weak Collections
大多數Cocoa的集合例如NSArray、NSSet和NSDictionary都具有強大功能,但它們不適合某些情況。NSArray與NSSet會保留你存儲進去的對象,NSDictionary會保存value和key,這些行為通常是你想要的,但對於某些工作它們並不適合。幸運的是,自從iOS6開始,一些其他的集合開始出現:NSPointerArray、NSHashTable與NSMapTable。它們統稱為Apple文檔的指針集合類(pointer collection classes),並且有時使用NSPointerFunctions類來進行配置。
NSPointerArray類似NSArray, NSHashTable類似NSSet,而NSMapTable類似NSDictionary。每個新的集合類都可以配置為保持弱引用,指向空對象或者其他異常情況。NSPointerArray的一個額外好處是它還可以存儲NULL值。
指針集合類可以使用NSPointerFunctions來廣泛的配置,但大多數情況下,它只是簡單的傳送一個NSPointerFunctionsOptions flag到–initWithOptions:。最常見的情況,例如+weakObjectsPointerArray,有自己的構造函數。
六、 NSCache
NSCache有幾個被低估的功能,比如事實上它是線程安全的,你可能在任何無鎖的線程里改變一個NSCache。NSCache也被設計來融合對象遵從<NSDiscardableContent>,其中最常見的類型是NSPurgeableData,通過調用beginContentAccess 與 endContentAccess,你可以控制何時安全放棄這個對象。這不僅在你的應用運行時提供自動緩存管理,它甚至有助於你的應用被暫停。通常情況下,當內存緊張時,內存警告沒有釋放出足夠的內存,iOS會開始殺死暫停在后台的應用。在這種情形下,你的應用沒有得到delegate信息,就這樣被殺死。不過如果你使用NSPurgeableData,iOS會釋放這塊內存給你,即使你的應用被暫停。
想得到更多關於NSCache的信息,請參考官方文檔NSDiscardableContent與NSPurgeableData。
七、NSURLComponents
有時,Apple會悄悄添加一些有趣的類。在iOS7里,Apple增加了NSURLComponents,但卻沒有相關的參考文檔,你需要到NSURL.h里來查看它(NSURL.h里有許多有趣的方法,你可以進去仔細研究)。
NSURLComponents讓取出URL的各個部分變得容易,例如:
1 NSString *URLString = 2 @"http://en.wikipedia.org/wiki/Special:Search?search=ios"; 3 NSURLComponents *components = [NSURLComponents 4 componentsWithString:URLString]; 5 NSString *host = components.host;
你也可以使用NSURLComponents來組成或修改URL:
1 components.host = @"es.wikipedia.org"; 2 NSURL *esURL = [components URL];
八、 CFStringTransform
CFStringTransform可以以神奇的方式來音譯字符串,例如,你可以使用選項kCFStringTransformStripCombiningMarks: 來刪除重音符號:
1 CFMutableStringRef string = CFStringCreateMutableCopy(NULL, 0, CFSTR("Schläger")); 2 CFStringTransform(string, NULL, kCFStringTransformStripCombiningMarks, false); 3 ... => string is now “Schlager” CFRelease(string);
當你在處理非拉丁文字系統時(例如中文和阿拉伯語),CFStringTransform更是如虎添翼,它可以轉換許多書寫系統為拉丁文字。例如,你可以將中文轉換為拼音:
1 CFMutableStringRef string = CFStringCreateMutableCopy(NULL, 0, CFSTR("你好")); 2 CFStringTransform(string, NULL, kCFStringTransformToLatin, false); 3 ... => string is now “nˇı hˇao” 4 CFStringTransform(string, NULL, kCFStringTransformStripCombiningMarks, 5 false); 6 ... => string is now “ni hao” CFRelease(string);
九、 instancetype
Objective-C中早就有了一些微妙的子類的問題。考慮下面的情況:
1 @interface Foo : NSObject 2 + (Foo *)fooWithInt:(int)x; @end 3 @interface SpecialFoo : Foo 4 @end 5 ... 6 SpecialFoo *sf = [SpecialFoo fooWithInt:1];
這段代碼會產生一個警告:“Incompatible pointer types initializing ’SpecialFoo *’ with an expression of type ’Foo *’。”問題在於fooWithInt返回了一個Foo對象,而編譯器無法知道返回的類型確實是一個更具體的類(SpecialFoo),這種情況相當常見。
有幾種解決這個問題的方案。
方案一:首先,你可能重載fooWithInt:,代碼如下:
1 @interface SpecialFoo : Foo 2 + (SpecialFoo *)fooWithInt:(int)x; 3 @end 4 5 @implementation SpecialFoo 6 + (SpecialFoo *)fooWithInt:(int)x { 7 return (SpecialFoo *)[super fooWithInt:x]; 8 }
這種方法雖然可以解決,但非常不方便,你不得不只是為了類型轉換重寫許多方法。
方案二:你還可以在調用時執行類型轉換:
1 SpecialFoo *sf = (SpecialFoo *)[SpecialFoo fooWithInt:1];
這種方法雖然也可以解決,但對調用者很不方便,加入大量的類型轉換也會消除類型檢查,因此它更容易出錯。
方案三:最常見的解決辦法是返回ID類型:
1 @interface Foo : NSObject + (id)fooWithInt:(int)x; 2 @end 3 4 @interface SpecialFoo : Foo 5 @end 6 ... 7 SpecialFoo *sf = [SpecialFoo fooWithInt:1];
這種辦法相當方便,而且消除了類型檢查。這是上面三個方案中最好用的,這就是為什么id無處不在的原因。
方案四:使用instancetype作為返回類型
instancetype表示“當前類”(id與instancetype的區別請自行Google),比使用id更適合解決這個問題。代碼如下:
1 @interface Foo : NSObject 2 + (instancetype)fooWithInt:(int)x; 3 @end 4 5 @interface SpecialFoo : Foo 6 @end 7 ... 8 SpecialFoo *sf = [SpecialFoo fooWithInt:1];
為了保持一致性,最好使用instancetype作為雙方的init方法和便利的構造函數的返回類型。
十、Base64 和 Percent編碼
Cocoa早就需要方便的訪問Base64編碼和解碼。Base64是許多Web協議的標准,並且在許多你需要存儲任意數據到一個字符串里的情況下非常有用。
在iOS7,新的NSData方法例如initWithBase64EncodedString:options: 和 base64EncodedStringWithOptions: 可以用來在Base64和NSData間轉換。
Percent編碼對於Web協議同樣重要,特別是URLs,你現在可以使用[NSString stringByRemovingPercentEncoding]來對percent編碼進行解碼。盡管已經有stringByAddingPercentEscapesUsingEncoding:方法來進行percent編碼,iOS7還是添加了一個stringByAddingPercentEncodingWithAllowedCharacters:方法,允許你控制percent編碼的字符。
十一、 -[NSArray firstObject]
這是一個極小的改變,但是我仍要提到它,因為我們等待它已久:多年來,許多開發者用實現分類來獲取數組的首個對象,現在Apple終於添加了方法firstObject。就像lastObject一樣,如果數組是空的,firstObject返回nil,而不是objectAtIndex:0。
十二、摘要
Cocoa有很長的歷史,充滿了傳統和慣例,同時Cocoa也是一個發展的、活躍的框架。在這個章節里面,你已經學習到一些數十年里OC開發的最佳實踐。你學會了為類、方法和變量選擇最好的命名方式;學到了一些並不眾所周知的功能例如associative references和NSURLComponents。即使作為老練的OC開發者,你仍希望學到一些之前並不知道的Cocoa技巧。
十三、更多閱讀
1. 官方文檔
- CFMutableString
- Reference CFStringTokenizer
- Reference Collections Programming Topics
- Collections Programming Topics, “Pointer Function Options”
- Programming with Objective-C
2. 其他資源
- nshipster.com
- 馬特·湯普森的博客,每周更新
- https://github.com/00StevenG/NSString-Japanese
- 如果你需要處理日文文本,這是一個非常有用的分類,用來處理各種復雜的書寫系統
本書源代碼:http://pan.baidu.com/s/1bnnJZIJ
