導航:首頁 > 源碼編譯 > 源碼缺陷實例分析

源碼缺陷實例分析

發布時間:2024-06-15 06:17:39

『壹』 存儲性能優化 MMKV源碼解析

好久沒有更新常用的第三方庫了。讓我們來聊聊MMKV這個常用的第三方庫。MMKV這個庫是做什麼的呢?他本質上的定位和sp有點相似,經常用於持久化小數據的鍵值對。其速度可以說是當前所有同類型中速度最快,性能最優的庫。

它的最早的誕生,主要是因為在微信iOS端有一個重大的bug,一個特殊的文本可以導致微信的iOS端閃退,而且還出現了不止一次。為了統計這種閃退的字元出現頻率以及過濾,但是由於出現的次數,發現原來的鍵值對存儲組件NSUserDefaults根本達不到要求,會導致cell的滑動卡頓。

因此iOS端就開始創造一個高新性能的鍵值對存儲組件。於此同時,Android端SharedPreferences也有如下幾個缺點:

因此Android也開始復用iOS的MMKV,而後Android有了多進程的寫入數據的需求,Android組又在這個基礎上進行改進。

這里是官方的性能的比較圖:

能看到mmkv比起我們開發常用的組件要快上數百倍。

那麼本文將會從源碼角度圍繞MMKV的性能為什麼會如此高,以及SharePrefences為什麼可能出現ANR的原因。

請注意下文是以MMKV 1.1.1版本源碼為例子分析。如果遇到什麼問題歡迎來到本文 https://www.jianshu.com/p/c12290a9a3f7 互相討論。

老規矩,先來看看MMKV怎麼使用。mmkv其實和SharePrefences一樣,有增刪查改四種操作。

MMKV作為一個鍵值對存儲組件,也對了存儲對象的序列化方式進行了優化。常用的方式比如有json,Twitter的Serial。而MMKV使用的是Google開源的序列化方案:Protocol Buffers。

Protocol Buffers這個方案比起json來說就高級不少:

使用方式可以閱讀下面這篇文章: https://www.jianshu.com/p/e8712962f0e9

下面進行比較幾個對象序列化之間的要素比較

而MMKV就是看重了Protocol Buffers的時間開銷小,選擇Protocol Buffers進行對象緩存的核心。

使用前請初始化:

當然mmkv除了能夠寫入這些基本類型,只要SharePrefences支持的,它也一定能夠支持。

同上,每一個key讀取的數據類型就是decodexxx對應的類型名字。使用起來十分簡單。

能夠刪除單個key對應的value,也能刪除多個key分別對應的value。containsKey判斷mmkv的磁碟緩存中是否存在對應的key。

mmkv和SharePrefences一樣,還能根據模塊和業務劃分對應的緩存文件:

這里創建了一個id為a的實例在磁碟中,進行數據的緩存。

當需要多進程緩存的時候:

MMKV可以使用Ashmem的匿名內存進行更加快速的大對象傳輸:
進程1:

最重要的一點,mmkv把SharePrefences的緩存遷移到mmkv中,之後的使用就和SharePrefences一致。

這里就是把SharedPreferences的myData數據遷移到mmkv中。當然如果我們需要保持SharePreferences的用法不變需要自己進行自定義一個SharePreferences。

mmkv的用法極其簡單,接下來我們關注他的原理。

首先來看看MMKV的初始化。

能看到實際上initialize分為如下幾個步驟:

能看到其實就是做這個判斷。由於此時設置的是libc++的打包方式。此時BuildConfig.FLAVOR就是StaticCpp,就不會載入c++_shared。當然,如果我們已經使用了c++_shared庫,則沒有必要打包進去,使用defaultPublishConfig "SharedCppRelease"會嘗試的查找動態鏈接庫_shared。這樣就能少2M的大小。

請注意一個前提的知識,jni的初始化,在調用了 System.loadLibrary之後,會通過dlopen把so載入到內存後,調用dlsym,調用jni中的JNI_OnLoad方法。

