本文授權轉載,作者:千煌89(簡書)
本篇是我閱讀《Effective Objective-C 2.0》的摘要與總結。
一、熟悉Objective-C
了解Objective-C語言的起源
Objective-C為C語言添加了面向對象特性,是其超集。Objective-C使用動態綁定的消息結構,也就是說,在運行時才會檢查對象類型。接收一條消息之後,究竟應執行何種代碼,由運行環境而非編譯器來決定。
理解C語言的核心概念有助於寫好Objective-C程序。尤其要掌握內存模型與指針。
在類的頭文件中盡量少引入其他頭文件
除非確有必要,否則不要引入頭文件。一般來說,應在某個類的頭文件中使用向前聲明(forward declaring)來提及別的類,並在實現文件中引入那些類的頭文件。這樣做可以盡量降低類之間的耦合。
有時無法使用向前聲明,比如要聲明某個類遵循一項協議。這種情況下,盡量把“該類遵循某協議”的這條聲明移至“class-continuation”分類中。如果不行的話,就把協議單獨放在一個頭文件中,然後將其引入。
多用literal語法,少用與之等價的方法
比如多用NSArray *array = @[@1,@2];少用NSArray *array = [NSArray arrayWithObjects:@1,@2,nil];
應該使用literal語法來創建字符串,數值,數組,字典。與創建此類對象的常規方法相比,這麼做更加簡明扼要。
應該通過取下標操作來訪問數組下標或字典中的鍵所對應的元素。
用literal語法創建數組或字典時,若值中有nil,則會拋出異常。因此,務必確保值裡不含nil。
多用類型常量,少用#define預處理指令
不要用預處理指令定義常量。這樣定義出來的常量不含類型信息,編譯器只是會在編譯前據此執行查找與替換操作。即使有人重新定義了常量值,編譯器也不會產生警告信息,這將導致應用程序中的常量值不一致。
在實現文件中使用static const來定義只在編譯單元內可見的常量。由於此類常量不在全局符號表中,所以無需為其名稱加前綴。
在頭文件中使用extern來聲明全局常量,並在相關實現文件中定義其值。這種常量要出現在全局符號表中,所以其名稱要加以區隔,通常用與之相關的類名做前綴。
用枚舉表示狀態、選項、狀態碼
應該用枚舉來表示狀態機的狀態、傳遞給方法的選項遺跡狀態碼等值,給這些值起個易懂的名字。
如果把傳遞給某個方法的選項表示為枚舉型,而多個選項又可同時使用,那麼就將各選項值定義為2的冪,以便通過按位或者操作將其組合起來。
用NS_ENUM與NS_OPTIONS宏來定義枚舉類型,並指明其底層數據類型。這樣做可以確保枚舉是用開發者所選的底層數據類型實現出來的,而不會采用編譯器所選的類型。
在處理枚舉類型的switch語句中不要實現default分支。這樣的話,加入新枚舉之後,編譯器就會提示開發者:switch語句並未處理所有的枚舉。
二、對象、消息、runtime
理解“屬性”這一概念
可以通過@property語法來定義對象中所封裝的數據。
通過“特質”來指定存儲數據所需的正確語義
在設置屬性所對應的實例變量時,一定要遵從該屬性所聲明的語義。
開發iOS程序時,應該使用nonatomic屬性,因為atomic屬性會嚴重影響性能。
在對象內部盡量直接訪問實例變量
在對象內部讀取數據時,應該直接通過實例變量來讀,而寫入數據時,應該通過屬性來寫。
在初始化方法及dealloc方法中,總是應該直接通過實例變量來讀寫數據。
有時會使用惰性初始化技術配置某份數據,這種情況下,需要通過屬性來讀取數據。
理解“對象等同性”這一概念
若想檢測對象的等同性,請提供“isEqual:”與hash方法。
相同的對象必須具有相同的hash碼,但是兩個hash碼相同的對象卻未必相同。
不要盲目的逐個監測每條屬性,而是應該依照具體需求來制定檢測方案。
編寫hash方法時,應該使用計算速度快而且哈希碼碰撞幾率低的算法。
以“類族模式”隱藏實現細節
類族模式可以把實現細節隱藏在一套簡單的公共接口後面。
系統框架中經常使用類族。
從類族的公共抽象基類中繼承子類時要當心,若有開發文檔,則應首先閱讀。
在既有類中,使用關聯對象(Associated Object)存放自定義數據
可以通過“關聯對象”機制來把兩個對象連起來。
定義關聯對象時可指定內存管理語義,用以模仿定義屬性時所采用的“擁有關系”與“非擁有關系”。
只有在其他做法不可行時才應選用關聯對象,因為這種做法通常會引入難於查找的bug。
理解objc_msgSend的作用
消息由接受者,selector及參數構成。給某對象“發送消息”也就相當於在該對象上調用方法。
發給某對象的全部消息都要由“動態消息派發系統”來處理,該系統會查出對應的方法,並執行其代碼。
理解消息轉發機制
若對象無法響應某個selector,則進入消息轉發流程。
通過運行期的動態方法解析功能,我們可以在需要用到某個方法時再將其加入類中。
對象可以將其無法解讀的某些selector轉交給其他對象處理。
經過上述兩步後,如果還是沒辦法處理selector,那就啟動完整的消息轉發機制。
用method swizzling調試黑盒方法
在runtime中,可以向類中新增或替換selector所對應的方法實現。
使用另一份實現來替換原有的方法實現,這道工序叫做method swizzling,開發者常用此技術向原有視線中添加功能。
一般來說,只有調試程序的時候才需要在runtime中修改方法實現,這種做法不宜濫用。
理解“類對象”的用意
每個實例都有一個指向Class對象的指針,用以表明其類型,而這些Class對象則構成了累的繼承體系。
如果對象類型無法在編譯期確定,那麼就應該使用類型信息查詢方法來探知。
盡量使用類型信息查詢方法來確定對象類型,而不要直接比較類對象,因為某些對象可能實現了消息轉發功能。
三、接口與API設計
用前綴避免命名空間沖突
選擇與你公司、應用程序或者二者皆有關聯之名稱作為類名的前綴,並在所有代碼中均使用這一前綴。
若自己所開發的程序庫中用到了第三方庫,則應為其中的名稱加上前綴。
Apple宣稱保留使用所有兩字母前綴的權利,所以自己所選用的前綴最好是三字母的。
提供“全能初始化方法”
在類中提供一個全能初始化方法,並於文檔裡指明。其它初始化方法均應調用此方法。
若全能初始化方法與超類不同,則需覆寫超類中對應方法。
如果超類的初始化方法並不適用於子類,那麼應該覆寫這個超類方法,並在其中拋出異常。
實現description方法
實現description方法返回一個有意義的字符串,用以描述該實例。
若想在調試時打印出更詳盡的對象描述信息,則應該實現debugDescription方法。
盡量使用不可變對象
盡量創建不可變的對象。
若某屬性僅可於對象內部修改,則在“class-continuation分類”中將其由readonly屬性擴展為readwrite屬性。
不要把可變的collection作為屬性公開,而應提供相關方法,一次修改對象中的可變collection。
使用清晰而協調的命名方式
起名時應遵從標准的Objective-C命名規范,這樣創建出來的接口更容易為開發者所理解。
方法名要言簡意赅,從左至右讀起來要像個日常用語中的句子才好。
方法名利不要使用縮略後的類型名稱。
給方法嗎起名時的第一要務就是確保其風格與你自己的代碼或所要集成的框架相符。
為私有方法名加前綴
給私有方法的名稱加上前綴,這樣可以很容易的將其通公共方法區分開。
不要單用一個下劃線做私有方法的前綴,因為這種做法的預留給蘋果公司用的。
理解Objective-C錯誤模型
只有發生了可使整個應用程序崩潰的嚴重錯誤時,才使用異常。
在錯誤不那麼嚴重的情況下,可以指派委托方法來處理錯誤,也可把錯誤信息放在NSError對象裡,經由輸出參數返回給調用者。
理解NSCopying協議
若想令自己所寫的對象具有拷貝功能,則需實現NSCopying協議。
如果自定義的對象分為可變版本與不可變版本,那麼就要同時實現NSCopying與NSMutableCopying協議。
復制對象時需決定采用淺拷貝還是深拷貝,一般情況下應該盡量執行淺拷貝。
如果你所寫的對象需要深拷貝,那麼可考慮新增一個專門執行深拷貝的方法。
四、協議與分類
通過委托與數據源協議進行對象間通信
委托模式為對象提供了一套接口,使其可由此將相關事件告知其他對象。
將委托對象應該支持的接口定義成協議,在協議中把可能需要吃力的事件定義成方法。
當某對象需要從另外一個對象中獲取數據時,可使用委托模式。在這種情況下,該模式亦稱數據源協議。
若有必要,可實現含有位段的結構體,將委托對象是否能響應相關協議方法這一信息緩存至其中。
將類的實現代碼分散到便於管理的數個分類之中
使用分類機制把類的實現代碼劃分成易於管理的小塊。
將應該視為私有的方法歸入名叫Private的分類中,以隱藏實現細節。
總是為第三方類的分類名稱加前綴
向第三方類中添加分類時,總應給其名稱加上你專用的前綴。
向第三方類中添加分類時,總應給其中的方法名加上你專用的前綴。
勿在分類中聲明屬性
把封裝數據所用的全部屬性都定義在主接口裡。
在class-continuation分類之外的其他分類中,可以定義存取方法,但盡量不要定義屬性。
使用class-continuation分類隱藏實現細節
通過class-continuation分類向類中新增實例變量。
如果某屬性在主接口中聲明為只讀,而類的內部又要用設置方法修改此屬性,那麼就在class-continuation分類中將其擴展為可讀寫。
把私有方法的原型聲明在class-continuation分類裡面。
若想使類遵循的協議不為人所知,則可於class-continuation分類中聲明。
通過協議提供匿名對象
協議可在某種程度上提供匿名類型。具體的對象類型可以淡化成遵從某些一的id類型,協議裡規定了對象所應實現的方法。
使用匿名對象來隱藏類型名稱或類名。
如果具體類型不重要,重要的是對象能夠響應(定義在協議裡的)特定方法,那麼可使用匿名對象來表示。
五、內存管理
理解引用計數
引用計數機制通過可以遞增遞減的計數器來管理內存。對象創建好後,其保留計數至少為1.若保留計數為正,則對象繼續存活。當保留計數降為0時,對象就被銷毀了。
在對象生命期中,其余對象通過引用來保留或釋放此對象。保留與釋放操作分別會遞增及遞減保留計數。
以ARC簡化引用計數
在ARC之後,程序員就無須擔心內存管理問題了。使用ARC來編程,可省去類中的許多樣板代碼。
ARC管理對象生命期的辦法基本上就是:在合適的地方插入保留及釋放操作。在ARC環境下,變量的內存管理語義總是通過方法名來體現。ARC將此確定為開發者必須遵守的規則。
ARC只負責管理Objective-C對象的內存。尤其要注意:CoreFoundation對象不歸ARC管理,開發者必須適時調用CFRetain/CFRelease。
在dealloc方法中只釋放引用並解除監聽
在dealloc方法裡,應該做的事情就是釋放指向其它對象的引用,並取消原來訂閱的鍵值觀測或NSNotificationCenter等通知,不要做其他事情。
如果對象持有文件描述符等系統資源,那麼應該專門編寫一個方法來釋放此種資源。這樣的類要和其使用者約定“用完資源後必須調用close方法。
執行異步任務的方法不應在dealloc裡調用;只能在正常狀態下執行的那些方法也不應在dealloc裡調用,因為此時對象已處於正在回收的狀態了。
編寫異常安全代碼時留意內存管理問題
捕獲異常時,一定要注意將try塊內所創立的對象清理干淨。
在默認情況下,ARC不生成安全處理異常所需的清理代碼。開啟編譯器標志後,可生成這種代碼,不過會導致應用程序變大,而且會降低運行效率。
以弱引用避免重復引用
將某些引用設為weak,可避免出現重復引用。
weak引用可以自動清空,也可以不自動清空。自動清空是隨著ARC而引入的新特性,由runtime來實現,在具備自動清空功能的弱引用上,可以隨意讀取其數據,因為這種引用不會指向已經回收過的對象。
以自動釋放池塊降低內存峰值
自動釋放池排布在棧中,對象收到autorelease消息後,系統將其放入最頂端的池裡。
合理運用自動釋放池,可降低應用程序的內存峰值。
@autoreleasepool這種新式寫法能創建出更為輕便的自動釋放池。
用“僵屍對象”調試內存管理問題
系統在回收對象時,可以不將其真的回收,而是把它轉化為僵屍對象。通過環境變量NSZombieEnabled可開啟此功能。
系統會修改對象的isa指針,令其指向特殊的僵屍類,從而使該對象變為僵屍對象。僵屍類能夠響應所有的selector響應方式為:打印一條包含消息內容及其接受者的消息,然後終止應用程序。
不要使用retainCount
對象的保留計數看似有用,實則不然,因為任何給定時間點上的絕對保留計數都無法反映對象生命期的全貌。
引入ARC後,retainCount方法就正式廢止了,在ARC下調用該方法會導致編譯器報錯。
六、block與GCD
理解block的概念
block是C、C++、Objective-C中的詞法閉包。
block可接收參數,也可返回值。
block可以分配在棧或堆上,也可以是全局的。分配在棧上的block可拷貝到堆裡,這樣的話,就和標准的Objective-C對象一樣,具備引用計數了。
為常用的block類型創建typedef
以typedef重新定義block類型,可以令block變量用起來更加簡單。
定義新類型時應遵循現有的命名習慣,勿使其名稱與別的的類型相沖突。
不妨為同一個block簽名定義多個類型別名。如果要重構的代碼使用了block類型的某個別名,那麼只需修改相應的typedef中的block簽名即可,無需改動其他typedef。
用handler塊降低代碼分散程度
在創建對象時,使用內聯的handler塊將相關業務邏輯一並聲明。
在有多個實例需要監控時,如果采用委托模式,那麼經常需要根據傳入的對象來切換,而若改用handler塊來實現,則可直接將block與相關對象放在一起。
設計API時如果用到了handler塊,那麼可以增加一個參數,使調用者可通過此參數來決定應該把block安排在哪個隊列上執行。
用block引用其所屬對象時不要出現循環引用
如果block所捕獲的對象直接或間接的保留了block本身,那麼就得當心循環引用的問題。
一定要找個適當的時機解除循環引用,而不能把責任推給API的調用者。
多用派發隊列,少用同步鎖
派發隊列可用來表述同步語義,這種做法要比使用@synchronized塊或NSLock對象更簡單。
將同步與異步派發結合起來,可以實現與普通加鎖機制一樣的同步行為,而這麼做卻不會阻塞執行異步派發的線程。
使用同步隊列及柵欄塊,可以領同步行為更加高效。
多用GCD,少用performSelector系列方法
performSelector系列方法在內存管理方面容易有疏失。它無法確定將要執行的selector具體是什麼,因而ARC編譯器就無法插入適當的內存管理方法。
performSelector系列方法所能處理的selector太過局限了,selector的返回值類型及發送給方法的參數個數都受到限制。
如果想把人物放在另一個線程上執行,那麼最好不要用performSelector系列方法而是應該把任務封裝到block裡然後調用GCD機制的相關方法來實現。
掌握GCD及操作隊列的使用時機
在解決多線程與任務管理問題時,派發隊列並非唯一方案。
操作隊列提供了一套高層的Objective-C API,能實現純GCD所具備的絕大部分功能,而且還能完成一些更為復雜的操作,那些操作若改用GCD來實現,則需另外編寫代碼。
通過Dispatch Group機制根據系統資源狀況來執行任務
一系列任務可貴如一個dispatch group之中。開發者可以在這組任務執行完畢時獲得通知。
通過dispatch group,可以在並發式派發隊列裡同時執行多項任務。此時GCD會根據系統資源狀況來調度這些並發執行的任務。開發者若自己來實現此功能,則需編寫大量代碼。
使用dispatch_once來執行只需運行一次的線程安全代碼
經常需要編寫只需執行一次的線程安全代碼。通過GCD所提供的dispatch_once函數,很容易就能實現此功能。
標記應該聲明在static或global作用域中,這樣的話,在把只需執行一次的block傳給dispatch_once函數時,傳進去的標記也是相同的。
不要使用dispatch_get_current_queue
dispatch_get_current_queue函數的行為常常與開發者所預期的不同。此函數已廢棄,只應做調試之用。
由於派發隊列是按層級來組織的,所以無法單用某個隊列對象來描述當前隊列這一概念。
dispatch_get_current_queue函數用於解決由不可重入的代碼所引發的死鎖,然而能用此函數解決的問題,通常也能改用“隊列特定數據”來解決。
七、系統框架
熟悉系統框架
許多系統框架都可以直接使用。其中最重要的是Foundation與CoreFoundation,這兩個框架提供了構建應用程序所需的許多核心功能。
很多常見任務都能用框架來做,例如音頻與視頻處理、網絡通信、數據管理等。
請記住,用純C寫成的框架與用Objective-C寫成的一樣重要,若想成為優秀的Objective-C開發者,應該掌握C語言的核心概念。
多用塊枚舉,少用for循環
遍歷collection有4種方式。最基本的辦法是for循環,其次是NSEnumerator遍歷方法及快速遍歷方法,最新、最先進的方式則是塊枚舉法。
塊枚舉法本身就能通過GCD來並發執行遍歷操作,無需另行編寫代碼。而采用其他遍歷方式則無法輕易實現這一點。
若提前知道待遍歷的collection含有何種對象,則應修改塊簽名,指出對象的具體類型。
隊自定義其內存管理語義的collection使用無縫橋接
通過無縫橋接技術,可以在Foundation框架中的Objective-C對象與CoreFoundation框架中的C語言數據結構之前來回轉換。
在CoreFoundation層面創建collection時,可以指定許多回調函數,這些函數表示此collection應如何處理其元素。然後,可運用無縫橋接技術,將其轉換成具備特殊內存管理語義的Objective-C collection。
構建緩存是選用NSCachae而非NSDictionary
實現緩存時應選用NSCache而非NSDictionary對象。因為NSCache可以提供優雅的自動刪減功能,而且是線程安全的,此外,它與字典不同,並不會拷貝鍵。
可以給NSCache對象設置上限,用以限制緩存中的對象總個數及總成本,而這些尺度則定義了緩存刪減其中對象的時機。但是絕對不要把這些尺度當成可靠的硬限制,他們僅對NSCache起指導作用。
將NSPurgeableData與NSCache搭配使用,可實現自動清除數據的功能,也就是說,當NSPurgeableData對象所占內存為系統丟棄時,該對象自身也會從緩存中移除。
如果緩存使用得當。那麼應用程序的響應速度就能提高。只有那種重新計算起來很費事的數據,才值得放入緩存,比如那些需要從網絡獲取或從磁盤讀取的數據。
精簡initialize與load的實現代碼
在加載階段,如果實現了load方法,那麼系統就會調用它。分類裡也可以定義此方法,類load方法要比分類中的先調用。與其他方法不同,load方法不參與覆寫機制。
首次使用某個類之前,系統會向其發送initialize消息。由於此方法遵從普通的復寫規則,所以通常應該在裡面判斷當前要初始化的是哪個類。
load與initialize方法都應該實現的精簡一些,這有助於保持應用程序的響應能力,也能減少引入依賴環的幾率。
無法在編譯期設定的全局常量,可以放在initialize方法裡初始化。
別忘了NSTimer會保留其目標對象
NSTimer對象會保留其目標,直到計時器本身失效為止,調用invalidate方法可令計時器失效,另外,一次性的計時器在觸發完任務之後也會失效。
反復執行任務的計時器,很容易引入循環引用,進入過這種計時器的目標對象又保留了計時器本身,那肯定會導致循環引用。這種循環引用,可能是直接發生的,也可能是通過對象圖裡的其他對象間接發生的。
可以擴充NSTimer的功能,用塊來打破循環引用。不過,除非NSTimer將來在公共接口裡提供此功能,否則必須創建分類,將相關實現代碼加入其中。