Effective Objective-C
一、熟悉Objective-C
1. 了解Objective-C的起源
- Objective-C為C語言添加了面向對象的特性,是其超集。使用動態綁定的消息結構,運行時才會檢查對象類型。接收一條消息后,究竟應執行何種代碼,由運行期環境而非編譯器決定;
- 尤其要掌握Objective-C語言的內存模型和指針;
2. 在類的文件中盡量少引入其他頭文件
- 為了不增加不必要的編譯時間,降低類的耦合;
- 如果不需要類的接口細節,應在頭文件中使用向前聲明來提及別的類,在實現文件中引入那個類的頭文件;
- 如果要遵循一項協議,協議盡量單獨放在一個頭文件中,然后在實現文件中頭文件并通過class-continuation遵循該協議;
3. 多用字面量語法,少用與之等價的方法
- 更加簡明扼要;
- 應該通過去下標訪問數組或字典
- 用字面量語法創建數組或字典是,若值中有nil,則會拋出異常,這樣更方便查找問題
4. 多用類型常量,少用#define預處理命令
- 預處理命令定義出來的常量不含類型信心,編譯器只會在編譯前執行查找與替換操作。即使有人重新定義常量值,編譯器也不會產生警告;
- static const定義只在編譯單元內可見的常量。定義基礎類型 static const int k = 0; 定義指針類型對象 NSString * static const str; (const要修飾變量名表示常量指針)
- 在頭文件中使用extern來聲明全局常量,并在相關實現文件中定義其值。extern NSString * const str; NSString * const str=@"";
5.用枚舉表示狀態、選項、狀態碼。NS_ENUM, NS_OPTIONS
二、對象、消息、運行期
6. 理解“屬性”這一概念
- 自動創建存取方法;
- 屬性特質要掌握,讀寫權限、內存管理語義;
- 開發iOS程序時應該使用nonatomic屬性,因為atomic會嚴重影響性能;
7. 在對象內部盡量直接訪問實例變量
- 在對象內部讀取數據時,應該直接通過實例變量來讀,而寫入數據時,則應通過屬性來寫。這樣做既能提高讀取操作的速度,又能控制對屬性的寫入操作;
- 在初始化方法及dealloc方法中,總是應該直接通過實例變量來讀寫數據。因為子類可能重載讀寫方法;
- 有時會惰性初始化(Lazy initialization),這種情況下需要通過屬性來讀取數據;
8. 理解對象等同性這一概念
- 若想檢測對象的等同性,請提供"isEqual"與hash方法
9. 以“類族模式”隱藏實現細節
- 采用了工廠模式,抽象基類
- 類族模式可以把實現細節隱藏在一套簡單的公共接口后面;
10. 在既有類中使用關聯對象存放自定義數據
- 給對象動態添加屬性 objc_setAssociatedObject / objc_getAssociateObject
- 可以通過“關聯對象”機制把兩個對象連起來,比如把對象與回調方法block關聯起來,就能在定義對象的附近定義block并關聯起來;
- 這種做法通常會引入難于查找的bug,所以慎用;
11. 理解objc_msgSend的作用
- 消息由接收者、選擇子、及參數構成。給某對象“發送消息”,也就相當于在該對象上調用方法;
- 發送某消息的全部消息都要由“動態消息派發系統”來處理,該系統會查出對應的方法,并執行其代碼;
12. 理解消息轉發機制
- 若對象無法響應某個選擇子,則進入消息轉發流程;
- 通過運行期的動態方法解析功能,我們可以在需要用到某個方法時再將其加入類中;
- 對象可以把其無法解讀的某些選擇子轉交給其他對象來處理;
- 經過上述兩步之后,如果還是沒辦法處理選擇子,那就啟動完整的消息轉發機制;
13. 用方法調配技術調試黑盒方法
- method swizzling;
- 在運行期,可以向類中新增或替換選擇子所對應的方法實現;
- 適合用于調試,對某個方法進行hook,
method_exchangeImplementations(m1, m2)
14. 理解”類對象“的用意
- 每個實例都有一個指向Class對象的指針,用以表明其類型,而這些Class對象則構成了類的繼承體系;
- 如果對象類型無法再編譯期確定,那么就該使用類型信息查詢方法來探知;(isMemberOfClass, isKindOfClass)
- 盡量使用類型信息查詢方法來確定對象類型,而不要直接比較類對象,因為某些對象可能實現了消息轉發機制;
- 類對象所屬的類型(isa指針指向)是另外一個類,叫做”元類“,用來表述類對象本身所具備的元數據。類方法就定義于此;
三、接口與API設計
15. 用前綴避免命名空間沖突
- 要選擇與你公司、應用程序或二者相關聯的名稱作為類名的前綴,并在所有代碼中均使用這一前綴;
- 若自己開發的程序庫中使用了第三方庫,則應為其中的名稱加上前綴
16. 提供全能初始化方法
- 在類中提供一個全能初始化方法,并于文檔里指明。其他初始化方法均應調用此方法;
- 若全能初始化方法與超類不同,則需覆寫超類中的對應方法;
- 如果超類的初始化方法不適用于子類,那么應該覆寫這個超類方法,并在其中拋出異常;
17. 實現description
方法
- 實現description方法返回一個有意義的字符串,用以描述該實例;
- 若想在調試時打印出更詳盡的對象描述信息,則應實現
debugDescription
方法;
18. 盡量使用不可變對象
- 盡量創建不可變對象
- 若某屬性僅可于對象內部修改,則在
class-continuation
分類中將其由readonly屬性擴展為readwrite屬性; - 不要把可變的collection作為屬性公開,而應提供相關方法,以此修改對象中的可變collection;
19. 使用清晰而協調的命名方式
- 起名時應遵從Objective-C的命名規范,這樣創建出來的接口更容易為開發者所理解;
- 方法名要言簡意賅,從左至右讀起來要像個日常用語中的句子才好;
- 方法名里不要使用縮略后的類型名稱;比如
str
,應用string
; - 給方法起名時的第一要務就是確保其風格與你自己的代碼或所要集成的框架相符;
20. 為私有方法名加前綴
-
p_privateMethod
,這類風格,可以很容易將其與公共方法區分開; - 不要單用一個下劃線做私有方法的前綴,因為這種做法是預留給蘋果公司用的;
21. 理解Objective-C錯誤模型
- Objective-C語言現在所采用的辦法是:只在及其罕見的情況下拋出異常,異常拋出之后,無需考慮恢復問題,而且應用程序此時也應該退出;
- 捕獲異常可能導致ARC模式下的內存泄露,所以慎用;
- 只有發生了可使整個應用程序崩潰的嚴重錯誤時,才應使用異常;
- 在錯誤不那么嚴重情況下可以指派委托方法(
delegate method
)來處理錯誤,也可以把錯誤信息放在NSError對象里,經由“輸出參數”反饋給調用者;
22. 理解NSCopying協議
- 如果想令自己的類支持拷貝操作,就要實現NSCopying協議,并提供方法
- (id)copyWithZone:(NSZone *)zone
;這里的參數zone,是因為以前開發程序需要把內存分成不同的區(zone),對象會被創建在某個zone里面。現在不需要指定zone了,因為每個程序都只有一個默認zone; - 類似于序列化對象的NSCoding協議,提供
initWithCoder
和encodeWithCoder
方法; - 深拷貝與淺拷貝
- 如果自定義的對象分為可變版本與不可變版本,那就要同時實現NSCopying與NSMutableCopying協議;
- 如果你所寫的對象需要深拷貝,那么可考慮新增一個專門執行深拷貝的方法
四、協議與分類
23. 通過委托與數據源協議進行對象間通信
- 使用委托進行對象間通信,為了不阻塞UI線程,在獲取完數據后再回調委托對象進行頁面數據刷新;
- delegate屬性需定義成weak;
- 委托模式為對象提供了一套接口,使其可由此將相關事件告知其他對象;
- 將委托對象應該支持的接口定義成協議,在協議中把可能需要處理的事件定義成方法;
- 當某對象需要從另外一個對象中獲取數據時,可以使用委托模式。這種情況下,該模式亦稱“數據源協議”(data source protocol);
- 若有必要,可實現含有位段的結構體,將委托對象是否能相應相關協議方法這一信息進行緩存;
24. 將類的實現代碼分散到便于管理的數個分類中
- 使用分類Category機制把類的實現代碼劃分成易于管理的小塊;
- 將應該視為“私有”的方法歸入名叫Private的分類中,以隱藏實現細節;
25. 總是為第三方類的分類名稱加前綴
- 為了避免和第三方類里的分類或方法重名。如果那個分類的加載時機晚于你所寫的分類,那么你的代碼就會被覆蓋掉(不過編譯器貌似會提醒);
- 向第三方類中添加分類時,總應給其名稱加上你專用的前綴。方法名也應加上前綴;
26. 勿在分類中聲明屬性
- 把封裝數據所用的全部屬性都定義在主接口里;
- 在Class-Continuation分類之外的其他分類中,可以定義存取方法,但盡量不要定義屬性;
27. 使用class-continuation分類隱藏實現細節
- 其實就是常用的在實現文件里@interface MyClass()這樣;
- 把細節隱藏起來,只供本類使用;
- 在頭文件中只讀,在class-continuation中擴展為可讀寫;
- 通過class-continuation分類向類中新增實例變量;
- 把私有方法的原型聲明在class-continuation分類里,也可以不聲明;
- 若想使類所遵循的協議 不為人知,則可于class-continuation分類中聲明;
28. 通過協議提供匿名對象
- 不是匿名內部類的意思(block有這樣的作用);
- 例如,返回類型id<DatabaseConnection>,處理數據庫連接所用的類的名稱就不會泄露了,設計API的時候也都可以通過這個方法處理數據庫連接對象;
- 協議可在某種程度上提供匿名類型。具體的類型可以淡化成遵從某協議的id類型,協議里規定了對象所應實現的方法;
- 如果具體類型不重要,重要的是對象能夠響應特定方法,那么可以使用匿名對象來表示;
五、內存管理
29. 理解引用計數
- 引用計數機制通過可以遞增遞減的計數器來管理內存。對象創建好之后,其保留計數至少為1。若保留計數為正,則對象繼續存活。當保留計數將為0時,對象就被銷毀了;
- 對象生命期中,其余對象通過引用來保留或釋放此對象。保留與釋放操作分別會遞增及遞減保留計數;
30. 以ARC簡化引用計數
- 若方法名以
alloc
new
copy
mutableCopy
開頭,則其返回的對象歸調用者所有,引用計數為1; - 有ARC之后,程序員就無須擔心內存管理的問題了。使用ARC編程,可省去類中的許多“樣板代碼”;
- 在ARC環境下,變量的內存管理語義可以通過修飾符進行指明,而原來則需要手工執行“保留”及“釋放”操作;
- 由方法所返回的對象,其內存管理語義總是通過方法名來體現。ARC將此確定為開發者必須遵守的規則;
- ARC只負責管理OC對象的內存。尤其要注意:CoreFoundation對象將不歸ARC管理,開發者必須適時調用CFRetain/CFRelease;
31. 在dealloc方法中只釋放引用并解除監聽;
- 在dealloc方法里,應該做的事情就是釋放指向其他對象的引用,并取消原來訂閱的
KVO
或NSNotificationCenter
通知,不要做其他事情; - 如果對象持有文件描述符等系統資源,那么應該專門編寫一個方法來釋放此種資源。這樣的類要和其使用者約定:用完資源后必須調用close方法;
- dealloc方法不一定會執行,不調用dealloc方法時為了優化程序效率(應用程序終止時,其占用的資源全部返還給系統),所以清理方法應該要有專門close方法而非dealloc中清理資源,比如關閉socket連接或數據庫連接;
- 執行異部任務的方法不應在dealloc里調用;只能在正常狀態下執行的那些方法也不應在dealloc里調用,因為此時對象已處于正在回收的狀態欄了;
- 在dealloc里不要調用屬性的存取方法,因為有人可能會覆寫這些方法,并于其中做一些無法再回收階段安全執行的操作;
32. 編寫“異常安全代碼”時留意內存管理問題
- 捕獲異常時,一定要注意將try塊內創立的對象清理干凈;
- 在默認情況下,ARC不生成安全粗粒異常所需的清理代碼。開啟編譯器標志后(打開
-fobj-arc-exception
),可生成這種代碼,不過會導致應用程序變大,而且會降低運行效率; - 如何設置編譯器標志?
Targets -> Build Phases -> Compile Sources
, 雙擊文件并輸入編譯器標志即可;
33. 以若引用避免保留環
- 將某些引用設為weak,避免循環引用;
- weak引用可以自動清空,也可以不自動清空。自動清空是隨著ARC而引入的新特性,由運行期系統來實現。在具備自動清空功能的弱引用上,可以隨意讀取其數據,因為這種弱引用不會指向已經回收過的對象;
34. 以“自動釋放池塊”降低內存峰值
- 自動釋放池排布在棧中,對象收到autorelease后,系統將其放入最頂端的池里;
- 合理運用自動釋放池,可降低應用程序的內存峰值;
- @autoreleasepool這種新式寫法能創建出更為輕便的自動釋放池;
35. 用“僵尸對象”調試內存管理問題
- 系統在回收對象時,可以不將其真的回收,而是轉化為僵尸對象,通過環境變量NSZoombieEnabled可開啟此功能;(但是xcode占用內存會變大好多,在調試野指針問題時才應該開啟);
36. 不要使用retainCount
- ARC已經將此方法廢棄了,如果使用的話編譯器直接報錯;
- 對象的保留計數看似有用,實則不然,因為任何給定時間點上的“絕對保留技術”都無法反應對象生命期的全貌;
六、塊與大中樞派發
37. 理解block這一概念
- block的強大之處是,在聲明它的范圍里,所有變量都可以為其所捕獲;
- 默認情況下,為block所捕獲的變量不可以在block里修改;添加__block修飾符可使得變量在block內修改;(注意對象是可以捕獲到并可修改的,如self)
- block也可以理解為一個對象,會執行一段代碼,類似一個方法;
- block是C, C++, Objective-C中的詞法閉包(closure,匿名內部類);
- block可接受參數,也可返回值;
- block可以分配在棧或堆上,也可以是全局的。分配在棧上的可以拷貝到堆里,這樣的話就和標準的OC對象一樣了,具備引用計數;
38. 為常用的block類型創建typedef
- 以typedef重新定義block類型,可令block變量用起來更加簡單;
- 定義新類型時應遵從現有的命名習慣
- 不妨為同一個block簽名定義多個類型別名,如果要重構的代碼使用了block的某個別名,就只需要修改相應的typedef中的block塊簽名即可。無須改動其他typedef;
39.用handler降低代碼分散程度
- 在創建對象時,可以使用內聯的handler塊將相關業務邏輯一并聲明;
- 在有多個實例需要監控時,如果采用委托模式,那么經常需要根據傳入的對象來切換,而若改用handler實現,則可直接將block與相關對象放在一起;
- 設計API時如果用到了handler塊,那么可以增加一個參數,調用者可通過此參數來決定應該把block安排在哪個隊列上執行;
40. 在block引用其所屬對象時不要出現保留環
- 如果block所捕獲的對象直接或間接地保留了block本身,那么就得當心循環引用問題;
- 一定要找個適當時機接觸保留環,而不能把責任推給API的調用者;
41. 多用派發隊列,少用同步鎖
- dispatch_sync, dispatch_async
- 派發隊列可用來表達同步語義,幣使用@synchronized或NSLock對象更簡單;
- 將同步與異步派發結合起來,可以實現與普通加鎖機制一樣的同步行為,而這么做卻不會阻塞執行異步派發的線程;
- 使用同步隊列及柵欄block,可以令同步行為更加高效;
42. 多用GCD,少用performSelector系列方法
- performSelector系列方法在內存管理方面容易有疏。無法確定將要執行的選擇子具體是什么,因而ARC編譯器也就無法插入適當的內存管理方法;
- performSelector系列方法所能處理的selector太過局限了,選擇子的返回值類型及發送給方法的參數個數都受限制;
- 如果想把任務放在另一個線程上執行,那么最好不要用performSelector系列方法,而是應該把任務封裝在block里,然后調用GCD的相關方法來實現;
43. 掌握GCD及操作隊列的使用實際
- 操作隊列
NSOperationQueue
,在解決多線程與任務管理問題時,派發隊列并非唯一方案; - 操作隊列提供了一套高層的OC API,能實現純GCD所具備的絕大部分功能。而且能完成一些更加復雜的操作:取消某個操作、指定操作間的以來關系、通過KVO監控對象屬性(isFinished, isCanceled)、指定操作優先級、重用NSOperation對象等;
44. 通過Dispatch Group
機制,根據系統資源狀況來執行任務
- 一系列任務可歸入一個
dispatch group
之中,開發者可以在這組任務執行完畢時獲得通知,dispatch_group_notify
函數可實現; - 通過dispatch_group,可以在并發式派發隊列里同時執行多項任務,此時GCD會根據系統資源狀況來調度任務。
45. 使用dispatc_once來執行只需運行一次的線程安全代碼
- 原子操作,只執行一次,線程安全、更高效;
- 標記
dispatch_once_t
應該生命在static或global作用域中,這樣的話,在把只需執行一次的block傳給dispatch_once函數時,傳進去的標記也是相同的;
46. 不要使用dispatch_get_current_queue
- dispatch_get_current_queue函數的行為常常與預期不同。此函數已廢棄,只做調試用;
- 由于派發隊列是按層級來組織的,所以無法單用某個隊列對象來描述“當前隊列”這一概念;
- dispatch_get_current_queue函數用于解決由于不可重入的代碼所引發的死鎖,然而能用此函數解決的問題,通常也能改用“隊列特定數據”來解決;(在當前隊列上同步執行一個新任務,就會引發死鎖,原因是不可重入的代碼);
七、系統框架
47. 熟悉系統框架
- Foundation與CoreFoundation的無縫橋接;
- CFNetWork, CoreAudio, AVFoundation, CoreData, CoreText;
48. 多用block枚舉,少用for循環
- for循環,遍歷dictionary或set比較麻煩,會增加額外開銷;代碼簡單;
- 枚舉類NSEnumerator,不能獲取當前下標;可反向遍歷;
- 快速遍歷for..in,簡單效率高,但對于dictionary還是不便;不能獲取當前下標;繼承自NSEnumerator類;
- 基于block的遍歷方式,夠強大;
- block枚舉法本身就能通過GCD并發執行遍歷操作;
49. 對自定義其內存管理語義的collection使用無縫橋接
- 例如,NSArray -> CFArrayRef;
- 在CoreFoundation層面創建collection時,可以指定許多回調函數,這些函數表示此collection應該如何處理其元素。然后通過無縫橋接技術,將其轉換成具備特殊內存管理語義的OC collecion;
50. 構建緩存是選用NSCache而非NSDictionary
- NSCache具備自動刪減功能,而且是線程安全的;此外,它與NSDictionary不同,并不會拷貝鍵;
- 將NSPurgeableData與NSCache搭配使用,可實現自動清除數據功能,也就是說當NSPurgeableData對象所占內存被系統丟棄時,該對象本身也將從緩存中移除;
51. 精簡initialize與load的實現代碼
- 在load方法中使用其他類是不安全的;因為不知道系統加載類的順序,使用別的類的時候可能還沒有加載該類;
- initialize方法在程序首次用該類之前調用,且只調用一次;
- load和initialize精簡一些,有助于保持應用程序的響應能力;
- 無法再編譯器設定的全局常亮,可以在initialize方法里初始化;
52. 別忘了NSTimer會保留目前對象
- repeat的Timer記得invalidate;
- NSTimer會保留其目標targer,知道計時器失效為止;
- 可以擴充NSTimer功能,用block來打破保留環。不過,除非NSTimer將來在公共接口里提供此功能,否則必須創建分類,將相關實現代碼加入其中;引入中間者block,設置weak引用,三者之前就不會出現保留換了;