編寫高質(zhì)量iOS與OS X代碼的52個有效方法
第二遍看這本書,想邊看邊做一些記錄,記錄本書的一些重要知識點(分章節(jié))
1.熟悉Objective-C
1.1了解OC起源
運行期組件本質(zhì)上就是一種與開發(fā)者所編代碼相鏈接的動態(tài)庫,其代碼能把開發(fā)者編寫的所有程序粘合起來,因此只需要更新運行期組件,即可提升應用程序性能。
所有oc對象都必須要通過指針的形式聲明,因為對象所占的內(nèi)存總是分配在堆空間中,不能在棧上分配oc對象。
有些不帶*的變量,比如CGRect,因為其中保存的不是oc對象,而是一些如int、float、double、char等非對象類型,使用結(jié)構(gòu)體就可以了,因為創(chuàng)建對象需要額外的開銷(比如分配及釋放堆內(nèi)存)
1.2在類的頭文件里盡量少引入其他頭文件
.h文件中通過@class 類名,來向前聲明一個類(通常出現(xiàn)在一個類的屬性里有一個另一個類,這時候不需要知道這個類的實現(xiàn)細節(jié),只需要知道有這個類就行) 在實現(xiàn)的.m文件中再去引入
將引入頭文件的時機盡量延后,可以減少類的使用者所需引入的頭文件數(shù)量,可以降低類的耦合,減少編譯時間。
1.3多用字面量語法
使用@來使使代碼更加簡潔清晰,使用字面量語法創(chuàng)建出來的對象都是不可變的,要想可變可以mutableCopy一份
1.4多用類型常量,少用#define預處理指令
常量有類型,有助于編寫文檔,并且編譯器可以檢查是否進行了修改,若常量局限于文件之內(nèi),則在前面加k,若在類外可見,則以類名為前綴。
常量的位置,預處理的不適合加在頭文件中,因為其它引入這份文件的可能也會出現(xiàn)這個名字,容易引起沖突。
類型常量也是,如果僅限于類內(nèi),就不應該放在頭文件里,放在頭文件里會變成一個全局變量。
在頭文件中使用extern來聲明全局常量,這種常量會出現(xiàn)在全局符號表中。
用static const來定義,只在編譯單元可見(static的作用)的常量(const),不用static的話會導致多個編譯單元會聲明同名變量,會有錯誤信息。
1.5用枚舉表示狀態(tài)、選項、狀態(tài)碼
實現(xiàn)枚舉所用的數(shù)據(jù)類型取決于編譯器,其二進制位要能完全表示下枚舉編號,比如1個字節(jié)的char能表示256種枚舉變量。
枚舉變量系統(tǒng)會自動從0開始編號,如果你手動將第一個編成1,那么就會從1開始。
在定義選項的時候,若某些選項可以彼此組合,可以通過按位或操作符 | 來組合,其對應的枚舉定義變量可以使用1 <<0 ,1 << 1, 1 << 2這種方式來定義,就可以判斷出哪個禁用哪個開啟了。
在switch中判斷枚舉時,最好不要加default,這樣的話如果以后加了新狀態(tài)編譯器會有提示。
2 對象、消息、運行期
2.6 理解屬性這一概念
OC中把實例變量當作一種存儲偏移量的特殊變量,會在運行期查找,所以甚至可以在運行期向類中新增實例變量,稱為穩(wěn)固的ABI,可以將某些變量從接口的public段中移走,保護其實現(xiàn)的內(nèi)部信息。
屬性特質(zhì):原子性、讀寫權(quán)限、內(nèi)存管理語義、方法名(具體不展開)
2.7 在對象內(nèi)部盡量直接訪問實例變量
在寫入實例變量時,通過設置方法來做,讀取的時候直接訪問,這樣即可以提高讀取操作的速度,又能控制對屬性的寫入操作。通過設置方法來做可以保證其“內(nèi)存管理語義”能夠得以貫徹。
在初始化方法中應該直接訪問實例變量,否則子類可能會覆寫設置方法
惰性初始化:一個屬性的對象相當復雜,創(chuàng)建的成本很高,并且不常用,需要在獲取方法中進行惰性初始化,如果沒用獲取方法直接訪問的話,是不行的。
2.8理解對象等同性
==操作符比較的是兩個指針本身,而不是其指的對象
NSObject里有一個isEqual來判斷兩個對象的等同性。
有些類自帶了判斷等同性的方法,比如NSString,有一個isEqualToString,這種方法會比單純的isEqual快因為后者要判斷類型等等。
兩個對象的hash值一樣不一定相等,如果是同一個類的不同對象,則hashcode相同一定相同。和hash函數(shù)有關系
2.9以類族模式隱藏實現(xiàn)細節(jié)
比如UIButton,創(chuàng)建按鈕的時候,要調(diào)用一個類方法,然后這個類方法里根據(jù)不同的按鈕類型進行繪制,繪制方法放在子類里,一個基類,然后很多子類,是一種工廠模式。但是oc不能指定某個基類是抽象的,一般基類接口會沒有init方法。
2.10在既有類中使用關聯(lián)對象存放自定義數(shù)據(jù)
可以通過關聯(lián)對象機制來把兩個對象連起來,定義關聯(lián)對象時可指定內(nèi)存管理語義,只有在其他方法不行的時候才考慮關聯(lián)對象,因為這種做法容易引入bug
2.11理解 objc_msgSend
C語言是靜態(tài)綁定,在編譯期就能決定運行時所應調(diào)用的函數(shù)。
接收者 選擇子 參數(shù) 選擇子和參數(shù)合起來稱為消息。
objc_msgSend會將匹配結(jié)果緩存到快速映射表里(fast map),每個類都有一塊這樣的緩存
這只是部分消息的調(diào)用,還有些 objc_msgSend_stret _fpret Super等 針對不同的消息(返回結(jié)構(gòu)體 返回浮點數(shù) 給超類發(fā)消息)
每個類里都有一張表格,其中的指針都會指向函數(shù),選擇子的名稱是查表所用的鍵
2.12 理解消息轉(zhuǎn)發(fā)機制
消息轉(zhuǎn)發(fā)兩大階段:第一階段先征詢接收者,所屬的類,看其是否能動態(tài)添加方法,以處理當前這個未知的選擇子,這叫做動態(tài)方法解析,第二階段涉及完整的消息轉(zhuǎn)發(fā)機制。
動態(tài)方法解析:返回bool值,表示這個類能否新增一個實例方法用以處理此選擇子。包括resolveInstanceMethod和resolveClassMethod
備援接收者:能不能把這條消息轉(zhuǎn)發(fā)給其它接收者處理 forwardingTargetForSelector
完整的消息轉(zhuǎn)發(fā):創(chuàng)建NSInvocation對象,把尚未處理的消息的全部細節(jié)裝進去,包含選擇子,目標,參數(shù) forwordInvocation
2.13 用方法調(diào)配技術(shù)調(diào)試黑盒方法
方法調(diào)配 黑魔法 (method swizzling) 交換方法實現(xiàn)
通常用來為黑盒方法添加日志記錄功能,利于程序調(diào)試。只需要新編寫一個方法,然后在此方法中實現(xiàn)所需的附加功能,并調(diào)用原有實現(xiàn)。
2.14 理解類對象的用意
每個oc對象實例都是指向某塊內(nèi)存數(shù)據(jù)的指針,每個對象結(jié)構(gòu)體的首個成員是class類的變量,定義了對象所屬的類,isa指針
Class本身也是oc對象,其中存放類的元數(shù)據(jù),例如類的實例實現(xiàn)了幾個方法,具備多少個實例變量等信息。
類對象的isa指向另一個類叫元類,其中存放著類方法
isMemberOfClass能夠判斷出對象是否為某個特定類的實例,isKindOfClass能夠判斷對象是否為某類或者其派生類的實例
類對象是單例,每個類的Class只有一個實例。
對于代理對象,調(diào)用class的時候,會返回這個代理對象本身(NSProxy的子類),如果調(diào)用isKindOfClass的查詢方法,那么這條消息會被轉(zhuǎn)發(fā)給接受代理的對象,兩者結(jié)果不同
3.接口與API設計
3.15 用前綴避免命名空間沖突
3.16 提供全能初始化方法
有多個不同初始化方法的,將其中一個設為全能初始化方法,其它方法都調(diào)用他,這樣以后要更改會比較方便
3.17 實現(xiàn)description方法
“%@”,object 該對象會收到description消息。自定義類實現(xiàn)這個方法返回其描述
還有個debugdescription,用來在調(diào)試的時候輸出更詳盡的信息
3.18 盡量使用不可變對象
如果要內(nèi)部修改,可以在分類中擴展為readwrite屬性,可變的collection也不應該作為屬性公開,而應該提供相應的方法,來修改,通常提供一個readonly屬性供外界使用,該屬性返回一個不可變的內(nèi)部可變set的拷貝。
3.19 使用清晰而協(xié)調(diào)的命名方式
方法與變量名:駝峰式大小寫 首字母小寫
類名:首字母大寫
繼承子類時注意要和父類協(xié)調(diào)
3.20 為私有方法名加前綴
比如使用p_XXX命名私有方法,用于區(qū)分
3.21 理解oc錯誤模型
ARC默認不是異常安全的,拋出異常那么不會自動釋放,可以通過設置編譯器標志來實現(xiàn),-fobjc-arc-exception
現(xiàn)在使用的方法是,只在極其罕見的情況下才拋出異常,并且拋出異常程序會直接退出,也就無須考慮恢復問題,就不需要額外的異常安全代碼了。
對于其他錯誤,oc會讓方法返回nil/0或是使用NSError,表明其中有錯誤發(fā)生。
NSError中封裝了三條信息:
domain 錯誤范圍 XXXErrorDomain
code 錯誤碼 自己定義的話最好定義為枚舉類型
User info 用戶信息 錯誤鏈
常見方法是通過委托協(xié)議來傳遞錯誤,錯誤發(fā)生時,當前對象會把經(jīng)由協(xié)議中的某個方法傳給其委托對象。
也可以使用輸出參數(shù)的方法把NSError對象回傳給調(diào)用者
3.22 理解NSCopying協(xié)議
該協(xié)議只有一個方法,copyWithZone,因為以前內(nèi)存是分成不同的區(qū)的,對象會在某個區(qū)里,現(xiàn)在只有一個區(qū),默認區(qū)
深淺拷貝區(qū)別
4.協(xié)議與分類
4.23 通過委托與數(shù)據(jù)源協(xié)議進行對象間通信
有些是必須實現(xiàn)的,有些是可選的@optional 可選方法 內(nèi)部通過respondsToSelector來判斷委托對象是否實現(xiàn)了相關的方法,如果頻繁檢測的話,只有第一次的結(jié)果是有用的,因此可以把這個結(jié)果緩存起來,優(yōu)化效率。
緩存的方式:使用了帶有位段的結(jié)構(gòu)體,用于存放能否響應的信息
4.24 將類的實現(xiàn)代碼分散到便于管理的數(shù)個分類之中
便于調(diào)試,在調(diào)試器的回溯信息中很容易能夠定位方法所在的功能區(qū)。
把私有方法放入private分類中,隱藏其實現(xiàn)細節(jié)。
4.25 總是為第三方類的分類名稱加前綴
多個分類會覆蓋,以最后一個為準,所以通過給分類加前綴的方式,減少沖突(給第三方類添加分類的時候)
4.26 勿在分類中聲明屬性
分類中的屬性是不能自動生成存取方法和實例變量,雖然可以通過關聯(lián)對象的方式在分類中合成實例變量。但是這樣在內(nèi)存管理上容易出錯(實現(xiàn)存取方法的時候沒有遵循內(nèi)存管理語義)。
屬性只是定義實例變量及存取方法所用的語法糖。
4.27 使用“class-continuation分類”隱藏實現(xiàn)細節(jié)
class-continuation是唯一能聲明實例變量的分類,其分類沒有名字,沒有特定的實現(xiàn)文件,其中的方法都應該定義在類的主實現(xiàn)文件中。
放在其中可以隱藏起來只供本類使用。
.mm文件 通過objc++編譯,在其頭文件里可以引入c++的頭文件,通過class-continuation的方式,可以將其放入分類中,使得其頭文件里沒有C++,這樣就可以使引入的人不再需要用objc++編譯,一些系統(tǒng)庫都是這么做的,比如WebKit,CoreAnimation,其中有很多后端代碼都是C++編寫的,但對外公布的是一套純objc接口。
class-continuation還可以用來將public中聲明為只讀的屬性擴展為可讀寫(重新聲明一遍),這樣既能令外界無法修改對象,又能在內(nèi)部按照需要修改。
還可以在該分類中聲明遵循的私有協(xié)議
4.28 通過協(xié)議提供匿名對象
匿名類 id 用于協(xié)議聲明受委托者的屬性時,那么任何類都能充當這一屬性。
數(shù)據(jù)庫等中用于隱藏類名。
有些時候?qū)ο箢愋筒恢匾?,也可以用id,表名其類型不重要
5 內(nèi)存管理
5.29 理解引用計數(shù)
autorelease:待稍后清理自動釋放池時再遞減其引用計數(shù)(通常是在下一次事件循環(huán)時遞減)
MRC中,在解除分配之后,只是放回可用內(nèi)存池,如果釋放之后繼續(xù)對其進行訪問,而此時該內(nèi)存沒有被覆寫,那么還是有效的。
所以一般release之后都會清空指針
在一些方法返回的地方,直接release那么返回不了,用autorelease,可以讓對象稍后釋放,延長對象生命期,能夠傳回調(diào)用者
5.30 以ARC簡化引用計數(shù)
ARC會自動調(diào)用保留與釋放,還會簡化能夠互相抵消的retain、release、autorelease
ARC只負責管理oc對象的內(nèi)存,CF對象要開發(fā)者自己管理
5.31 在dealloc方法中只釋放引用并解除監(jiān)聽
不要做其它的事情,因為此時對象正在回收的狀態(tài)
5.32 編寫異常安全代碼時留意內(nèi)存管理問題
try catch中容易忘記釋放內(nèi)存 可以加入finally然后釋放 (MRC)
ARC中比較復雜,因為不能調(diào)用release,所以需要開啟一個編譯器的標志,默認關閉,但會降低運行效率。在objc++時的時候默認開啟,因為C++頻繁使用異常
5.33 以弱引用避免保留環(huán)
MAC OS X的objc程序可以選擇啟用垃圾收集器,能夠檢測保留環(huán),但從10.8開始就被廢棄了
使用weak使用
5.34 以自動釋放池塊降低內(nèi)存峰值
@autoreleasepool:最外圍捕捉
可以用來控制內(nèi)存峰值
5.35 用僵尸對象調(diào)試內(nèi)存管理問題
NSZombieEnabled環(huán)境變量設為YES,之后所有已經(jīng)回收的實例就會轉(zhuǎn)化為特殊的僵尸對象,而不會真正回收,僵尸對象收到消息之后就會拋出異常,其中準確說明了發(fā)送過來的消息和回收之前的對象,是調(diào)試內(nèi)存管理問題的最佳方式。
系統(tǒng)在即將回收對象時會檢測這個環(huán)境變量,看是否轉(zhuǎn)化為僵尸對象。(isa指針指向?qū)擃惖慕┦悾?br>
NSZombie類和NSobject一樣都是以一個根類
5.36 不要使用retainCount
只是某個時間點上的值,并為考慮自動釋放池稍后清空的影響。單例對象的保留計數(shù)都會很大,系統(tǒng)會盡量把NSString實現(xiàn)成單例對象。
引入ARC之后retainCount徹底廢止,使用會報錯
6 塊與大中樞派發(fā)
6.37 理解塊這一概念
在它聲明的范圍內(nèi),所有變量都可以為其捕獲,聲明變量的時候可以加上__block,那么值可以在塊內(nèi)修改。
塊也是對象,也有引用計數(shù)
塊內(nèi)部結(jié)構(gòu) isa,flag,reserved,invoke,descriptor,還有捕獲到的變量 invoke指向?qū)崿F(xiàn)代碼,descriptor指向結(jié)構(gòu)體,其中聲明了塊對象的總體大小,輔助指針
6.38 為常用的塊類型創(chuàng)建typedef
typedef int(^EOCBlock)(BOOL flag,int value);
※6.39 用handler塊降低代碼分散程度
異步方法在執(zhí)行完任務之后,需要以某種手段通知相關代碼,一種是通過委托協(xié)議的方式,另一種是使用塊,把completion handler定義為塊,可以把業(yè)務邏輯和對象放在一起
6.40 用塊引用其所屬對象時不要出現(xiàn)保留環(huán)
6.41 多用派發(fā)隊列,少用同步鎖
用GCD會更加簡單,將同步與異步派發(fā)結(jié)合起來可以實現(xiàn)與普通加鎖機制一樣的同步行為,又不會阻塞執(zhí)行異步派發(fā)的線程。
使用同步隊列加柵欄塊,可以使同步行為更加高效。
※6.42 多用GCD,少用performSelector方法
performSelector在編譯期不知道要執(zhí)行的選擇子是什么的情況下,ARC下編譯代碼會不添加釋放操作,可能會引起內(nèi)存泄露。
如果想把任務放到另一個線程上執(zhí)行,最好用GCD而不用performSelector(可以選擇執(zhí)行的線程或者延遲執(zhí)行 這些GCD都可以實現(xiàn))
6.43 掌握GCD及操作隊列的使用時機
操作隊列即NSOperationQueue,能實現(xiàn)一些更加復雜的操作
6.44 通過Dispatch Group機制,根據(jù)系統(tǒng)資源狀況來執(zhí)行任務
6.45 使用Dispatch Once來執(zhí)行只需運行一次的線程安全代碼
6.46 不要使用dispatch_get_currnt_queue
ios 6之后已經(jīng)啟用了 容易死鎖
7 系統(tǒng)框架
7.47 熟悉系統(tǒng)框架
Cocoa,ios上Cocoa Touch,本身并不是框架,但是里面集成了一批創(chuàng)建應用程序時經(jīng)常會用到的框架。
Foundation框架中的類使用NS前綴,還有CoreFoundation,CFNetwork,CoreData.....
7.48 多用塊枚舉,少用for循環(huán)
for循環(huán)
OC1.0 NSEnumerator 遍歷
OC2.0 快速遍歷
最新的塊遍歷 enumerateObjectUsingBlock,既能獲取對象又能知道下標,還能終止遍歷
7.49 對自定義其內(nèi)存管理語義的collection使用無縫橋接
__bridge transfer和retain 用來在Foundation中的oc對象與CoreFoundation中的C語言數(shù)據(jù)結(jié)構(gòu)中來回轉(zhuǎn)換
7.50 構(gòu)建緩存時選用NSCache而非NSDictionary
NSCache在資源將要耗盡時會自動刪減緩存
NSCache不會拷貝鍵,而很多時候鍵都是由不能拷貝的對象充當?shù)?br>
NSCache是線程安全的
NSPurgeableData與NSCache搭配使用,如果某NSPurgeableData對象為系統(tǒng)丟棄時,也會自動從緩存中移除
7.51 精簡initialize與load的實現(xiàn)代碼
load:只會調(diào)用一次,問題在于,執(zhí)行該方法時,運行期系統(tǒng)處于脆弱狀態(tài),在load中使用其他類是不安全的,因為不知道那個類是否已經(jīng)加載好了
而且某個類的load如果沒實現(xiàn),不管其超類有沒有實現(xiàn),都不會調(diào)用,如果分類里也出現(xiàn)load,那么兩個都會調(diào)用,類的先調(diào)用分類后調(diào)用
initialize:惰性調(diào)用,用到了這個類才調(diào)用,為什么要精簡?
initialize的時候一定要線程安全,因此其它線程會阻塞,所以其要精簡,減少阻塞;開發(fā)者無法控制類的初始化時機;如果用到了其它類,就會迫使其初始化,而那個類可能用到了本類的數(shù)據(jù),而本類此時沒有初始化完畢。
7.52 別忘了NSTimer會保留其目標對象
使用invalidate讓計時器失效來防止保留環(huán),可以通過塊來解決(定義一個弱引用然后塊去捕獲他)