Jetpack 的 DataStore 是一種數(shù)據(jù)存儲(chǔ)解決方案,可以像 SharedPreferences 一樣存儲(chǔ)鍵值對(duì)或使用 protocol buffers 存儲(chǔ)類型化的對(duì)象。 DataStore 使用 Kotlin 的協(xié)程和 Flow 以異步的、一致性的、事務(wù)性的方式來(lái)存儲(chǔ)數(shù)據(jù),對(duì)比 SharedPreferences 有許多改進(jìn)和優(yōu)化,主要作為 SharedPreferences 的替代品,并且由 SharedPreferences 遷移非常方便。
DataStore 提供了兩種方式:
Preferences DataStore:以鍵值對(duì)的形式存儲(chǔ)在本地,和 SP 類似,但是 DataStore 是基于
Flow實(shí)現(xiàn)的,不會(huì)阻塞主線程,但不能保證類型安全。Proto DataStore:存儲(chǔ)自定義數(shù)據(jù)類型的對(duì)象(typed objects),通過(guò)
protocol buffers將對(duì)象序列化存儲(chǔ)在本地,這要求通過(guò)protocol buffers預(yù)先定義 schema,但是能保證類型安全。
既然 DataStore 是 SP 的替代和改進(jìn),那 SP 存在著什么問(wèn)題需要被改進(jìn)呢?
SharedPreferences 的不足
SharedPreference 是一個(gè)輕量級(jí)的數(shù)據(jù)存儲(chǔ)方式,使用起來(lái)非常方便,以鍵值對(duì)的形式存儲(chǔ)在本地,但存在以下問(wèn)題:
通過(guò) getXXX() 方法獲取數(shù)據(jù),可能會(huì)導(dǎo)致主線程阻塞
所有 getXXX() 方法都是同步的,在主線程調(diào)用 get 方法,必須等待 SP 加載完畢,初始化 SP 的時(shí)候,會(huì)將整個(gè) xml 文件內(nèi)容加載內(nèi)存中,如果文件很大,讀取較慢,會(huì)導(dǎo)致主線程阻塞。
val sp = getSharedPreferences("ByteCode", Context.MODE_PRIVATE) // 異步加載 SP 文件內(nèi)容
sp.getString("jetpack", ""); // 等待 SP 加載完畢
getSharedPreferences 時(shí)開(kāi)啟一個(gè)線程異步讀取數(shù)據(jù),最終會(huì)進(jìn)入SharedPreferencesImpl的loadFromDisk方法:
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
// Debugging
if (mFile.exists() && !mFile.canRead()) {
Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
}
Map<String, Object> map = null;
StructStat stat = null;
Throwable thrown = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
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;
}
synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
// It's important that we always signal waiters, even if we'll make
// them fail with an exception. The try-finally is pretty wide, but
// better safe than sorry.
try {
if (thrown == null) {
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
// In case of a thrown exception, we retain the old map. That allows
// any open editors to commit and store updates.
} catch (Throwable t) {
mThrowable = t;
} finally {
mLock.notifyAll();
}
}
}
在這里通過(guò)對(duì)象鎖 mLock機(jī)制來(lái)對(duì)其進(jìn)行加鎖操作。只有當(dāng) SP 文件中的數(shù)據(jù)全部讀取完畢之后才會(huì)調(diào)用mLock.notifyAll() 來(lái)釋放鎖,而 get 方法會(huì)在 awaitLoadedLocked 方法中調(diào)用 mLock.wait()來(lái)等待SP 的初始化完成。所以雖然這是異步方法,但當(dāng)讀取的文件比較大時(shí),還沒(méi)讀取完,接著調(diào)用 getXXX() 方法需等待其完成,就可能導(dǎo)致主線程阻塞。
SharedPreference 不能保證類型安全
調(diào)用 getXXX() 方法的時(shí)候,可能會(huì)出現(xiàn) ClassCastException 異常,因?yàn)槭褂孟嗤?key 進(jìn)行操作的時(shí)候,putXXX 方法可以使用不同類型的數(shù)據(jù)覆蓋掉相同的 key。
val key = "jetpack"
val sp = getSharedPreferences("ByteCode", Context.MODE_PRIVATE)
sp.edit { putInt(key, 0) } // 使用 Int 類型的數(shù)據(jù)覆蓋相同的 key
sp.getString(key, ""); // 使用相同的 key 讀取 Sting 類型的數(shù)據(jù)
由于 SP 內(nèi)部是通過(guò)Map來(lái)保存對(duì)于的key-value,所以它并不能保證key-value的類型固定,導(dǎo)致通過(guò)get方法來(lái)獲取對(duì)應(yīng)key的值的類型也是不安全的。
在getString的源碼中,會(huì)進(jìn)行類型強(qiáng)制轉(zhuǎn)換,如果類型不對(duì)就會(huì)導(dǎo)致程序崩潰。由于SP不會(huì)在代碼編譯時(shí)進(jìn)行提醒,只能在代碼運(yùn)行之后才能發(fā)現(xiàn),避免不掉可能發(fā)生的異常。
SharedPreference 加載的數(shù)據(jù)會(huì)一直留在內(nèi)存中,浪費(fèi)內(nèi)存
通過(guò) getSharedPreferences() 方法加載的數(shù)據(jù),最后會(huì)將數(shù)據(jù)存儲(chǔ)在靜態(tài)的成員變量中。靜態(tài)的 ArrayMap 緩存每一個(gè) SP 文件,而每個(gè) SP 文件內(nèi)容通過(guò) Map 緩存鍵值對(duì)數(shù)據(jù),這樣數(shù)據(jù)會(huì)一直留在內(nèi)存中,浪費(fèi)內(nèi)存。
apply() 方法雖然是異步的,仍可能會(huì)發(fā)生 ANR
apply 異步提交解決了線程的阻塞問(wèn)題,但如果 apply 任務(wù)過(guò)多數(shù)據(jù)量過(guò)大,可能會(huì)導(dǎo)致ANR的產(chǎn)生。
apply() 方法不是異步的嗎,為什么還會(huì)造成 ANR 呢?apply() 方法本身沒(méi)有問(wèn)題,但是當(dāng)生命周期處于 handleStopService() 、 handlePauseActivity() 、 handleStopActivity() 的時(shí)候會(huì)一直等待 apply() 方法將數(shù)據(jù)保存成功,否則會(huì)一直等待,從而阻塞主線程造成 ANR。
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");
}
}
};
// 注意:將awaitCommit添加到隊(duì)列中
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
// 成功寫入磁盤之后才將awaitCommit移除
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);
}
這里關(guān)鍵點(diǎn)是會(huì)將 awaitCommit 加入到 QueuedWork 隊(duì)列中,只有當(dāng) awaitCommit 執(zhí)行完之后才會(huì)進(jìn)行移除。
另一方面,在 Activity 和 Service 的 handleStopService() 、 handlePauseActivity() 、 handleStopActivity() 中會(huì)等待 QueuedWork 中的任務(wù)全部完成,一旦 QueuedWork 中的任務(wù)非常耗時(shí),例如 SP 的寫入磁盤數(shù)據(jù)量過(guò)多,就會(huì)導(dǎo)致主線程長(zhǎng)時(shí)間未響應(yīng),從而產(chǎn)生 ANR:
public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,
int configChanges, PendingTransactionActions pendingActions, String reason) {
ActivityClientRecord r = mActivities.get(token);
if (r != null) {
if (userLeaving) {
performUserLeavingActivity(r);
}
r.activity.mConfigChangeFlags |= configChanges;
performPauseActivity(r, finished, reason, pendingActions);
// Make sure any pending writes are now committed.
if (r.isPreHoneycomb()) {
//等待任務(wù)完成
QueuedWork.waitToFinish();
}
mSomeActivitiesChanged = true;
}
}
SharedPreference 不能跨進(jìn)程通信
SP 是不能跨進(jìn)程通信的,雖然在獲取 SP 時(shí)提供了MODE_MULTI_PROCESS,但內(nèi)部并不是用來(lái)跨進(jìn)程的。
public SharedPreferences getSharedPreferences(File file, int mode) {
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
// 重新讀取SP文件內(nèi)容
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
在這里使用 MODE_MULTI_PROCESS 只是重新讀取一遍文件而已,并不能保證跨進(jìn)程通信。
apply() 方法沒(méi)有結(jié)果回調(diào)
為了防止 SP 寫入時(shí)阻塞線程,一般都會(huì)使用 apply 方法來(lái)將數(shù)據(jù)異步寫入到文件中,但它無(wú)法有返回值,也沒(méi)有對(duì)應(yīng)的結(jié)果回調(diào),所以無(wú)法得知此次寫入結(jié)果是成功還是失敗。
DataStore 有哪些改進(jìn)
針對(duì) SP 的幾個(gè)問(wèn)題,DataStore 都?jí)蚰芤?guī)避。
-
DataStore內(nèi)部使用kotlin協(xié)程通過(guò)掛起的方式來(lái)避免阻塞線程,避免產(chǎn)生 ANR。 -
DataStore不僅支持 SP 同時(shí)還支持protocol buffers類型的存儲(chǔ),protocol buffers是可以保證數(shù)據(jù)類型安全的。 -
DataStore能夠在編譯階段提醒 SP 類型錯(cuò)誤,減少寫代碼時(shí)的失誤導(dǎo)致類型不安全問(wèn)題。

