優(yōu)化App的持久化策略

轉(zhuǎn)載自'小專(zhuān)欄'RyRYanZhong'

這個(gè) session 覆蓋了 app 儲(chǔ)存文件的方方面面, 對(duì)于經(jīng)常需要寫(xiě)入沙盒的 app來(lái)說(shuō), 提供了很多好的 guideline, 以及底層原理的分析.

使用HEIC格式圖片

蘋(píng)果建議我們本地的圖片切換成使用 HEIC 格式這種更高效的圖片格式, HEIC格式本身比jpeg小50%, 因此下載和上傳都更快, 在磁盤(pán)中存取也更快, 同時(shí)也支持透明度和無(wú)損壓縮, 在單個(gè) HEIC 圖片容器中可支持儲(chǔ)存多個(gè)圖片.

將圖片放入asset catalog

asset catalog 原生支持了 app slicing, 可以根據(jù)下載機(jī)型的不同, slice 出對(duì)應(yīng)的圖片放入安裝包中, 而不是直接將2x, 3x等倍圖全部打入安裝包, 可以有效減少包體積. 另外image的加載也會(huì)更快, 尤其是啟動(dòng)時(shí), apple 聲稱(chēng)可以比不使用 asset catalog 的情況提升10%的圖片加載速度, 所以使用 heic 加 asset catalog 組合, 會(huì)有奇效

文檔數(shù)據(jù)元數(shù)據(jù) File system metadata

image

檔 metadata 的數(shù)據(jù)寫(xiě)入經(jīng)常會(huì)發(fā)生, 而且 IO 開(kāi)銷(xiāo)是很大的, 例如你的 app 中有一個(gè) plist 文件記錄上一次的啟動(dòng)時(shí)間, 每次啟動(dòng) app, 都讀取該 plist 獲取上次啟動(dòng)時(shí)間, 然后寫(xiě)入當(dāng)前時(shí)間這個(gè)簡(jiǎn)單的操作會(huì)發(fā)生一次讀取 IO, 三次寫(xiě)入 IO, 還有一次 fsync() 的操作, 并且以下行為都會(huì)造成 File system metadata 寫(xiě)入

  • 創(chuàng)建文件
  • 刪除文件
  • 重命名文件
  • 更新文件

而 File system metadata 包括以下元素

  • 文件名
  • 大小
  • 地理位置
  • 修改時(shí)間
  • 等等....

例如當(dāng)我們寫(xiě)一個(gè)240byte的 NSDictionary 到文件時(shí), 首先是 update file system tree


image

基于寫(xiě)時(shí)復(fù)制 (copy on write)策略, 不會(huì)馬上更新 file system tree 的結(jié)點(diǎn), 而是創(chuàng)建一個(gè)結(jié)點(diǎn)的拷貝


image

每一次操作, 都會(huì)有自己的 transaction id, 這個(gè)寫(xiě)操作就會(huì)生成一個(gè)新的結(jié)點(diǎn), ID也會(huì)被更新, 一個(gè)簡(jiǎn)單的寫(xiě)入240byte的數(shù)據(jù)進(jìn)入disk的時(shí)候, 會(huì)同步導(dǎo)致以下數(shù)據(jù)的更新. 包括:
更新file system node (4k),
更新object map (4k),
metadata總大小: 8k
文件本身: 本身是240byte, 但ios的寫(xiě)入文件最小單位是4k
所以總共是12k
因此每次更新數(shù)據(jù)到 disk 里都是有代價(jià)的, 如果我們只是需要?jiǎng)?chuàng)建一些臨時(shí)數(shù)據(jù), 例如字典, 數(shù)組這類(lèi)數(shù)據(jù), 建議不要把這些數(shù)據(jù)直接寫(xiě)入到磁盤(pán)中, 直接在內(nèi)存中使用并銷(xiāo)毀就可以了, 如果這類(lèi)原始數(shù)據(jù)有持久化的需求, 應(yīng)該通過(guò)一個(gè)中間類(lèi)來(lái)統(tǒng)一管理內(nèi)存寫(xiě)入到 disk 的邏輯, 盡量減少你的app需要的文件數(shù)量

syncing to disk

