iOS當(dāng)中的Cache設(shè)計

Cache的設(shè)計是個基礎(chǔ)計算機理論畔柔,也是程序員的重要基本功之一。Cache幾乎無處不在,CPU的L1 L2 Cache推姻,iOS系統(tǒng)的clean page和dirty page機制,HTTP的tag機制等框沟,這些背后都是Cache設(shè)計思想的應(yīng)用藏古。

為什么需要Cache

Cache的目的是為了追求更高的速度體驗,Cache的源頭是兩種數(shù)據(jù)讀取方式在成本和性能上的差異忍燥。

在開始著手設(shè)計Cache之前拧晕,需要先理清數(shù)據(jù)存儲的媒介。作為客戶端開發(fā)人員來說梅垄,我們所關(guān)注的數(shù)據(jù)存儲方式也有不少種:

  • 數(shù)據(jù)最開始是存儲在Server上防症,這些數(shù)據(jù)需要通過網(wǎng)絡(luò)請求獲取。
  • 從Server獲取數(shù)據(jù)時哎甲,會經(jīng)過各種中間網(wǎng)絡(luò)節(jié)點(比如代理)蔫敲,這些節(jié)點有時會緩存我們的數(shù)據(jù)。
  • 把數(shù)據(jù)下載到本地之后炭玫,我們會在本地disk緩存一份奈嘿,這樣或許不用每次都重新去服務(wù)器請求。
  • 存到disk之后吞加,數(shù)據(jù)的存儲方式會影響到讀取的速度裙犹,以B+ Tree存儲的sqlite就比直接序列化NSArray到文件之中要快不少。
  • App啟動時衔憨,系統(tǒng)會將從Server下載到的數(shù)據(jù)叶圃,從disk加載到memory,memory的讀寫性能比disk要快很多践图。
  • 到了Memory中掺冠,不同的數(shù)據(jù)結(jié)構(gòu)存儲方式也會存在速度上的差異。用NSDictionary(hash表)形式存儲讀數(shù)據(jù)码党,寫性能都比Array好德崭,但space開銷更大斥黑。雖說memory的讀寫性能比disk都高了很多,但在大集合類數(shù)據(jù)操作的時候有時也會遇到瓶頸眉厨。
  • 比Memory更快的還有Register锌奴,L1,L2憾股,只不過對于iOS App開發(fā)來說鹿蜀,很少深入到這一層面的優(yōu)化。

上面所說的每一個環(huán)節(jié)服球,都存在性能和成本上的差別茴恰,Server的數(shù)據(jù)自然是最及時最準(zhǔn)確的,但一個App要以NSArray的形式獲取到Server的數(shù)據(jù)有咨,中間要經(jīng)過「漫長」的過程琐簇,可以說每一步中都存在cache的設(shè)計思想。

對于Cache的理解和實踐座享,前提是我們對于存儲媒介婉商,和不同數(shù)據(jù)結(jié)構(gòu)差異,有比較深入的掌握渣叛。

我們大部分App的性能優(yōu)化丈秩,如果涉及到Cache,一般都是在Memory這一媒介上做處理淳衙。將需要從Disk中蘑秽,或者通過CPU復(fù)雜計算才能獲取的數(shù)據(jù),通過合理的數(shù)據(jù)結(jié)構(gòu)存儲在Memory中箫攀,就能解決我們App開發(fā)里肠牲,絕大部分的Cache需求了。這一層面的Cache設(shè)計也有著不同的姿勢靴跛,先來看看簡單可用型缀雳。

簡單可用型Cache

得益于Foundation中NSDictionary的封裝,我們可以用hash表這種數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)一個簡單可用的cache機制梢睛,先來看一個實例:

- (NSString*)getFormmatedPhoneNumber:(NSNumber*)phone
{
  if(phone == nil)
  {
    return nil;    
  }
  
  return [PhoneFormatLib formatPhoneNumber:phone]; //CPU費時操作
}

這是個簡單的格式化手機號碼的函數(shù)肥印,其中formatPhoneNumber函數(shù)是個CPU Intensive的調(diào)用翩概,而且在業(yè)務(wù)場景中針對同一個手機號碼坯认,需要經(jīng)常性的獲取格式化之后的NSString凿宾,如果每次都重復(fù)計算顯然是對CPU資源的浪費糙置,而且性能也不好。我們可以加個簡單的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就完成了一個快速的cache設(shè)計旁赊,馬上就可以提交給測試,把優(yōu)化成果甩產(chǎn)品經(jīng)理臉上竞膳,這歸功于hash表O(1)的時間復(fù)雜度航瞭。內(nèi)存空間會多消耗一些诫硕,不過對于小量的數(shù)據(jù)影響比較小坦辟,現(xiàn)代的hash表不會一開始就分配大量的空間,而是隨著數(shù)據(jù)的增加而逐漸擴容章办。