實際上這裡面做的事情十分簡單:

能從這些native方法中看到了所有MMKV的存儲方法,設置支持共享內存ashemem的存儲,支持直接獲取native malloc申請的內存

接下來就是MMKV正式的初始化方法了。

這個方法實際上調用的是pthread_once方法。它一般是在多線程環境中,根據內核的調度策略,選擇一個線程初始化一次的方法。

其實這裡面的演算法很簡單:

defaultMMKV此時調用的是getDefaultMMKV這個native方法,默認是單進程模式。從這里的設計都能猜到getDefaultMMKV會從native層實例化一個MMKV對象,並且讓實例化好的java層MMKV對象持有。之後Java層的方法和native層的方法一一映射就能實現一個直接操作native對象的Java對象。

我們再來看看MMKV的mmkvWithID。

感覺上和defaultMMKV有點相似,也是調用native層方法進行初始化,並且讓java層MMKV對象持有native層。那麼我們可否認為這兩個實例化本質上在底層調用同一個方法,只是多了一個id設置呢?

可以看看MMKV.h文件:

這里就能看到上面的推測是正確的,只要是實例化,最後都是調用mmkvWithID進行實例化。默認的mmkv的id就是mmkv.default。Android端則會設置一個默認的page大小,假設4kb為例子。

所有的mmkvID以及對應的MMKV實例都會保存在之前實例化的g_instanceDic散列表中。其中mmkv每一個id對應一個文件的路徑,其中路徑是這么處理的:

如果發現對應路徑下的mmkv在散列表中已經緩存了,則直接返回。否則就會把相對路徑保存下來,傳遞給MMKV進行實例化,並保存在g_instanceDic散列表中。

我們來看看MMKV構造函數中幾個關鍵的欄位是怎麼初始化。

mmkvID就是經過md5後對應緩存文件對應的路徑。

能看到這里是根據當前的mode初始化id,如果不是ashmem匿名共享內存模式進行創建,則會和上面的處理類似。id就是經過md5後對應緩存文件對應的路徑。

注意這里mode設置的是MMKV_ASHMEM,也就是ashmem匿名共享內存模式則是如下創建方法:

實際上就是在驅動目錄下的一個內存文件地址。

接下來,在構造函數中使用了共享的文件鎖進行保護後,調用loadFromFile進一步的初始化MMKV內部的數據。

我們大致的了解MMKV中每一個欄位的負責的職責,但是具體如何進行工作下文都會解析。

在這裡面我們遇到了看起來十分核心的類MemoryFile,它的名字有點像 Ashmem匿名共享內存 一文中描述過Java層的映射的匿名內存文件。

我們先來看看MemoryFile的初始化。

MemeoryFile分為兩個模式進行初始化:

這里的處理很簡單:

能看到此時將會調用mmap系統調用,通過設置標志位可讀寫,MAP_SHARED的模式進行打開。這樣就file就在在內核中映射了一段4kb內存,以後訪問文件可以不經過內核,直接訪問file映射的這一段內存。

關於mmap系統調用的源碼解析可以看這一篇 Binder驅動的初始化 映射原理 。

能看到在這個過程中實際上還是通過ftruncate進行擴容,接著調用zeroFillFile,先通過lseek把指針移動當前容量的最後,並把剩餘的部分都填充空數據'\0'。最後映射指向的地址是有效的,會先解開後重新進行映射。

為什麼要做最後這個步驟呢?如果閱讀過我解析的mmap的源碼一文,實際上就能明白,file使用MAP_SHARED的模式本質上是給file結構體綁定一段vma映射好的內存。ftruncate只是給file結構體進行了擴容,但是還沒有對對應綁定虛擬內存進行擴容,因此需要解開一次映射後,重新mmap一次。

MMKV在如果使用Ashmem模式打開:

接下來loadFromFile 這個方法可以說是MMKV的核心方法,所有的讀寫,還是擴容都需要這個方法,從映射的文件內存,緩存到MMKV的內存中。