OS cache: 性能最好的一層, 使用 logical I/O, 由于是儲(chǔ)存在內(nèi)存中, 所以 I/O操作很高效 (使用logical I/O)
Disk cache: 磁盤(pán)儲(chǔ)存的物理映射. (使用 physical I/O)
permanent storage: 最終用于持久化數(shù)據(jù)的介質(zhì), 對(duì)于iOS來(lái)說(shuō), 就是閃存 (使用 physical I/O)
緩存有以上幾個(gè)層級(jí), 對(duì)于 app 來(lái)說(shuō), 離 cpu 越近的 cache, 性能就越好, 但同時(shí)我們也希望cache能確實(shí)地落在磁盤(pán)中. 數(shù)據(jù)在內(nèi)存當(dāng)中時(shí)對(duì)于app而言速度是最快的, 也沒(méi)有任何的 IO 開(kāi)銷(xiāo), 但是當(dāng)我們需要將數(shù)據(jù)從內(nèi)存一層一層地注入到閃存時(shí), 就需要注意 IO 開(kāi)銷(xiāo)了.

下面介紹幾個(gè)將數(shù)據(jù)從 OS cache 層逐步 flush 到 Permanent Storage 的函數(shù)


image

fsync()

該函數(shù)用于將數(shù)據(jù)從 OS cache 層寫(xiě)入到 Disk cache 層, 但數(shù)據(jù)可能不是立即寫(xiě)到 permanent storage 層, 如果沒(méi)有代碼的明確指令, 實(shí)際上是由設(shè)備的固件決定數(shù)據(jù)什么時(shí)候從 disk cache 進(jìn)入 permanent cache, 并且寫(xiě)入的順序是沒(méi)有保障的, 從OS cache寫(xiě)入 Disk cache 的順序并不決定從 disk cache 進(jìn)入 permanent cache的順序 因此fsync()的過(guò)度使用是昂貴的, 這個(gè)函數(shù)是會(huì)直接導(dǎo)致 IO 的發(fā)生, 其實(shí)我們沒(méi)有必要手動(dòng)顯式地調(diào)用這個(gè)函數(shù), 因?yàn)?OS 本身會(huì)周期性的調(diào)用fsync()來(lái)寫(xiě)入數(shù)據(jù), 因?yàn)榇蠖鄶?shù)情況下沒(méi)有必要手動(dòng)觸發(fā)fsync()

FULLSYNC

該函數(shù)用于將數(shù)據(jù)從 OS cache 層寫(xiě)入到 permanent cache 層, 并且會(huì)觸發(fā)所有已經(jīng)存在于 disk cache 上的數(shù)據(jù)寫(xiě)入到 permanen cache, 并且OS本身會(huì)周期性的調(diào)用該函數(shù), 因此理由同上, 大多數(shù)情況下沒(méi)有必要手動(dòng)觸發(fā)

文件序列化格式的選擇

開(kāi)發(fā)者一般會(huì)使用 Plist, XML, JSON 這三個(gè)常見(jiàn)的格式, 這些都是常見(jiàn)的數(shù)據(jù)格式, 便于使用而且普適性高, 也易于解析, 適合不是頻繁讀寫(xiě)的數(shù)據(jù), 但是每次改動(dòng)都是全量的讀寫(xiě), 導(dǎo)致整個(gè)文件讀取和重新寫(xiě)入, 就會(huì)引起上面所說(shuō)的從OS cache層到 permanent cache 層的IO操作, 即使你寫(xiě)入一個(gè)很小的數(shù)據(jù), 由于文件本身攜帶的 meta data 操作, 也可能會(huì)產(chǎn)生數(shù)據(jù)量是寫(xiě)入data本身幾倍大的IO開(kāi)銷(xiāo)

舉個(gè)例子

以下是 file activity instruments 監(jiān)控我們創(chuàng)建, 讀取, 和更新上述這個(gè) plist 時(shí), 引起了12個(gè)獨(dú)立的 IO 操作


image

