IOS Cache設(shè)計(jì)詳細(xì)介紹及簡單示例
IOS Cache設(shè)計(jì)
Cache的設(shè)計(jì)是個(gè)基礎(chǔ)計(jì)算機(jī)理論,也是程序員的重要基本功之一。Cache幾乎無處不在,CPU的L1 L2 Cache,iOS系統(tǒng)的clean page和dirty page機(jī)制,HTTP的tag機(jī)制等,這些背后都是Cache設(shè)計(jì)思想的應(yīng)用。
為什么需要Cache
Cache的目的是為了追求更高的速度體驗(yàn),Cache的源頭是兩種數(shù)據(jù)讀取方式在成本和性能上的差異。
在開始著手設(shè)計(jì)Cache之前,需要先理清數(shù)據(jù)存儲(chǔ)的媒介。作為客戶端開發(fā)人員來說,我們所關(guān)注的數(shù)據(jù)存儲(chǔ)方式也有不少種:
- 數(shù)據(jù)最開始是存儲(chǔ)在Server上,這些數(shù)據(jù)需要通過網(wǎng)絡(luò)請求獲取。
- 從Server獲取數(shù)據(jù)時(shí),會(huì)經(jīng)過各種中間網(wǎng)絡(luò)節(jié)點(diǎn)(比如代理),這些節(jié)點(diǎn)有時(shí)會(huì)緩存我們的數(shù)據(jù)。
- 把數(shù)據(jù)下載到本地之后,我們會(huì)在本地disk緩存一份,這樣或許不用每次都重新去服務(wù)器請求。
- 存到disk之后,數(shù)據(jù)的存儲(chǔ)方式會(huì)影響到讀取的速度,以B+ Tree存儲(chǔ)的sqlite就比直接序列化NSArray到文件之中要快不少。
- App啟動(dòng)時(shí),系統(tǒng)會(huì)將從Server下載到的數(shù)據(jù),從disk加載到memory,memory的讀寫性能比disk要快很多。
- 到了Memory中,不同的數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)方式也會(huì)存在速度上的差異。用NSDictionary(hash表)形式存儲(chǔ)讀數(shù)據(jù),寫性能都比Array好,但space開銷更大。雖說memory的讀寫性能比disk都高了很多,但在大集合類數(shù)據(jù)操作的時(shí)候有時(shí)也會(huì)遇到瓶頸。
- 比Memory更快的還有Register,L1,L2,只不過對(duì)于iOS App開發(fā)來說,很少深入到這一層面的優(yōu)化。
上面所說的每一個(gè)環(huán)節(jié),都存在性能和成本上的差別,Server的數(shù)據(jù)自然是最及時(shí)最準(zhǔn)確的,但一個(gè)App要以NSArray的形式獲取到Server的數(shù)據(jù),中間要經(jīng)過「漫長」的過程,可以說每一步中都存在cache的設(shè)計(jì)思想。
對(duì)于Cache的理解和實(shí)踐,前提是我們對(duì)于存儲(chǔ)媒介,和不同數(shù)據(jù)結(jié)構(gòu)差異,有比較深入的掌握。
我們大部分App的性能優(yōu)化,如果涉及到Cache,一般都是在Memory這一媒介上做處理。將需要從Disk中,或者通過CPU復(fù)雜計(jì)算才能獲取的數(shù)據(jù),通過合理的數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)在Memory中,就能解決我們App開發(fā)里,絕大部分的Cache需求了。這一層面的Cache設(shè)計(jì)也有著不同的姿勢,先來看看簡單可用型。
簡單可用型Cache
得益于Foundation中NSDictionary的封裝,我們可以用hash表這種數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)一個(gè)簡單可用的cache機(jī)制,先來看一個(gè)實(shí)例:
- (NSString*)getFormmatedPhoneNumber:(NSNumber*)phone { if(phone == nil) { return nil; } return [PhoneFormatLib formatPhoneNumber:phone]; //CPU費(fèi)時(shí)操作 }
這是個(gè)簡單的格式化手機(jī)號(hào)碼的函數(shù),其中 formatPhoneNumber 函數(shù)是個(gè)CPU Intensive的調(diào)用,而且在業(yè)務(wù)場景中針對(duì)同一個(gè)手機(jī)號(hào)碼,需要經(jīng)常性的獲取格式化之后的NSString,如果每次都重復(fù)計(jì)算顯然是對(duì)CPU資源的浪費(fèi),而且性能也不好。我們可以加個(gè)簡單的Cache來優(yōu)化:
static NSMutableDictionary* gPhoneCache = nil; - (NSString*)getFormmatedPhoneNumber:(NSNumber*)phone { if(phone == nil) { return nil; } NSString* phoneNumberStr = nil; [_phoneLock lock]; if(gPhoneCache == nil) { gPhoneCache = @{}.mutableCopy; } phoneNumberStr = [gPhoneCache objectForKey:phone]; if (phoneNumberStr == nil) { phoneNumberStr = [PhoneFormatLib formatPhoneNumber:phone]; [gPhoneCache setObject:phoneNumberStr forKey:phone]; } [_phoneLock unlock]; return phoneNumberStr; }
通過引入NSMutableDictionary,就避免了每次都需要重復(fù)調(diào)用 formatPhoneNumber 的問題,so easy就完成了一個(gè)快速的cache設(shè)計(jì),馬上就可以提交給測試,把優(yōu)化成果甩產(chǎn)品經(jīng)理臉上,這歸功于hash表O(1)的時(shí)間復(fù)雜度。內(nèi)存空間會(huì)多消耗一些,不過對(duì)于小量的數(shù)據(jù)影響比較小,現(xiàn)代的hash表不會(huì)一開始就分配大量的空間,而是隨著數(shù)據(jù)的增加而逐漸擴(kuò)容。
這種簡單可用型的Cache設(shè)計(jì),最大的問題在于,代碼過于零散且不可控。小量且分散的cache設(shè)計(jì)幾乎等同于挖坑,在你設(shè)計(jì)cache的時(shí)候可能數(shù)據(jù)量還小,但后面維護(hù)的時(shí)候,業(yè)務(wù)改變的時(shí)候,誰也不能保證這塊內(nèi)存的開銷依然可以忽略不計(jì)。而且這種內(nèi)存方面的損耗很難察覺,巧妙的隱蔽在某個(gè).m文件中,到后期想控制整個(gè)App的內(nèi)存開銷時(shí),會(huì)感覺到處都有坑,無從下手。你可能也發(fā)現(xiàn)了,上面這段Cache代碼沒有釋放Cache的地方。
所有對(duì)我們整個(gè)App有副作用的代碼都需要被集中管理,要能從架構(gòu)的層面去理解和定位。怎么去定義副作用呢?可以抽象成一種「寫操作」,往Cache中添加新的記錄就是寫操作,這種寫操作的副作用是額外的內(nèi)存開銷,Cache的本質(zhì)是以空間換時(shí)間,這空間損耗就是我們的副作用,一個(gè)副作用會(huì)引發(fā)其他更多的副作用,理清這些副作用往往需要反復(fù)查閱大量的代碼。更好的辦法是,一開始就把有副作用的代碼集中管理。
優(yōu)雅可控型Cache
避免Cache代碼散亂放置的做法是,設(shè)計(jì)一個(gè)優(yōu)雅可控的Cache模塊。一個(gè)App中,可能會(huì)有各種各樣的數(shù)據(jù)需要Cache,phoneNumberCache,avatarCache,spaceshipCache等等,我們需要有個(gè)源頭來追蹤這些cache,直觀的做法是通過工廠類來生成和持有這些各式各樣的cache:
//CacheFactory.h @interface CacheFactory : NSObject + (instancetype)sharedInstance; - (id<MyCacheProtocol>)getPhoneNumberCache; - (void)clearPhoneNumberCache; - (id<MyCacheProtocol>)getAvatarCache; - (void)clearAvatarCache; @end
這樣當(dāng)我們需要評(píng)估各種Cache對(duì)整個(gè)App內(nèi)存開銷的影響之時(shí),只需要從CacheFactory代碼著手即可,調(diào)試起來也有跡可循,其他工程師接手你的代碼也會(huì)感激涕零的。
通過protocol的方式,將cache的聲明和實(shí)現(xiàn)想分離,這也是個(gè)好習(xí)慣。cache的另一個(gè)重要知識(shí)點(diǎn)是cache的淘汰策略,不同的策略表現(xiàn)也不一樣,F(xiàn)IFO,LRU,2Queues等等,現(xiàn)在有不少成熟的第三方cache框架可以使用,系統(tǒng)也提供了淘汰策略不明確的NSCache,如果沒有動(dòng)手寫過任何cache淘汰策略,我還是建議大家自己動(dòng)手試著做一個(gè),至少要讀一下相關(guān)的實(shí)現(xiàn)源碼,了解這些淘汰策略很有必要,在做一些深度優(yōu)化的時(shí)候需要因地制宜來做決定。
cache的使用要有收有放,不能只創(chuàng)建不釋放,事實(shí)上,所有涉及到data的操作都要考慮data的生命周期。我們做業(yè)務(wù)的時(shí)候,多是以Controller為基礎(chǔ)單位,有些場景下,一個(gè)Controller在退出之后被再次進(jìn)入的可能性就非常之低了,適時(shí)的清理cache會(huì)讓我們App的整體表現(xiàn)更好。
Immutable Cache
Cache中存放的是啥?是Data。說到Data,就不得不提peak君最愛啰嗦的”Immutability(不可變性)”了,Immutability和我們代碼的穩(wěn)定性有著極大的關(guān)系,大到就像「房間里的大象」,很重要也容易被忽視。
在實(shí)踐Immutability的時(shí)候,需要先將Data做分類,再去區(qū)分每一種類型Data如何去實(shí)施不可變性。做Data分類最重要的是分清楚值類型和引用類型的差別。傳值的時(shí)候傳遞的是新的內(nèi)存拷貝,所以值類型大多是安全的,傳指針的時(shí)候傳遞的是同一塊共享內(nèi)存空間,這也是指針之所以危險(xiǎn)的一大原因。bool,Int,long等等這些primitive type都是值類型,可以放心的傳遞,而對(duì)象類型往往是以指針的形式在傳遞,需要特別的注意,我們一般通過copy的方式(生成新的內(nèi)存拷貝)來傳遞。這也是為什么Swift中將很多原先在Objective C中基礎(chǔ)類變?yōu)橹殿愋偷脑?,?qiáng)化Immutability,讓我們的代碼更加安全。
我們看下不同類型的數(shù)據(jù)在Cache中的讀寫操作。
值類型-讀
值類型可以安心返回:
- (int)spaceshipCount { //... return _shipCount; }
值類型-寫
值類型也可以安全的寫:
- (void)setSpaceshipCount:(int)count { _shipCount = count; }
對(duì)象類型-讀
指針類型需要生成新拷貝:
- (User*)luckyUser { //... return [_luckyUser copy]; }
對(duì)象類的copy方法需要我們手動(dòng)實(shí)現(xiàn)NSCopying protocol,開發(fā)的初期雖然顯得繁瑣了些,但后期的回報(bào)很大。而且這里的copy必須是deep copy,User中的每一個(gè)被持有的property都需要遞歸copy。
對(duì)象類型-寫
對(duì)象類型寫操作的危險(xiǎn)之處在于函數(shù)的入?yún)?,入?yún)⒁彩菍?duì)象類型的話,傳入的是一個(gè)共享的引用:
- (void)setLuckyUser:(User*)user { //... _luckyUser = [user copy]; }
集合類型-讀
集合類也需要copy,是bug和crash的重災(zāi)區(qū):
- (NSArray*)hotDishes { //... return [_hotDishes copy]; }
集合類型-寫
- (void)setHotDishes:(NSArray*)dishes { //... _hotDishes = [dished copy]; }
看到這里,大家可能也發(fā)現(xiàn)了,其實(shí)原則也比較簡單,只要保證業(yè)務(wù)模塊從Cache中獲取的數(shù)據(jù)都是獨(dú)立的copy,就能避免數(shù)據(jù)共享帶來的各種隱患。Cache模塊有點(diǎn)類似函數(shù)式編程中的純函數(shù),既不依賴于外部的狀態(tài),也不會(huì)修改外部的狀態(tài),重點(diǎn)處理每一個(gè)函數(shù)調(diào)用的input(入?yún)ⅲ┖蚾utput(返回值)即可。
多線程安全
Cache多線程安全的重點(diǎn)在于對(duì)集合類的處理,Cache本身多數(shù)時(shí)候都是在管理數(shù)據(jù)的集合。需要特別注意的是NSString其實(shí)也應(yīng)該歸到集合類,從數(shù)據(jù)讀寫和多線程安全方面看,NSString和NSArray在很多方面表現(xiàn)都是一致的。一些成熟的第三方Cache庫已經(jīng)替我們處理好了多線程安全的問題,如果是自己造的輪子,尤其要注意保證讀寫都是原子操作,至于如何使用鎖,相關(guān)的文章分享已經(jīng)很多了,此處不做贅述了。
總結(jié)
了解Cache關(guān)鍵在于明白其背后的設(shè)計(jì)思想,進(jìn)而能對(duì)我們App的行為有更全面的掌握,能明白每一個(gè)業(yè)務(wù)流程背后對(duì)數(shù)據(jù)處理的瓶頸在哪。隨著代碼越寫越多,業(yè)務(wù)越來越復(fù)雜,今天或明天,我們總要遇到需要應(yīng)用Cache設(shè)計(jì)的時(shí)候。
感謝閱讀,希望能幫助到大家,謝謝大家對(duì)本站的支持!
- iOS中的NSURLCache數(shù)據(jù)緩存類用法解析
- Objective-C的緩存框架EGOCache在iOS App開發(fā)中的使用
- C++開發(fā)在IOS環(huán)境下運(yùn)行的LRUCache緩存功能
- 使用Javascript判斷瀏覽器終端設(shè)備(PC、IOS(iphone)、Android)
- iOS 條碼及二維碼掃描(從相冊中讀取條形碼/二維碼)及掃碼過程中遇到的坑
- iOS實(shí)現(xiàn)時(shí)間顯示幾分鐘前,幾小時(shí)前以及剛剛的方法示例
- IOS正則表達(dá)式判斷輸入類型(整理)
- iOS 仿百度外賣-首頁重力感應(yīng)的實(shí)例
- IOS 開發(fā)之應(yīng)用喚起實(shí)現(xiàn)原理詳解
- IOS TextFiled與TextView 鍵盤的收起以及處理鍵盤遮擋
- IOS與網(wǎng)頁JS交互詳解及實(shí)例
相關(guān)文章
UITextView實(shí)現(xiàn)只允許鏈接交互不允許選擇圖片的方法
這篇文章主要介紹了UITextView實(shí)現(xiàn)只允許鏈接交互不允許選擇圖片的方法,文中介紹的非常詳細(xì),相信對(duì)大家具有一定的參考價(jià)值,需要的朋友們下面來一起看看吧。2017-03-03iOS如何獲取屏幕寬高、設(shè)備型號(hào)、系統(tǒng)版本信息
這篇文章主要介紹了iOS如何獲取屏幕寬高、設(shè)備型號(hào)、系統(tǒng)版本信息的相關(guān)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11Xcode 下刪除Provisioning Profiles文件詳細(xì)介紹
這篇文章主要介紹了Xcode 下刪除Provisioning Profiles文件詳細(xì)介紹的相關(guān)資料,需要的朋友可以參考下2016-12-12iOS中TableView如何統(tǒng)一數(shù)據(jù)源代理詳解
這篇文章主要給大家介紹了關(guān)于iOS中TableView如何統(tǒng)一數(shù)據(jù)源代理的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07iOS開發(fā)總結(jié)之UILabel常用屬性介紹
下面小編就為大家分享一篇iOS開發(fā)總結(jié)之UILabel常用屬性介紹,具有很的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2017-12-12Objective-C方法的聲明實(shí)現(xiàn)及調(diào)用方法
這篇文章主要介紹了Objective-C方法的聲明實(shí)現(xiàn)及調(diào)用方法,包括五參數(shù)的方法和單個(gè)參數(shù)的方法,結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2024-02-02