SharePreference源码学习和多进程的场景
復習了下SharePreference的使用,以及了解下SharePreference的源碼實現,解決多進程情況下的SharePreference問題,做下筆記。
參考文章:
源碼分析:
www.jianshu.com/p/8eb2147c3…
www.jianshu.com/p/3b2ac6201…
SharePreference的多進程解決方案:
juejin.im/entry/59083…
SharePreference
Android平臺中一個輕量級的存儲庫,用來保存應用程序的各種配置信息。本質是一個以“key-value”鍵值對的方式保存數據的xml文件。
文件保存地址:在/data/data/package name/shared_prefs目錄下就可以查看到這個文件了
簡單使用例子
//獲取得到SharePreference,第一個參數是文件名稱,第二個參數是操作模式//一般是MODE_PRIVATE模式,指定該SharedPreferences數據只能被本應用程序讀、寫SharedPreferences sharedPreferences = getSharedPreferences("test", MODE_PRIVATE);//創建Editor對象SharedPreferences.Editor editor=sharedPreferences.edit();//保存數據editor.putString("name","donggua");//editor.commit();editor.apply();//讀取數據String result=sharedPreferences.getString("name","默認值"); 復制代碼commit和apply的區別
當使用commit去提交數據的時候,發現IDE提示讓我們使用apply方法。
- commit:同步提交,commit將同步的把數據寫入磁盤和內存緩存,并且有返回值。
- apply:異步提交,會把數據同步寫入內存緩存,然后異步保存到磁盤,可能會失敗,失敗不會收到錯誤回調。
兩者的區別:
- commit的效率會比apply慢一點。在一個進程中,如果在不關心提交結果是否成功的情況下,優先考慮apply方法。
- 都是原子性操作,但是原子的操作不同。commit的從數據提交到保存到內存后再保存到磁盤中,中間不可打斷。而apply方法是將數據保存到內存后就可以返回了,異步執行保存到磁盤的操作,
源碼分析
獲取SharePreference對象
利用Context獲取到SharePreference實例,ContextImpl是Context的實現類,實現了getSharedPreferences方法。
- 因為SharedPreferences是支持自定義文件名的,所以這里利用了ArrayMap<File, SharedPreferencesImpl>來緩存不同文件對應的SharedPreferencesImpl對象。一個File文件對應一個SharePreference對象。
- getSharedPreferencesCacheLocked(),獲取緩存的ArrayMap<File, SharedPreferencesImpl>對象,沒有則創建一個。
SharedPreferencesImpl是SharedPreferences接口的實現類,實現了commit和apply方法。先看看SharedPreferencesImpl的構造方法。會異步調用一個startLoadFromDisk的方法,作用是從磁盤中把SharePreference文件里面保存的xml信息讀取到內存中,并保存到Map里面。
SharedPreferencesImpl(File file, int mode) {mFile = file;mBackupFile = makeBackupFile(file);mMode = mode;mLoaded = false;mMap = null;mThrowable = null;startLoadFromDisk(); }private void startLoadFromDisk() {synchronized (mLock) {mLoaded = false;}new Thread("SharedPreferencesImpl-load") {public void run() {loadFromDisk();}}.start(); }private void loadFromDisk(){ //省略部分代碼 try {stat = Os.stat(mFile.getPath());if (mFile.canRead()) {BufferedInputStream str = null;try {str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);//進行xml解析map = (Map<String, Object>) XmlUtils.readMapXml(str);} catch (Exception e) {Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);} finally {IoUtils.closeQuietly(str);}} } catch (ErrnoException e) {// An errno exception means the stat failed. Treat as empty/non-existing by// ignoring. } catch (Throwable t) {thrown = t; } }//省略部分代碼。//將解析結果保存的map進行賦值 if (map != null) {mMap = map;mStatTimestamp = stat.st_mtim;mStatSize = stat.st_size; } 復制代碼讀取數據
例如SharedPreferencesImpl的實現getString()方法,是直接從內存中的mMap直接就把數據讀取出來,并沒有涉及到磁盤操作。(恍然大悟,以前以為讀取數據也要去讀取file文件)
@Override @Nullable public String getString(String key, @Nullable String defValue) {synchronized (mLock) {awaitLoadedLocked();String v = (String)mMap.get(key);return v != null ? v : defValue;} } 復制代碼保存數據
EditorImpl類實現了Editor接口。apply和commit都會調用commitToMemory方法,將數據保存到內存中,后面調用enqueueDiskWrite將數據保存到磁盤中。
//臨時緩存多個key的數據,后面提交數據的時候,就遍歷這個map就行 private final Map<String, Object> mModified = new HashMap<>(); //CountDownLatch,等待直到保存到磁盤的操作完成。 final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);//在當前線程直接寫文件,調用await,同步等待,最后返回操作的結果result @Override public 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 {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; }//異步等待保存操作,無法獲取操作的結果 @Override public void apply() {final long startTime = System.currentTimeMillis();//保存到內存中final MemoryCommitResult mcr = commitToMemory();final Runnable awaitCommit = new Runnable() {@Overridepublic void run() {try {mcr.writtenToDiskLatch.await();} catch (InterruptedException ignored) {}if (DEBUG && mcr.wasWritten) {Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration+ " applied after " + (System.currentTimeMillis() - startTime)+ " ms");}}};QueuedWork.addFinisher(awaitCommit);Runnable postWriteRunnable = new Runnable() {@Overridepublic void run() {awaitCommit.run();QueuedWork.removeFinisher(awaitCommit);}};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); } 復制代碼多進程中的SharePreference
上面講的默認是單進程中的SharePreference,讀取操作是直接從內存中的Map讀取的,不涉及IO操作。如果是在多進程中的話,不同進程之間的內存并不是共享的,這個時候讀寫同一個SharePreference就會出現問題了。比如多個進程對同一個sharedpreference進行修改,總會有一個進程獲取到的結果不是實時修改后的結果。
解決方法:推薦使用ContentProvider來處理多進程間的文件共享。
ContentProvider的特點:
- ContentProvider內部的同步機制會防止多個進程同時訪問,避免數據沖突。
- ContentProvider的數據源,并不是只能選擇數據庫,其實核心操作就在update()和query()這兩個操作,里面操作存取的數據源其實可以根據我們需要,替換成文件,也可以換成SharedPreferences。
所以我們可以使用ContentProvider做了一下中間媒介,讓它幫我們實現多進程同步機制,里面操作的數據改成SharedPreferences來實現。這樣的話就可以實現了跨進程訪問SharePreference。
下面簡單地寫一個demo,讀取的時候只需要傳進相應的uri就行了。比如下面的代碼,path字段的第二個是fileName,第三個是key值。
public class MultiProcessSharePreference extends ContentProvider{ @Nullable @Override public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {//獲取xml的文件名,默認取path字段的第一個Log.d(TAG, "query: uri:" + uri);String tableName = uri.getPathSegments().get(0);String name = uri.getPathSegments().get(1);String key = uri.getPathSegments().get(2);Log.d(TAG, "query: tableName:" + tableName);Log.d(TAG, "query: fileName:" + name);Log.d(TAG, "query: key:" + key);//創建sharedPreferences對象SharedPreferences sharedPreferences = getContext().getSharedPreferences(name, Context.MODE_PRIVATE);//創建一個cursor對象MatrixCursor cursor = null;switch (uriMatcher.match(uri)) {case CODE_PREFERENCE_STRING:String value = sharedPreferences.getString(key, "默認值");cursor = new MatrixCursor(PREFERENCE_COLUMNS, 1);MatrixCursor.RowBuilder rowBuilder = cursor.newRow();rowBuilder.add(value);break;default:Log.d(TAG, "query: Uri No Match");}return cursor; } } 復制代碼- MatrixCursor: 如果需要一個cursor而沒有一個現成的cursor的話,那么可以使用MatrixCursor實現一個虛擬的表。MatrixCursor.RowBuilder是用來添加Row數據的,通過rowBuilder的add方法,就可以把數值添加到行里面了。使用場景:比如ContentProvider的query方法是返回一個cursor類型的數據,而數據源用的是SharePreference,這個時候就可以利用MatrixCursor。MartixCursor本質上是用一個一位數據來模擬一個二維數據,根據行值和列值就可以找到對應的數據了。
MatrixCursor的源碼解析:blog.csdn.net/zhang_jun_l…
//定義每一列的字段名字 public static final String COLUMN_VALUE = "value"; //創建一個字符數組,字符數組的值對應著表的字段 private static String[] PREFERENCE_COLUMNS = {COLUMN_VALUE}; //構造一個MatrixCursor對象 MatrixCursor cursor = new MatrixCursor(PREFERENCE_COLUMNS, 1); //通過matrixCursor的addRow方法添加一行值 MatrixCursor.RowBuilder rowBuilder = cursor.newRow(); rowBuilder.add(value); 復制代碼- 優化一下的思路:
- 在ContentProvider里面加一個HashMap<String,SharePreference>進行一下緩存,key值是文件名,value是對應的SharePreference對象,這樣的話,就不用每次都去加載SharePreference對象了。
- 在ContentProvider里面實現回調listener,在key值有變化的時候,進行通知訂閱者。
總結
以上是生活随笔為你收集整理的SharePreference源码学习和多进程的场景的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Flutter 基础Widgets之Ap
- 下一篇: 论如何监听一个对象所有属性的变化