面是單單的更新plist操作, 調(diào)用了系統(tǒng)的writeToFile函數(shù), 最后再調(diào)用棧上系統(tǒng)為我們調(diào)用了fsync, 所以數(shù)據(jù)就會(huì)直接由OS cache層一直寫(xiě)入到 Disk cache 層, 并從 OS cache 層被清除, 如果在寫(xiě)入后我們?nèi)匀灰^續(xù)使用數(shù)據(jù), 就會(huì)失去了OS cache這一層的緩存, 而需要重新開(kāi)啟IO去磁盤(pán)中讀取數(shù)據(jù)
因此使用plist這類(lèi)文件來(lái)儲(chǔ)存需要頻繁讀寫(xiě)的數(shù)據(jù), 是非常不合適的

image

Core Data

由蘋(píng)果推出的 Core Data 其底層其實(shí)是基于sqlite實(shí)現(xiàn)的, 也是蘋(píng)果推薦開(kāi)發(fā)者使用的數(shù)據(jù)緩存系統(tǒng), 因?yàn)樗梢怨芾韺?duì)象關(guān)系, 創(chuàng)建關(guān)系型的數(shù)據(jù)庫(kù), 可以注冊(cè)屬性觀察與通知, 自動(dòng)版本檢測(cè), 自動(dòng)解決寫(xiě)沖突, 并且在內(nèi)部集成了 iCloudKit (iOS 13或以后)

sqlite

關(guān)于直接使用sqlite, 蘋(píng)果特別提出了關(guān)閉與開(kāi)啟連接的開(kāi)銷(xiāo), 每次開(kāi)啟和關(guān)閉DB的連接, 都會(huì)觸發(fā)sqlite的一致性檢測(cè), 日志恢復(fù), 日志標(biāo)志位設(shè)置等等操作, 因此apple建議不要過(guò)多的開(kāi)啟和關(guān)閉連接, 而是在app的生命周期里, 開(kāi)發(fā)者盡可能的保持連接一直開(kāi)啟, 例如可以建立一個(gè)獨(dú)立的子線(xiàn)程來(lái)保持與DB的連接, 然后全局通過(guò)那個(gè)子線(xiàn)程去操作DB

日志

關(guān)于日志, 開(kāi)發(fā)者平時(shí)可能對(duì)于 sqlite 日志的 mode 沒(méi)有過(guò)多的關(guān)注, 但其實(shí)日志 mode 的不同對(duì)性能同樣有很大的影響, Delete Mode是sqlite的默認(rèn)日志mode, 但WAL Mode是更推薦的日志mode, 首先是因?yàn)楦俚膶?xiě)操作, 這個(gè)日志模式會(huì)自動(dòng)組合多個(gè)寫(xiě)操作到同一頁(yè), 同時(shí)也使用更少的 barrier, 支持多個(gè)讀操作與寫(xiě)操作并發(fā), 并且支持?jǐn)?shù)據(jù)快照, 例如我們要寫(xiě)入4頁(yè)的DB, WAL Mode并不會(huì)分別寫(xiě)在這4個(gè)頁(yè)中, 而且統(tǒng)一寫(xiě)在Write Ahead log file中

事務(wù) Transaction

在多個(gè) INSERT, UPDATE, DELETE 操作時(shí), 建議使用 Transaction, 可有效減低 IO 次數(shù)


image

例如我們有3個(gè) Transaction, 修改 DB 上同一頁(yè)的數(shù)據(jù), 這會(huì)造成 DB 上同一頁(yè)的數(shù)據(jù)被修改3次


image

但如果將這3個(gè)操作放在一個(gè)Transaction里, 寫(xiě)操作就會(huì)被合并成一次

File Size and Privacy

當(dāng)我們從 DB 中刪除數(shù)據(jù)時(shí), 在 DB 中儲(chǔ)存該數(shù)據(jù)的空間會(huì)被設(shè)置為可用, 但被刪除的數(shù)據(jù)是實(shí)際上有可能仍然在磁盤(pán)上直至有新數(shù)據(jù)寫(xiě)入, 如果是涉及安全和敏感的數(shù)據(jù), 可以使用
PRAGMA schema.auto_vacuume=INCREMENTAL這種刪除模式, 該模式會(huì)總動(dòng)清理被刪除的數(shù)據(jù), 并且在iOS13 是默認(rèn)模式

不要使用 VACUUM

