[iOS翻譯]《iOS 7 Programming Pushing the Limits》系列:你可能不知道的Objective-C技巧


簡介:

如果你閱讀這本書,你可能已經牢牢掌握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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM