Android SharedPreference 詳解

前提:基于 Android API 30

1. 認識 SharedPreference

SharedPreference 是 Android 提供的輕量級的,線程安全的數據存儲機制,使用 key-value 鍵值對的方式將數據存儲在 xml 文件中,存儲路徑為

/data/data/yourPackageName/shared_prefs

存儲的數據是持久化(persistent)的,應用重啟后仍然可以獲取到存儲的數據。由于SharedPreference 是輕量級的,所以不適合存儲過多和過大的數據的場景,這種情況下應該考慮數據庫

2. 獲取 SharedPreference

SharedPreference 是一個接口,實現類是 android.app.SharedPreferencesImpl,獲取 SharedPreference 的方法由 Context 提供

val sp = context.getSharedPreferences("FreemanSp", MODE_PRIVATE)

第一個參數是 name,無論哪個 context 對象,只要傳入的 name 一樣,獲取到的就是同一個SharedPreference 對象

第二個參數是 mode,Android 提供 4 中模式可選,其中三種已明確標識為過期

  • MODE_PRIVATE,私有模式,只能由當前應用讀寫
  • MODE_WORLD_READABLE,其他應用可讀,已過期
  • MODE_WORLD_WRITEABLE,其他應用可寫,已過期
  • MODE_MULTI_PROCESS,同一應用存在多進程情況下共享,不可靠,已過期

聲明 MODE_WORLD_READABLEMODE_WORLD_WRITEABLE 這兩中模式,其他應用可以完全讀寫自己應用的數據,這是及其危險的,谷歌推薦使用 ContentProvier 方式去做數據共享,共享的數據可以完全由 provider 定義的 uri 做出限制。
甚至在 android N 后指定這兩種方式,應用直接報錯

    private void checkMode(int mode) {if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {if ((mode & MODE_WORLD_READABLE) != 0) {throw new SecurityException("MODE_WORLD_READABLE no longer supported");}if ((mode & MODE_WORLD_WRITEABLE) != 0) {throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");}}}

基于 mode 的限制,本文一切都已 MODE_PRIVATE 展開

創建 SharedPreferencesImpl 對象時,會起一個子線程,將 xml 文件解析的數據全量保存在內存中

    private void startLoadFromDisk() {synchronized (mLock) {mLoaded = false;}new Thread("SharedPreferencesImpl-load") {public void run() {loadFromDisk();}}.start();}

如果存儲著大量數據時會有占用較大內存

3. 寫入數據

SharedPreference通過調用edit()方法獲取到一個Editor對象,其實現類是android.app.SharedPreferencesImpl.EditorImpl。寫入數據提供了同步 commit 和異步 apply 兩個提交方法

3.1 commit()

調用方式

 sp.edit().putLong("currentTime", System.currentTimeMillis()).commit()

該方法是個同步方法,IDE 會出現警告提示

Commit your preferences changes back from this Editor to the SharedPreferences object it is editing. This atomically performs the requested modifications, replacing whatever is currently in the SharedPreferences.
Note that when two editors are modifying preferences at the same time, the last one to call commit wins.
If you don’t care about the return value and you’re using this from your application’s main thread, consider using apply instead

結論是如果不考慮返回結果或者在主線程,應該考慮用 apply 方法替代,原因來看下原碼

        @Overridepublic boolean commit() {long startTime = 0;if (DEBUG) {startTime = System.currentTimeMillis();}// 先提交到內存MemoryCommitResult mcr = commitToMemory();// 寫入磁盤SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */);try {// 當前線程 await,確保 mcr 已經提交到了磁盤mcr.writtenToDiskLatch.await();} catch (InterruptedException e) {return false;} finally {if (DEBUG) {Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration+ " committed after " + (System.currentTimeMillis() - startTime)+ " ms");}}notifyListeners(mcr);return mcr.writeToDiskResult;}

再詳細看下 commitToMemory(),這個方法的主要作用有兩個:

  1. 寫入內存
  2. 構造寫入磁盤的map,需要注意的是這個map是全量寫入的
        private MemoryCommitResult commitToMemory() {long memoryStateGeneration;boolean keysCleared = false;List<String> keysModified = null; // 需要提交存儲的鍵值對Set<OnSharedPreferenceChangeListener> listeners = null;Map<String, Object> mapToWriteToDisk; // 需要寫入磁盤的鍵值對,這里會是整個 xml 文件的全量數據synchronized (SharedPreferencesImpl.this.mLock) {// We optimistically don't make a deep copy until// a memory commit comes in when we're already// writing to disk.if (mDiskWritesInFlight > 0) { // 多線程提交時,對 mMap 做深拷貝,這個 mMap 就是整個 xml 文件的全量鍵值對// We can't modify our mMap as a currently// in-flight write owns it.  Clone it before// modifying it.// noinspection uncheckedmMap = new HashMap<String, Object>(mMap);}mapToWriteToDisk = mMap;// 同時要寫入磁盤的提交計數標記為 +1,如果同時有異步多個提交,后面的 commit 有可能會提交到子線程,并且由于版本控制,最后一個 commit 的才會寫入到磁盤mDiskWritesInFlight++; boolean hasListeners = mListeners.size() > 0;if (hasListeners) {keysModified = new ArrayList<String>();listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());}synchronized (mEditorLock) {boolean changesMade = false;if (mClear) { // 只有在調用了 Editor#clear() 方法才會進入這段代碼,因為 mEditorLock 鎖if (!mapToWriteToDisk.isEmpty()) {changesMade = true;mapToWriteToDisk.clear();}keysCleared = true;mClear = false;}// 將用戶要修改的數據合并到 mapToWriteToDiskfor (Map.Entry<String, Object> e : mModified.entrySet()) {String k = e.getKey();Object v = e.getValue();// "this" is the magic value for a removal mutation. In addition,// setting a value to "null" for a given key is specified to be// equivalent to calling remove on that key.if (v == this || v == null) {if (!mapToWriteToDisk.containsKey(k)) {continue;}mapToWriteToDisk.remove(k);} else {if (mapToWriteToDisk.containsKey(k)) {Object existingValue = mapToWriteToDisk.get(k);if (existingValue != null && existingValue.equals(v)) {continue;}}mapToWriteToDisk.put(k, v);}changesMade = true;if (hasListeners) {keysModified.add(k);}}mModified.clear();if (changesMade) {mCurrentMemoryStateGeneration++;}// 寫入的版本管理,優化寫入磁盤的寫入次數,避免資源浪費和數據覆蓋memoryStateGeneration = mCurrentMemoryStateGeneration;}}return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,listeners, mapToWriteToDisk);}

寫入內存后接著就要執行寫入磁盤

  SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */);

enqueueDiskWrite 方法的主要作用是

  1. 同步寫入磁盤
  2. 如果此時有新的 commit 提交,舊提交將會被拋棄,新提交則有可能提交到子線程
    private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {final boolean isFromSyncCommit = (postWriteRunnable == null); // 同步commit,isFromSyncCommit = truefinal Runnable writeToDiskRunnable = new Runnable() {@Overridepublic void run() {synchronized (mWritingToDiskLock) {/** 真正執行寫入到磁盤的操作,但是 writeToFile 會有 memoryStateGeneration 控制,如果當前的 mcr 版本和 memoryStateGeneration 不一致,則當前的寫入會被對其* 這也是 commit 方法注釋上寫的:* Note that when two editors are modifying preferences at the same time, the last one to call commit wins*/ writeToFile(mcr, isFromSyncCommit);}synchronized (mLock) {mDiskWritesInFlight--;}if (postWriteRunnable != null) {postWriteRunnable.run();}}};// Typical #commit() path with fewer allocations, doing a write on// the current thread.if (isFromSyncCommit) { // apply 方法不會進入到這個代碼塊boolean wasEmpty = false;synchronized (mLock) {// 如果此時有新的 commit 執行到 commitToMemory ,則 mDiskWritesInFlight 則有可能大于 1,此時 wasEmpty = falsewasEmpty = mDiskWritesInFlight == 1;}if (wasEmpty) { // 如果 wasEmpty 為 true,則直接在當前線程執行寫入磁盤writeToDiskRunnable.run();return;}}// 將寫入磁盤的操作加入到子線程QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);}

總結

  • 如果主線程執行commit會在主線程執行 IO 操作,數據量太大可能會造成 ANR
  • edit() 方法每次都會返回一個新的Editor對象,應該緩存一個Editor對象重復使用,SP 是線程安全的
  • 如果實在需要使用commit,可以考慮將頻繁寫入的key單獨放到一個 SP,這樣避免資源浪費
  • 盡量避免多次調用commit,可以多次put數據,在合適實際執行一次commit
  • 避免使用commit
3.2 apply()

調用方式

 sp.edit().putLong("currentTime", System.currentTimeMillis()).apply()

該方法是個異步方法,Android 中推薦使用這個方法存儲數據,直接看源碼

public void apply() {final long startTime = System.currentTimeMillis();final MemoryCommitResult mcr = commitToMemory(); // 寫入內存,構造寫入磁盤的map,同commit一樣,這個map是全量寫入的final Runnable awaitCommit = new Runnable() { // 這個 Runnable 的作用就是為了統計寫入磁盤的耗時@Overridepublic void run() {try {mcr.writtenToDiskLatch.await(); // block 待 mcr 寫入磁盤完成后 writtenToDiskLatch 降為0,await()立刻返回,awaitCommit 繼續執行} catch (InterruptedException ignored) {}if (DEBUG && mcr.wasWritten) {Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration+ " applied after " + (System.currentTimeMillis() - startTime)+ " ms");}}};QueuedWork.addFinisher(awaitCommit); // 將 awaitCommit 加入到 QueuedWork 子線程的消息隊列// postWriteRunnable 會在寫入磁盤完成后觸發執行Runnable postWriteRunnable = new Runnable() {@Overridepublic void run() {awaitCommit.run(); // 觸發 awaitCommit ,統計寫入耗時QueuedWork.removeFinisher(awaitCommit);}};// postWriteRunnable 會在真正執行寫入磁盤的 runnable 調用SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);// Okay to notify the listeners before it's hit disk// because the listeners should always get the same// SharedPreferences instance back, which has the// changes reflected in memory.notifyListeners(mcr);
}

commit一樣,apply也是先寫入內存后并構建全量寫入磁盤的map。不同的是在調用 enqueueDiskWrite時傳入了一個postWriteRunnableenqueueDiskWrite 通過判斷這個參數來區別是否是異步執行,該方法在commit流程中已分析過一部分,這里再來看apply的流程

   private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) { // apply 傳入的 postWriteRunnable 不為 nullfinal boolean isFromSyncCommit = (postWriteRunnable == null); // isFromSyncCommit = falsefinal Runnable writeToDiskRunnable = new Runnable() {@Overridepublic void run() {synchronized (mWritingToDiskLock) {// 如果 memoryStateGeneration 一致,將完成磁盤的寫入writeToFile(mcr, isFromSyncCommit);}synchronized (mLock) {mDiskWritesInFlight--;}if (postWriteRunnable != null) {postWriteRunnable.run(); // 通知寫入完成}}};// Typical #commit() path with fewer allocations, doing a write on// the current thread.if (isFromSyncCommit) {boolean wasEmpty = false;synchronized (mLock) {wasEmpty = mDiskWritesInFlight == 1;}if (wasEmpty) {writeToDiskRunnable.run();return;}}// writeToDiskRunnable 直接加入子線程消息隊列QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);}

總結

  • edit() 方法每次都會返回一個新的Editor對象,應該緩存一個Editor對象重復使用,SP 是線程安全的
  • 多次 apply 也不會造成多次寫入磁盤,因為有版本控制
  • 推薦使用apply

4. SharedPreference 造成ANR的原因

  1. 在主線程 commit 寫入大量的數據
  2. 在 ActivityThread 共用四個地方會調用QueuedWork.waitToFinish();,分別是:
  • handleServiceArgs
  • handleStopService
  • handlePauseActivity
  • handleStopActivity

這些都是Android組件的生命周期管理,如果QueuedWork有遺留的runnable沒有執行完,則會將剩余的runnable在主線程執行,導致產生了ANR的風險

解決辦法

  1. 避免使用commit,使用 apply
  2. 將頻繁寫入的key放到獨立的 SharedPreference
  3. 使用 MMKV 遷移或者代替SharedPreference

5. MMKV 簡單分析

  1. mmkv 提供了 importFromSharedPreference 方法用來做數據遷移
  2. mmkv 不支持 getAll 方法,因為通過 protoBuf 二進制存取,在其他存取操作時都會指定數據類型,比如getInt, getBoolean。但是如果使用getAll,mmkv不知道每個key對應的數據類型,所以無法decode。為了能夠代理 getAll,并避免第三方sdk有使用該方法,可以將類型轉換上移,在使用mmkv存取時統一用 String 類型,再拿出值時再由業務自身去判斷(因為業務知道自己在寫入時,每個key對應的數據類型),SharedPreference 自身也只是返回了一個 Map<String, Object> 對象
  3. 支持多進程讀寫
  4. 每次都是增量寫入,本身不提供緩存。當寫入時,發現內存不夠會動態申請內存空間

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/73223.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/73223.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/73223.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

自動化測試腳本語言選擇

測試人員在選擇自動化測試腳本語言時面臨多種選項。Python、Java、C#、JavaScript 和 Ruby 都是常見選擇&#xff0c;但哪種語言最適合&#xff1f;本文將詳細分析這些語言的特點、適用場景和優劣勢&#xff0c;結合行業趨勢和社會現象&#xff0c;為測試人員提供全面指導。 選…

【Java項目】基于JSP的KTV點歌系統

【Java項目】基于JSP的KTV點歌系統 技術簡介&#xff1a;采用JSP技術、B/S結構、MYSQL數據庫等實現。 系統簡介&#xff1a;KTV點歌系統的主要使用者分為管理員和用戶&#xff0c;實現功能包括管理員&#xff1a;個人中心、用戶管理、歌曲庫管理、歌曲類型管理、點歌信息管理&a…

element-plus文檔解析之Layout布局(el-row,el-col)

前言 這是element-plus提供的響應式布局組件。可以非常方便的實現響應式布局以及快速按比例分塊。 例如實現下面的效果&#xff1a; 第一行&#xff1a;寬度占100% 第二行&#xff1a;寬度1&#xff1a;1 第三行&#xff1a;1&#xff1a;1&#xff1a;1 第四行&#xff1a;1…

【Java】——數據類型和變量

個人主頁&#xff1a;User_芊芊君子 &#x1f389;歡迎大家點贊&#x1f44d;評論&#x1f4dd;收藏?文章 文章目錄&#xff1a; 1.Java中的注釋1.1.基本規則1.2.注釋規范 2.標識符3.關鍵字4.字面常量5.數據類型6.變量6.1變量的概念6.2語法6.3整型變量6.3.1整型變量6.3.2長整…

串口數據記錄儀DIY,體積小,全開源

作用 產品到客戶現場出現異常情況&#xff0c;這個時候就需要一個日志記錄儀、黑匣子&#xff0c;可以記錄產品的工作情況&#xff0c;當出現異常時&#xff0c;可以搜集到上下文的數據&#xff0c;從而判斷問題原因。 之前從網上買過&#xff0c;但是出現過丟數據的情況耽誤…

JVM中是如何定位一個對象的

在 Java 中&#xff0c;對象定位指的是如何通過引用&#xff08;Reference&#xff09;在堆內存中找到對象實例及其元數據&#xff08;如類型信息&#xff09;。JVM 主要通過 直接指針訪問 和 句柄訪問 兩種方式實現&#xff0c;各有其優缺點和應用場景&#xff1a; 一、直接指…

Mac 如何在idea集成SVN

在windows系統上面有我們最為熟悉的小烏龜TortoiseSVN&#xff0c;在mac系統上面則沒有什么好用的svn的工具&#xff0c;而且大部分都付費&#xff0c;需要各種渠道找PJ版&#xff0c;費事費力&#xff0c;作為程序員&#xff0c;大部分人應該都會安裝開發工具&#xff0c;本文…

批量測試IP和域名聯通性

最近需要測試IP和域名的聯通性&#xff0c;因數量很多&#xff0c;單個ping占用時間較長。考慮使用Python和Bat解決。考慮到依托的環境&#xff0c;Bat可以在Windows直接運行。所以直接Bat處理。 方法1 echo off for /f %%i in (E:\封禁IP\ipall.txt) do (ping %%i -n 1 &…

LabVIEW變頻器諧波分析系統

隨著工業自動化的發展&#xff0c;變頻器在電力、機械等領域的應用日益廣泛&#xff0c;但諧波問題直接影響系統效率與穩定性。傳統諧波檢測設備&#xff08;如Norma5000&#xff09;精度雖高&#xff0c;但價格昂貴且操作復雜&#xff0c;難以適應現場快速檢測需求。本項目基于…

Unity Shader學習總結

1.幀緩沖區和顏色緩沖區區別 用于存儲每幀每個像素顏色信息的緩沖區 幀緩沖區包括&#xff1a;顏色緩沖區 深度緩沖區 模板緩沖區 自定義緩沖區 2.ImageEffectShader是什么 后處理用的shader模版 3.computerShader 獨立于渲染管線之外&#xff0c;在顯卡上運行&#xff0c;大量…

OpenPLC WebServer啟動

簡述 OpenPLC OpenPLC 可運行在嵌入式系統和普通計算機上&#xff0c;其基本原理是在硬件上安裝類似 Linux 的操作系統&#xff0c;并在該環境下運行 OpenPLC 應用程序&#xff0c;從而讓用戶開發、調試和運行工業自動化控制邏輯。它目前只支持部分 ARM 架構的嵌入式系統&…

【基礎知識】回頭看Maven基礎

版本日期修訂人描述V1.02025/3/7nick huang創建文檔 背景 項目過程中&#xff0c;對于Maven的pom.xml文件&#xff0c;很多時候&#xff0c;我通過各種參考、仿寫&#xff0c;最終做出想要的效果。 但實際心里有些迷糊&#xff0c;不清楚具體哪個基礎的配置所實現的效果。 今…

ROS實踐(四)機器人SLAM建圖(gmapping)

目錄 一、SLAM技術 二、常用工具和傳感器 三、相關功能包 1. gmapping建圖功能包 2. map_server 四、SLAM 建圖實驗 1. 配置gmapping(launch文件) 2. 啟動機器人仿真(含機器人以及傳感器) 3. 運行gmapping節點 4. 啟動rviz可視化工具 5. 保存地圖文件 一、SLAM技…

二進制安裝指定版本的MariaDBv10.11.6

一、官網下載mariadb安裝包 Download MariaDB Server - MariaDB.org 找到對應的版本 下載安裝包后上傳到服務器這里不再贅述。 二、安裝二進制包 1、解壓安裝包 2、查看安裝包內的安裝提示文檔根據提示文檔進行安裝 # 解壓安裝包 tar xf mariadb-10.11.6-linux-systemd-x8…

【抽獎項目】|第二篇

前言&#xff1a; 高并發的活動預熱肯定不可以在數據庫操作&#xff0c;需要redis&#xff0c;特別是這種秒殺活動更是需要注意&#xff0c;所以可以在高并發的前夕先進行活動預熱。 思路&#xff1a; 1、 通過定時任務調度每分鐘查詢數據庫也沒有需要預熱的活動 2、采用分布式…

異或和之和 第十四屆藍橋杯大賽軟件賽省賽C/C++ 大學 A 組

異或和之和 題目來源 第十四屆藍橋杯大賽軟件賽省賽C/C++ 大學 A 組 原題鏈接 藍橋杯 異或和之和 https://www.lanqiao.cn/problems/3507/learning/ 問題描述 問題分析 要點1:異或運算 概念 異或(Exclusive OR,簡稱 XOR)是一種數學運算符,常用于邏輯運算與計算機…

從零到一:如何系統化封裝并發布 React 組件庫到 npm

1. 項目初始化 1.1 創建項目 首先&#xff0c;創建一個新的項目目錄并初始化 package.json 文件。 mkdir my-component-library cd my-component-library npm init -y1.2 安裝依賴 安裝開發所需的依賴項&#xff0c;如構建工具、測試框架等。 npm install --save-dev webp…

現代互聯網網絡安全與操作系統安全防御概要

現階段國與國之間不用對方路由器&#xff0c;其實是有道理的&#xff0c;路由器破了&#xff0c;內網非常好攻擊&#xff0c;內網共享開放端口也非常多&#xff0c;更容易攻擊。還有些內存系統與pe系統自帶瀏覽器都沒有javascript腳本功能&#xff0c;也是有道理的&#xff0c;…

2025-03-12 學習記錄--C/C++-PTA 習題8-4 報數

合抱之木&#xff0c;生于毫末&#xff1b;九層之臺&#xff0c;起于累土&#xff1b;千里之行&#xff0c;始于足下。&#x1f4aa;&#x1f3fb; 一、題目描述 ?? 習題8-4 報數 報數游戲是這樣的&#xff1a;有n個人圍成一圈&#xff0c;按順序從1到n編好號。從第一個人開…

【js逆向】某預約票網站 (webpack技術)

1、查看數據包 calendar是需要的數據包&#xff0c;看下它的請求參數。 accTimes參數加密&#xff0c;_times是時間戳。 2、全局搜索 accTimes 關鍵字 3、n的定義 4、把整個js代碼復制下來&#xff0c;應用到了webpack技術&#xff0c;圖中的Q是n