VACUUM 是性能比較差的清掃空閑頁(yè)的方式, 例如我們要 VACUUM 下面這個(gè)包含6個(gè)空閑頁(yè)(灰
色)的 DB, 這時(shí)會(huì)先將所有有效數(shù)據(jù)寫(xiě)到 journal 里


image

然后清空DB


image

最后再將 journal 的數(shù)據(jù)重新全部 insert 到 DB


image

最后再刪除 journal
所有數(shù)據(jù)都最少執(zhí)行了兩次寫(xiě)操作, 由于數(shù)據(jù)被 copy 了一份, 也會(huì)占用更多的內(nèi)存

因此建議使用 PRAGMA schema.auto_vacuume=INCREMENTAL, 原理如下
例如我們要清理下面兩個(gè)空閑頁(yè)

image

write ahead log會(huì)預(yù)先記錄末尾兩個(gè)準(zhǔn)備被移動(dòng)的頁(yè), 以及他們的父結(jié)點(diǎn)


image

然后將末尾的頁(yè)數(shù)據(jù)更新到空閑頁(yè)上


image

比起直接使用 VACUUM 的全量刪除和寫(xiě)入, 這個(gè)模式只更新了需要被清理的空閑頁(yè)的數(shù)據(jù), 明顯更高效

關(guān)于sqlite部分的總結(jié)

首先是保持 DB 連接的開(kāi)啟, 尤其是需要頻繁讀寫(xiě) DB 的應(yīng)用, 如果現(xiàn)在的設(shè)計(jì)模式是每次讀寫(xiě)都做成獨(dú)立的一次連接開(kāi)啟與關(guān)閉, 將會(huì)造成不必要的額外開(kāi)銷(xiāo)
在日志模式上使用 WAL mode, 會(huì)自動(dòng)幫你合并日志操作, 一次性執(zhí)行多個(gè) statement 時(shí)優(yōu)先考慮合并成一個(gè) transaction, 并且在需要清理DB的空閑數(shù)據(jù)時(shí), 使用 auto vacuum incremental. 以上都是讓你的sqlite db能更高效運(yùn)行的方法

總結(jié)

這次 wwdc 總結(jié)了儲(chǔ)存策略的方方面面, 磁盤(pán) IO 的開(kāi)銷(xiāo)可能是國(guó)內(nèi)開(kāi)發(fā)者很少注意到的一個(gè)點(diǎn), 估計(jì)將一些小數(shù)據(jù)作為文件存在磁盤(pán)然后運(yùn)行過(guò)程不斷讀寫(xiě)的項(xiàng)目不在少數(shù), 一個(gè)簡(jiǎn)單的writeToFiles的api就會(huì)導(dǎo)致多層的 cache IO 操作, IO 不但導(dǎo)致發(fā)熱和耗電, 而且發(fā)生在主線(xiàn)程也會(huì)造成卡頓. 如果你的項(xiàng)目暫時(shí)沒(méi)做過(guò)這方面的優(yōu)化, 用 instrument 做一下debug可能會(huì)發(fā)現(xiàn)不少"驚喜".

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末仙畦,一起剝皮案震驚了整個(gè)濱河市桃煎,隨后出現(xiàn)的幾起案子洪燥,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡搂鲫,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)磺平,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)魂仍,“玉大人,你說(shuō)我怎么就攤上這事拣挪∏O郑” “怎么了掰邢?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我草姻,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮介袜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘出吹。我一直安慰自己遇伞,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布捶牢。 她就那樣靜靜地躺著鸠珠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪秋麸。 梳的紋絲不亂的頭發(fā)上渐排,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音灸蟆,去河邊找鬼驯耻。 笑死,一個(gè)胖子當(dāng)著我的面吹牛炒考,可吹牛的內(nèi)容都是我干的可缚。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼斋枢,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼帘靡!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起瓤帚,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤描姚,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后缘滥,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡谒主,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年朝扼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片霎肯。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡擎颖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出观游,到底是詐尸還是另有隱情搂捧,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布懂缕,位于F島的核電站允跑,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜聋丝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一索烹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧弱睦,春花似錦百姓、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至火惊,卻和暖如春求类,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背矗晃。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工仑嗅, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人张症。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓仓技,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親俗他。 傳聞我的和親對(duì)象是個(gè)殘疾皇子脖捻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345