SharePreference原始碼學習和多程式的場景

入魔的冬瓜發表於2019-04-28

複習了下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>物件,沒有則建立一個。
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        //獲取快取的map
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        //拿到對應的檔案的SharePreference
        sp = cache.get(file);
        if (sp == null) {
            checkMode(mode);
            if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
                if (isCredentialProtectedStorage()
                        && !getSystemService(UserManager.class)
                                .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
                    throw new IllegalStateException("SharedPreferences in credential encrypted "
                            + "storage are not available until after user is unlocked");
                }
            }
            //沒有快取,建立SharedPreferencesImpl物件,並儲存到快取中
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            return sp;
        }
    }
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
        getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        // If somebody else (some other process) changed the prefs
        // file behind our back, we reload it.  This has been the
        // historical (if undocumented) behavior.
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}


@GuardedBy("ContextImpl.class")
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
        sSharedPrefsCache = new ArrayMap<>();
    }
    final String packageName = getPackageName();
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);
    }
    return packagePrefs;
}
複製程式碼

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() {
            @Override
            public 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() {
            @Override
            public 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值有變化的時候,進行通知訂閱者。

相關文章