進入到這個方法後進行如下的處理:

在這里,遇到了一個比較有歧義的欄位m_version ,從名字看起來有點像MMKV的版本號。其實它指代的是MMKV當前的狀態,由一個枚舉對象代表:

注意m_vector是一個長度16的char數組。其實很簡單,就是把文件保存的m_vector獲取16位拷貝到m_metaInfo的m_vector中。因為aes的加密必須以16的倍數才能正常運作。

初始化分為這6點,我們從最後三點開始聊聊MMKV的初始化的核心邏輯。我們還需要開始關注MMKV中內存存儲的結構。

能看到首先從m_file獲取映射的指針地址,往後讀取4位數據。這4位數據就是actualSize 真實數據。但是如果是m_metaInfo的m_version 大於等於3,則獲取m_metaInfo中保存的actualSize。

其校驗的手段,是通過比較m_metaInfo保存的crcDigest和從m_file中讀取的crcDigest進行比較,如果一致說明數據無誤,則返回true,設置loadFromFile為true。

其實這裡面只處理m_metaInfo的m_version的狀態大於等於3的狀態。我們回憶一下,在readActualSize方法中,把讀取當前存儲的數據長度,分為兩個邏輯進行讀取。如果大於等於3,則從m_metaInfo中獲取。

crc校驗失敗,說明我們寫入的時候發生異常。需要強制進行recover恢復數據。
首先要清除crc校驗校驗了什麼東西:

MMKV做了如下處理,只處理狀態等級在MMKVVersionActualSize情況。這個情況,在m_metaInfo記錄上一次MMKV中的信息。因此可以通過m_metaInfo進行校驗已經存儲的數據長度,進而更新真實的已經記錄數據的長度。

最後讀取上一次MMKV還沒有更新的備份數據長度和crc校驗欄位,通過writeActualSize記錄在映射的內存中。

如果最後彌補的校驗還是crc校驗錯誤,最後會回調onMMKVCRCCheckFail這個方法。這個方法會反射Java層實現的異常處理策略

如果是OnErrorRecover,則設置loadFromFile和needFullWriteback都為true,盡可能的恢復數據。當然如果OnErrorDiscard,則會丟棄掉所有的數據。

『貳』 紼嬪簭璁捐$己闄峰垎鏋愪笌瀹炶返鍓嶈█

褰撴彁鍒扮▼搴忚捐★紝澶у氭暟浜哄瑰叾騫朵笉闄岀敓錛岃嚜璁や負綺鵑氭ら亾銆傜劧鑰岋紝闃呰繪湰涔﹀悗錛屼綘浼氬彂鐜幫紝鍗充究鏄鑷璁や負楂樻墜鐨勪綘錛岃捐$殑紼嬪簭涔熷彲鑳介殣鈃忕潃浼楀氭湭瀵熻夌殑緙洪櫡銆傝繖浜涚己闄峰線寰瀵艱嚧杞浠惰繍琛岄棶棰橀戝彂錛屽獎鍝嶇敤鎴蜂綋楠岋紝鐢氳嚦鍗卞強鐢熷懡璐浜у畨鍏錛岃繖鏄杞浠跺紑鍙戦嗗煙闀挎湡闈涓寸殑鎸戞垬銆

鑷杞浠跺嵄鏈轟互鏉ワ紝杞浠跺伐紼嬪笀浠涓鐩村湪瀵繪眰鎻愬崌杞浠惰川閲忕殑絳栫暐錛屾彁鍑轟簡涓緋誨垪瑙e喅鏂規堛傜劧鑰岋紝鐢變簬緙洪櫡鐨勫嶆潅鎬у拰闅愯棌鎬э紝璁稿氱湅浼兼e父鐨勪唬鐮佷腑鍙鑳介殣鈃忕潃娼滃湪鐨勯庨櫓銆傛湰涔︿互紼嬪簭璁捐$己闄蜂負鏍稿績錛屼緷鎵樹綔鑰呬赴瀵岀殑杞浠舵祴璇曠粡楠岋紝鏁寸悊浜嗘祴璇曚腑鎻紺虹殑椴滀負浜虹煡鐨勯棶棰橈紝鏃ㄥ湪甯鍔╄昏呮繁鍏ョ悊瑙e苟閬垮厤榪欑被闂棰樸