這種簡單可用型的Cache設(shè)計锉走,最大的問題在于,代碼過于零散且不可控藕届。小量且分散的cache設(shè)計幾乎等同于挖坑挪蹭,在你設(shè)計cache的時候可能數(shù)據(jù)量還小,但后面維護(hù)的時候休偶,業(yè)務(wù)改變的時候梁厉,誰也不能保證這塊內(nèi)存的開銷依然可以忽略不計。而且這種內(nèi)存方面的損耗很難察覺踏兜,巧妙的隱蔽在某個.m文件中词顾,到后期想控制整個App的內(nèi)存開銷時,會感覺到處都有坑碱妆,無從下手肉盹。你可能也發(fā)現(xiàn)了,上面這段Cache代碼沒有釋放Cache的地方疹尾。

所有對我們整個App有副作用的代碼都需要被集中管理上忍,要能從架構(gòu)的層面去理解和定位。怎么去定義副作用呢纳本?可以抽象成一種「寫操作」窍蓝,往Cache中添加新的記錄就是寫操作,這種寫操作的副作用是額外的內(nèi)存開銷繁成,Cache的本質(zhì)是以空間換時間吓笙,這空間損耗就是我們的副作用,一個副作用會引發(fā)其他更多的副作用朴艰,理清這些副作用往往需要反復(fù)查閱大量的代碼观蓄。更好的辦法是,一開始就把有副作用的代碼集中管理祠墅。

優(yōu)雅可控型Cache

避免Cache代碼散亂放置的做法是侮穿,設(shè)計一個優(yōu)雅可控的Cache模塊。一個App中毁嗦,可能會有各種各樣的數(shù)據(jù)需要Cache亲茅,phoneNumberCache,avatarCache,spaceshipCache等等克锣,我們需要有個源頭來追蹤這些cache茵肃,直觀的做法是通過工廠類來生成和持有這些各式各樣的cache:

//CacheFactory.h
@interface CacheFactory : NSObject
+ (instancetype)sharedInstance;
- (id<MyCacheProtocol>)getPhoneNumberCache;
- (void)clearPhoneNumberCache;
- (id<MyCacheProtocol>)getAvatarCache;
- (void)clearAvatarCache;
@end

這樣當(dāng)我們需要評估各種Cache對整個App內(nèi)存開銷的影響之時,只需要從CacheFactory代碼著手即可袭祟,調(diào)試起來也有跡可循验残,其他工程師接手你的代碼也會感激涕零的。

通過protocol的方式巾乳,將cache的聲明和實現(xiàn)想分離您没,這也是個好習(xí)慣。cache的另一個重要知識點是cache的淘汰策略胆绊,不同的策略表現(xiàn)也不一樣氨鹏,F(xiàn)IFO,LRU压状,2Queues等等仆抵,現(xiàn)在有不少成熟的第三方cache框架可以使用,系統(tǒng)也提供了淘汰策略不明確的NSCache种冬,如果沒有動手寫過任何cache淘汰策略镣丑,我還是建議大家自己動手試著做一個,至少要讀一下相關(guān)的實現(xiàn)源碼碌廓,了解這些淘汰策略很有必要传轰,在做一些深度優(yōu)化的時候需要因地制宜來做決定。

cache的使用要有收有放谷婆,不能只創(chuàng)建不釋放慨蛙,事實上,所有涉及到data的操作都要考慮data的生命周期纪挎。我們做業(yè)務(wù)的時候期贫,多是以Controller為基礎(chǔ)單位,有些場景下异袄,一個Controller在退出之后被再次進(jìn)入的可能性就非常之低了通砍,適時的清理cache會讓我們App的整體表現(xiàn)更好。

Immutable Cache

Cache中存放的是啥烤蜕?是Data封孙。說到Data,就不得不提peak君最愛啰嗦的"Immutability(不可變性)"了讽营,Immutability和我們代碼的穩(wěn)定性有著極大的關(guān)系虎忌,大到就像「房間里的大象」,很重要也容易被忽視橱鹏。

在實踐Immutability的時候膜蠢,需要先將Data做分類堪藐,再去區(qū)分每一種類型Data如何去實施不可變性。做Data分類最重要的是分清楚值類型和引用類型的差別挑围。傳值的時候傳遞的是新的內(nèi)存拷貝礁竞,所以值類型大多是安全的,傳指針的時候傳遞的是同一塊共享內(nèi)存空間杉辙,這也是指針之所以危險的一大原因模捂。bool,Int奏瞬,long等等這些primitive type都是值類型枫绅,可以放心的傳遞泉孩,而對象類型往往是以指針的形式在傳遞硼端,需要特別的注意,我們一般通過copy的方式(生成新的內(nèi)存拷貝)來傳遞寓搬。這也是為什么Swift中將很多原先在Objective C中基礎(chǔ)類變?yōu)橹殿愋偷脑蛘渥颍瑥娀疘mmutability,讓我們的代碼更加安全句喷。

我們看下不同類型的數(shù)據(jù)在Cache中的讀寫操作镣典。

值類型-讀

值類型可以安心返回:

- (int)spaceshipCount
{
    //...
    return _shipCount;
}

值類型-寫

值類型也可以安全的寫:

- (void)setSpaceshipCount:(int)count
{
    _shipCount = count;
}

對象類型-讀

指針類型需要生成新拷貝:

- (User*)luckyUser
{
    //...
    return [_luckyUser copy];   
}