-
DataStore使用Flow來(lái)獲取數(shù)據(jù),每次保存數(shù)據(jù)之后都會(huì)通知最近的Flow,可以獲得到操作成功或失敗的結(jié)果。 -
DataStore完美支持 SP 數(shù)據(jù)的遷移,可以無(wú)成本過(guò)渡到DataStore。
對(duì)比圖
SharedPreferences、DataStore、MMKV 的對(duì)比:


DataStore 的使用和遷移
Preferences DataStore
添加依賴
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01"
構(gòu)建 DataStore
private val PREFERENCE_NAME = "DataStore"
var dataStore: DataStore<Preferences> = context.createDataStore(
name = PREFERENCE_NAME
存儲(chǔ)位置為 data/data/包名/files/datastore/ + PREFERENCE_NAME + .preferences_pb
讀取數(shù)據(jù)
注意:
Preferences DataStore只支持Int,Long,Boolean,Float,String這幾種鍵值對(duì)數(shù)據(jù)。
val KEY_BYTE_CODE = preferencesKey<Boolean>("ByteCode")
fun readData(key: Preferences.Key<Boolean>): Flow<Boolean> =
dataStore.data
.map { preferences ->
preferences[key] ?: false
}
dataStore.data 會(huì)返回一個(gè) Flow<T>,每當(dāng)數(shù)據(jù)變化的時(shí)候都會(huì)重新發(fā)出。
寫入數(shù)據(jù)
suspend fun saveData(key: Preferences.Key<Boolean>) {
dataStore.edit { mutablePreferences ->
val value = mutablePreferences[key] ?: false
mutablePreferences[key] = !value
}
}
通過(guò) DataStore.edit() 寫入數(shù)據(jù)的,DataStore.edit() 是一個(gè) suspend 函數(shù),所以只能在協(xié)程體內(nèi)使用。
從 SharedPreferences 遷移
遷移 SharedPreferences 到 DataStore 只需要 2 步。
- 構(gòu)建
DataStore的時(shí)候,需要傳入一個(gè)SharedPreferencesMigration
dataStore = context.createDataStore(
name = PREFERENCE_NAME,
migrations = listOf(
SharedPreferencesMigration(
context,
SharedPreferencesRepository.PREFERENCE_NAME
)
)
)
- 當(dāng)
DataStore對(duì)象構(gòu)建完了之后,需要執(zhí)行一次讀取或者寫入操作,即可完成SharedPreferences遷移到DataStore,當(dāng)遷移成功之后,會(huì)自動(dòng)刪除SharedPreferences使用的文件。
注意: 只從 SharedPreferences 遷移一次,因此一旦遷移成功之后,應(yīng)該停止使用 SharedPreferences。
Proto DataStore
Protocol Buffers:是 Google 開(kāi)源的跨語(yǔ)言編碼協(xié)議,可以應(yīng)用到 C++ 、C# 、Dart 、Go 、Java 、Python 等等語(yǔ)言,Google 內(nèi)部幾乎所有 RPC 都在使用這個(gè)協(xié)議,使用了二進(jìn)制編碼壓縮,體積更小,速度比 JSON 更快,但是缺點(diǎn)是犧牲了可讀性。
Proto DataStore 通過(guò) protocol buffers 將對(duì)象序列化存儲(chǔ)在本地,比起 Preference DataStore 支持更多類型,使用二進(jìn)制編碼壓縮,體積更小速度更快。使用 Proto DataStore 需要先引入 protocol buffers。
本文只對(duì) Proto DataStore 做簡(jiǎn)單介紹。
添加依賴
// Proto DataStore
implementation "androidx.datastore:datastore-core:1.0.0-alpha01"
// protobuf
implementation "com.google.protobuf:protobuf-javalite:3.10.0"
當(dāng)添加完依賴之后需要新建 proto 文件,在本文示例項(xiàng)目中新建了一個(gè) common-protobuf 模塊,將新建的 person.proto 文件,放到了 common-protobuf 模塊 src/main/proto 目錄下。
在 common-protobuf 模塊,build.gradle 文件內(nèi),添加以下依賴:
implementation "com.google.protobuf:protobuf-javalite:3.10.0"
新建 Person.proto 文件,添加以下內(nèi)容
syntax = "proto3";
option java_package = "com.hi.dhl.datastore.protobuf";
option java_outer_classname = "PersonProtos";
message Person {
// 格式:字段類型 + 字段名稱 + 字段編號(hào)
string name = 1;
}
執(zhí)行 protoc ,編譯 proto 文件
protoc --java_out=./src/main/java -I=./src/main/proto ./src/main/proto/*.proto
構(gòu)建 DataStore
object PersonSerializer : Serializer<PersonProtos.Person> {
override fun readFrom(input: InputStream): PersonProtos.Person {
try {
return PersonProtos.Person.parseFrom(input) // 是編譯器自動(dòng)生成的,用于讀取并解析 input 的消息
} catch (exception: Exception) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override fun writeTo(t: PersonProtos.Person, output: OutputStream) = t.writeTo(output) // t.writeTo(output) 是編譯器自動(dòng)生成的,用于寫入序列化消息
}
讀取數(shù)據(jù)
fun readData(): Flow<PersonProtos.Person> {
return protoDataStore.data
.catch {
if (it is IOException) {
it.printStackTrace()
emit(PersonProtos.Person.getDefaultInstance())
} else {
throw it
}
}
寫入數(shù)據(jù)
suspend fun saveData(personModel: PersonModel) {
protoDataStore.updateData { person ->
person.toBuilder().setAge(personModel.age).setName(personModel.name).build()
}
}
從 SharedPreferences 遷移
創(chuàng)建映射關(guān)系
構(gòu)建 DataStore 并傳入 shardPrefsMigration
執(zhí)行一次讀取或者寫入操作
SuperApp引入
SuperApp 當(dāng)前使用 SP 實(shí)現(xiàn)小數(shù)據(jù)存取,具體由 IPCConfig 工具類封裝 SP 提供靜態(tài)方法供各處使用。鑒于 DataStore 的各項(xiàng)改進(jìn)及遷移非常方便,可以考慮從 SP 遷移到 DataStore。
Proto DataStore 雖然有更多優(yōu)勢(shì),但需要引入Protocol Buffers,同時(shí)開(kāi)發(fā)者需要如 proto 語(yǔ)法等更多的學(xué)習(xí)成本,使用和遷移也會(huì)稍微麻煩些。考慮到現(xiàn)在暫時(shí)沒(méi)有 Proto DataStore 對(duì)應(yīng)的使用場(chǎng)景,可以先遷移到 Preferences DataStore,后續(xù)如有需要再做處理。
初步改寫 IPCConfig:
const val SHARED_PREFERENCES_NAME = "com.tplink.superapp_preferences"
const val DATA_STORE_NAME = "IPCConfig"
object IPCConfig {
private var mDataStore: DataStore<Preferences>? = null
@JvmStatic
fun putBoolean(context: Context?, key: String?, flag: Boolean) {
setConfig(context, key, flag)
}
@JvmStatic
fun getBoolean(context: Context?, key: String?, defaultValue: Boolean): Boolean {
return getConfig(context, key, defaultValue)
}
@JvmStatic
fun putInt(context: Context?, key: String?, num: Int) {
setConfig(context, key, num)
}
@JvmStatic
fun getInt(context: Context?, key: String?, defaultValue: Int): Int {
return getConfig(context, key, defaultValue)
}
@JvmStatic
fun putString(context: Context?, key: String?, value: String) {
setConfig(context, key, value)
}
@JvmStatic
fun getString(context: Context?, key: String?, defaultValue: String): String {
return getConfig(context, key, defaultValue)
}
@JvmStatic
fun putLong(context: Context?, key: String?, value: Long) {
setConfig(context, key, value)
}
@JvmStatic
fun getLong(context: Context?, key: String?, defaultValue: Long): Long {
return getConfig(context, key, defaultValue)
}
private fun getDataStore(context: Context): DataStore<Preferences>? {
if (mDataStore == null) {
mDataStore = context.createDataStore(
name = DATA_STORE_NAME,
migrations = listOf(
SharedPreferencesMigration(
context,
SHARED_PREFERENCES_NAME
)
)
)
}
return mDataStore
}
private inline fun <reified T : Any> getConfig(
context: Context?,
key: String?,
defaultValue: T
): T {
if (context == null || key == null) {
return defaultValue
}
return runBlocking {
getDataStore(context)?.data
?.catch {
// 當(dāng)讀取數(shù)據(jù)遇到錯(cuò)誤時(shí),如果是IOException異常,發(fā)送一個(gè)emptyPreferences重新使用
// 但是如果是其他的異常,最好將它拋出去,不要隱藏問(wèn)題
it.printStackTrace()
if (it is IOException) {
emit(emptyPreferences())
} else {
throw it
}
}?.map {
it[preferencesKey<T>(key)] ?: defaultValue
}?.first() ?: defaultValue
}
}
private inline fun <reified T : Any> setConfig(context: Context?, key: String?, value: T) {
if (context == null || key == null) {
return
}
GlobalScope.launch {
getDataStore(context)?.edit {
it[preferencesKey<T>(key)] = value
}
}
}
}
遷移前后文件結(jié)構(gòu):

測(cè)試可正常使用。
這樣修改可以只改變一個(gè)文件,各調(diào)用處無(wú)需變動(dòng),就完成到 Preferences DataStore 的遷移,但是 get 方法都是 runBlocking 同步方法,沒(méi)有使用到 DataStore 的全部功能。這里只是為了簡(jiǎn)單驗(yàn)證下遷移的可行性和便捷性,后續(xù)可以繼續(xù)優(yōu)化充分利用好 DataStore 的優(yōu)勢(shì)。