鏈涔﹀垎涓轟簲涓絝犺妭錛屽唴瀹硅﹀敖銆傜涓絝犵潃閲嶄簬闈欐佹祴璇曪紝娑電洊浜嗘枃妗e℃煡銆佷唬鐮佸℃煡銆佹妧鏈璇勫″拰浠g爜璧版煡絳夋柟娉曪紝騫跺歸儴鍒嗘柟娉曡繘琛屼簡瀵規瘮銆傜浜屻佷笁絝犳槸鏍稿績鍐呭癸紝璇︾粏瑙f瀽浜咰/C++鍜孞ava璇璦紼嬪簭鍦ㄧ紪鐮侀庢牸銆佸唴瀛樼$悊銆佺紦鍐插尯浣跨敤銆佹寚閽堝拰浠g爜瀹夊叏絳夋柟闈㈢殑甯歌侀棶棰橈紝騫墮氳繃瀹炰緥鍒嗘瀽鍜屾彁渚涗慨鏀瑰緩璁銆

絎鍥涚珷鎺㈣ㄤ簡杞浠惰川閲忕殑闈欐佸害閲忥紝浠嬬粛浜嗗寘鎷琀alstead搴﹂噺鍜孧cCabe搴﹂噺鍦ㄥ唴鐨勪簲縐嶈蔣浠惰川閲忔ā鍨嬨傜浜旂珷鍒欑粨鍚堝疄闄呮祴璇曞疄璺碉紝璁茶В浜哖olyspace銆並lockwork銆乀estbed鍜孧cCabe浠g爜闈欐佸垎鏋愬伐鍏風殑浣跨敤鏂規硶錛屼負瀹炶返鑰呮彁渚涗簡鍏蜂綋宸ュ叿鐨勮繍鐢ㄦ寚鍗椼

闃呰繪湰涔︼紝鏃犺烘槸杞浠舵祴璇曚漢鍛樿繕鏄紼嬪簭璁捐¤咃紝閮藉皢浠庝腑鍙楃泭鑹澶氥傚湪姝わ紝鐗瑰埆鎰熻阿寮犳棴杈夈佹潹璞廣佹潕鍗庤幑銆佽儭鍏㈢帀銆佽懀鏄曘佷簬闀塊捄鍚屽織鍦ㄧ紪鍐欒繃紼嬩腑鐨勮礎鐚錛屼粬浠鐨勪粯鍑轟負璇昏呮彁渚涗簡瀹濊吹鐨勬寚瀵箋

閱讀全文

與源碼缺陷實例分析相關的資料

熱點內容
怎麼用c語言編譯簡單的小游戲 瀏覽:814
伺服器如何以域用戶登錄 瀏覽:602
安卓os14怎麼默認桌面 瀏覽:549
應用市場下載在哪個文件夾 瀏覽:895
安卓上的谷歌地圖怎麼用 瀏覽:183
安卓命令行打包 瀏覽:516
編程文字與數字教學視頻 瀏覽:817
如何看手機號碼注冊哪些app 瀏覽:413
linux查看總內存 瀏覽:852
python進程間共享 瀏覽:438
js如何獲取本地伺服器地址 瀏覽:70
gfx什麼時候支持安卓十一系統 瀏覽:941
壓縮機90兆帕 瀏覽:932
程序員調侃語句 瀏覽:582
不是php函數的是 瀏覽:1001
壓縮文件好處 瀏覽:787
3d266期神童三膽計演算法 瀏覽:189
通過愛思助手怎麼下載app 瀏覽:323
vi命令將文件創在桌面上 瀏覽:925
程序員做競價 瀏覽:698