對象類的copy方法需要我們手動實現(xiàn)NSCopying protocol,開發(fā)的初期雖然顯得繁瑣了些唾琼,但后期的回報很大兄春。而且這里的copy必須是deep copy,User中的每一個被持有的property都需要遞歸copy锡溯。

對象類型-寫

對象類型寫操作的危險之處在于函數(shù)的入?yún)⒏嫌撸雲(yún)⒁彩菍ο箢愋偷脑挘瑐魅氲氖且粋€共享的引用:

- (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)了,其實原則也比較簡單倡蝙,只要保證業(yè)務(wù)模塊從Cache中獲取的數(shù)據(jù)都是獨立的copy九串,就能避免數(shù)據(jù)共享帶來的各種隱患。Cache模塊有點類似函數(shù)式編程中的純函數(shù)寺鸥,既不依賴于外部的狀態(tài)猪钮,也不會修改外部的狀態(tài),重點處理每一個函數(shù)調(diào)用的input(入?yún)ⅲ┖蚾utput(返回值)即可胆建。

多線程安全

只要談到數(shù)據(jù)的處理烤低,就避免不了多線程安全的話題,可以看下我之前寫的幾篇關(guān)于多線程安全的文章:

iOS多線程到底不安全在哪里眼坏?

正確使用多線程同步鎖@synchronized()

如何用Xcode8解決多線程問題

Cache多線程安全的重點在于對集合類的處理拂玻,Cache本身多數(shù)時候都是在管理數(shù)據(jù)的集合酸些。需要特別注意的是NSString其實也應(yīng)該歸到集合類,從數(shù)據(jù)讀寫和多線程安全方面看檐蚜,NSString和NSArray在很多方面表現(xiàn)都是一致的魄懂。一些成熟的第三方Cache庫已經(jīng)替我們處理好了多線程安全的問題,如果是自己造的輪子闯第,尤其要注意保證讀寫都是原子操作市栗,至于如何使用鎖,相關(guān)的文章分享已經(jīng)很多了咳短,此處不做贅述了填帽。

總結(jié)

了解Cache關(guān)鍵在于明白其背后的設(shè)計思想,進(jìn)而能對我們App的行為有更全面的掌握咙好,能明白每一個業(yè)務(wù)流程背后對數(shù)據(jù)處理的瓶頸在哪篡腌。隨著代碼越寫越多,業(yè)務(wù)越來越復(fù)雜勾效,今天或明天嘹悼,我們總要遇到需要應(yīng)用Cache設(shè)計的時候。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末层宫,一起剝皮案震驚了整個濱河市杨伙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌萌腿,老刑警劉巖限匣,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異毁菱,居然都是意外死亡米死,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門鼎俘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來哲身,“玉大人,你說我怎么就攤上這事贸伐】碧欤” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵捉邢,是天一觀的道長脯丝。 經(jīng)常有香客問我,道長伏伐,這世上最難降的妖魔是什么宠进? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮藐翎,結(jié)果婚禮上材蹬,老公的妹妹穿的比我還像新娘实幕。我一直安慰自己,他們只是感情好堤器,可當(dāng)我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布昆庇。 她就那樣靜靜地躺著,像睡著了一般闸溃。 火紅的嫁衣襯著肌膚如雪整吆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天辉川,我揣著相機與錄音表蝙,去河邊找鬼。 笑死乓旗,一個胖子當(dāng)著我的面吹牛府蛇,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播寸齐,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼欲诺,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了渺鹦?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤蛹含,失蹤者是張志新(化名)和其女友劉穎毅厚,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體浦箱,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡吸耿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了酷窥。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片咽安。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蓬推,靈堂內(nèi)的尸體忽然破棺而出妆棒,到底是詐尸還是另有隱情,我是刑警寧澤沸伏,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布糕珊,位于F島的核電站,受9級特大地震影響毅糟,放射性物質(zhì)發(fā)生泄漏红选。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一姆另、第九天 我趴在偏房一處隱蔽的房頂上張望喇肋。 院中可真熱鬧坟乾,春花似錦、人聲如沸蝶防。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽慧脱。三九已至渺绒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間菱鸥,已是汗流浹背宗兼。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留氮采,地道東北人殷绍。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像鹊漠,于是被迫代替她去往敵國和親主到。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,754評論 25 707
  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結(jié)起來就是把...
    Dove_iOS閱讀 27,130評論 30 470
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理躯概,服務(wù)發(fā)現(xiàn)登钥,斷路器,智...
    卡卡羅2017閱讀 134,629評論 18 139
  • 以前做旅游產(chǎn)品的互聯(lián)網(wǎng)上的營銷娶靡,后來又做旅游產(chǎn)品牧牢,我也一直在思考,產(chǎn)品和營銷的平衡點在哪里姿锭?營銷先行塔鳍,產(chǎn)品滯后,會...
    婷子的媽閱讀 124評論 0 0
  • 需求分析: 1呻此、多個子網(wǎng)的單天H文件合轮纫。也就是同一天的H文件都排列到一起,并在文件地址 后面 加上一個符號 “+”...
    測繪小兵閱讀 275評論 1 1