Jetpack 的 DataStore
是一種數據存儲解決方案撕贞,可以像 SharedPreferences
一樣存儲鍵值對或使用 protocol buffers
存儲類型化的對象。 DataStore
使用 Kotlin 的協程和 Flow
以異步的竞慢、一致性的落蝙、事務性的方式來存儲數據织狐,對比 SharedPreferences
有許多改進和優(yōu)化,主要作為 SharedPreferences
的替代品筏勒,并且由 SharedPreferences
遷移非常方便移迫。
DataStore
提供了兩種方式:
Preferences DataStore:以鍵值對的形式存儲在本地,和 SP 類似管行,但是 DataStore 是基于
Flow
實現的厨埋,不會阻塞主線程,但不能保證類型安全捐顷。Proto DataStore:存儲自定義數據類型的對象(typed objects)荡陷,通過
protocol buffers
將對象序列化存儲在本地,這要求通過protocol buffers
預先定義 schema迅涮,但是能保證類型安全废赞。
既然 DataStore
是 SP 的替代和改進,那 SP 存在著什么問題需要被改進呢叮姑?
SharedPreferences 的不足
SharedPreference
是一個輕量級的數據存儲方式唉地,使用起來非常方便,以鍵值對的形式存儲在本地传透,但存在以下問題:
通過 getXXX()
方法獲取數據耘沼,可能會導致主線程阻塞
所有 getXXX()
方法都是同步的,在主線程調用 get
方法朱盐,必須等待 SP 加載完畢群嗤,初始化 SP 的時候,會將整個 xml 文件內容加載內存中兵琳,如果文件很大狂秘,讀取較慢骇径,會導致主線程阻塞。
val sp = getSharedPreferences("ByteCode", Context.MODE_PRIVATE) // 異步加載 SP 文件內容
sp.getString("jetpack", ""); // 等待 SP 加載完畢
getSharedPreferences
時開啟一個線程異步讀取數據赃绊,最終會進入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();
}
}
}
在這里通過對象鎖 mLock
機制來對其進行加鎖操作既峡。只有當 SP 文件中的數據全部讀取完畢之后才會調用mLock.notifyAll()
來釋放鎖,而 get
方法會在 awaitLoadedLocked
方法中調用 mLock.wait()
來等待SP 的初始化完成碧查。所以雖然這是異步方法运敢,但當讀取的文件比較大時,還沒讀取完忠售,接著調用 getXXX()
方法需等待其完成传惠,就可能導致主線程阻塞。
SharedPreference 不能保證類型安全
調用 getXXX()
方法的時候稻扬,可能會出現 ClassCastException
異常卦方,因為使用相同的 key 進行操作的時候,putXXX
方法可以使用不同類型的數據覆蓋掉相同的 key泰佳。
val key = "jetpack"
val sp = getSharedPreferences("ByteCode", Context.MODE_PRIVATE)
sp.edit { putInt(key, 0) } // 使用 Int 類型的數據覆蓋相同的 key
sp.getString(key, ""); // 使用相同的 key 讀取 Sting 類型的數據
由于 SP 內部是通過Map
來保存對于的key-value
盼砍,所以它并不能保證key-value
的類型固定,導致通過get
方法來獲取對應key
的值的類型也是不安全的逝她。
在getString
的源碼中浇坐,會進行類型強制轉換,如果類型不對就會導致程序崩潰黔宛。由于SP
不會在代碼編譯時進行提醒近刘,只能在代碼運行之后才能發(fā)現,避免不掉可能發(fā)生的異常臀晃。
SharedPreference 加載的數據會一直留在內存中觉渴,浪費內存
通過 getSharedPreferences()
方法加載的數據,最后會將數據存儲在靜態(tài)的成員變量中徽惋。靜態(tài)的 ArrayMap
緩存每一個 SP 文件案淋,而每個 SP 文件內容通過 Map 緩存鍵值對數據,這樣數據會一直留在內存中险绘,浪費內存哎迄。
apply()
方法雖然是異步的,仍可能會發(fā)生 ANR
apply
異步提交解決了線程的阻塞問題隆圆,但如果 apply
任務過多數據量過大,可能會導致ANR
的產生翔烁。
apply()
方法不是異步的嗎渺氧,為什么還會造成 ANR 呢?apply()
方法本身沒有問題蹬屹,但是當生命周期處于 handleStopService()
侣背、 handlePauseActivity()
白华、 handleStopActivity()
的時候會一直等待 apply()
方法將數據保存成功,否則會一直等待贩耐,從而阻塞主線程造成 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添加到隊列中
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);
}
這里關鍵點是會將 awaitCommit
加入到 QueuedWork
隊列中,只有當 awaitCommit
執(zhí)行完之后才會進行移除潮太。
另一方面管搪,在 Activity
和 Service
的 handleStopService()
、 handlePauseActivity()
铡买、 handleStopActivity()
中會等待 QueuedWork
中的任務全部完成更鲁,一旦 QueuedWork
中的任務非常耗時,例如 SP 的寫入磁盤數據量過多奇钞,就會導致主線程長時間未響應澡为,從而產生 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()) {
//等待任務完成
QueuedWork.waitToFinish();
}
mSomeActivitiesChanged = true;
}
}
SharedPreference 不能跨進程通信
SP 是不能跨進程通信的,雖然在獲取 SP 時提供了MODE_MULTI_PROCESS
景埃,但內部并不是用來跨進程的媒至。
public SharedPreferences getSharedPreferences(File file, int mode) {
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
// 重新讀取SP文件內容
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
在這里使用 MODE_MULTI_PROCESS
只是重新讀取一遍文件而已,并不能保證跨進程通信谷徙。
apply()
方法沒有結果回調
為了防止 SP 寫入時阻塞線程拒啰,一般都會使用 apply
方法來將數據異步寫入到文件中,但它無法有返回值蒂胞,也沒有對應的結果回調图呢,所以無法得知此次寫入結果是成功還是失敗。
DataStore 有哪些改進
針對 SP 的幾個問題骗随,DataStore
都夠能規(guī)避蛤织。
-
DataStore
內部使用kotlin
協程通過掛起的方式來避免阻塞線程,避免產生 ANR鸿染。 -
DataStore
不僅支持 SP 同時還支持protocol buffers
類型的存儲指蚜,protocol buffers
是可以保證數據類型安全的。 -
DataStore
能夠在編譯階段提醒 SP 類型錯誤涨椒,減少寫代碼時的失誤導致類型不安全問題摊鸡。
-
DataStore
使用Flow
來獲取數據,每次保存數據之后都會通知最近的Flow
蚕冬,可以獲得到操作成功或失敗的結果免猾。 -
DataStore
完美支持 SP 數據的遷移,可以無成本過渡到DataStore
囤热。
對比圖
SharedPreferences
猎提、DataStore
、MMKV
的對比:
DataStore 的使用和遷移
Preferences DataStore
添加依賴
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01"
構建 DataStore
private val PREFERENCE_NAME = "DataStore"
var dataStore: DataStore<Preferences> = context.createDataStore(
name = PREFERENCE_NAME
存儲位置為 data/data/包名/files/datastore/ + PREFERENCE_NAME + .preferences_pb
讀取數據
注意:
Preferences DataStore
只支持Int
,Long
,Boolean
,Float
,String
這幾種鍵值對數據旁蔼。
val KEY_BYTE_CODE = preferencesKey<Boolean>("ByteCode")
fun readData(key: Preferences.Key<Boolean>): Flow<Boolean> =
dataStore.data
.map { preferences ->
preferences[key] ?: false
}
dataStore.data
會返回一個 Flow<T>
锨苏,每當數據變化的時候都會重新發(fā)出疙教。
寫入數據
suspend fun saveData(key: Preferences.Key<Boolean>) {
dataStore.edit { mutablePreferences ->
val value = mutablePreferences[key] ?: false
mutablePreferences[key] = !value
}
}
通過 DataStore.edit()
寫入數據的,DataStore.edit()
是一個 suspend
函數伞租,所以只能在協程體內使用贞谓。
從 SharedPreferences 遷移
遷移 SharedPreferences
到 DataStore
只需要 2 步。
- 構建
DataStore
的時候葵诈,需要傳入一個SharedPreferencesMigration
dataStore = context.createDataStore(
name = PREFERENCE_NAME,
migrations = listOf(
SharedPreferencesMigration(
context,
SharedPreferencesRepository.PREFERENCE_NAME
)
)
)
- 當
DataStore
對象構建完了之后裸弦,需要執(zhí)行一次讀取或者寫入操作,即可完成SharedPreferences
遷移到DataStore
驯击,當遷移成功之后烁兰,會自動刪除SharedPreferences
使用的文件。
注意: 只從 SharedPreferences
遷移一次徊都,因此一旦遷移成功之后沪斟,應該停止使用 SharedPreferences
。
Proto DataStore
Protocol Buffers
:是 Google 開源的跨語言編碼協議暇矫,可以應用到 C++
主之、C#
、Dart
李根、Go
槽奕、Java
、Python
等等語言房轿,Google 內部幾乎所有 RPC 都在使用這個協議粤攒,使用了二進制編碼壓縮,體積更小囱持,速度比 JSON 更快夯接,但是缺點是犧牲了可讀性。
Proto DataStore
通過 protocol buffers
將對象序列化存儲在本地纷妆,比起 Preference DataStore
支持更多類型盔几,使用二進制編碼壓縮,體積更小速度更快掩幢。使用 Proto DataStore
需要先引入 protocol buffers
逊拍。
本文只對 Proto DataStore
做簡單介紹。
添加依賴
// Proto DataStore
implementation "androidx.datastore:datastore-core:1.0.0-alpha01"
// protobuf
implementation "com.google.protobuf:protobuf-javalite:3.10.0"
當添加完依賴之后需要新建 proto 文件际邻,在本文示例項目中新建了一個 common-protobuf
模塊芯丧,將新建的 person.proto 文件,放到了 common-protobuf
模塊 src/main/proto
目錄下世曾。
在 common-protobuf 模塊注整,build.gradle 文件內,添加以下依賴:
implementation "com.google.protobuf:protobuf-javalite:3.10.0"
新建 Person.proto
文件,添加以下內容
syntax = "proto3";
option java_package = "com.hi.dhl.datastore.protobuf";
option java_outer_classname = "PersonProtos";
message Person {
// 格式:字段類型 + 字段名稱 + 字段編號
string name = 1;
}
執(zhí)行 protoc 肿轨,編譯 proto 文件
protoc --java_out=./src/main/java -I=./src/main/proto ./src/main/proto/*.proto
構建 DataStore
object PersonSerializer : Serializer<PersonProtos.Person> {
override fun readFrom(input: InputStream): PersonProtos.Person {
try {
return PersonProtos.Person.parseFrom(input) // 是編譯器自動生成的,用于讀取并解析 input 的消息
} catch (exception: Exception) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override fun writeTo(t: PersonProtos.Person, output: OutputStream) = t.writeTo(output) // t.writeTo(output) 是編譯器自動生成的蕊程,用于寫入序列化消息
}
讀取數據
fun readData(): Flow<PersonProtos.Person> {
return protoDataStore.data
.catch {
if (it is IOException) {
it.printStackTrace()
emit(PersonProtos.Person.getDefaultInstance())
} else {
throw it
}
}
寫入數據
suspend fun saveData(personModel: PersonModel) {
protoDataStore.updateData { person ->
person.toBuilder().setAge(personModel.age).setName(personModel.name).build()
}
}
從 SharedPreferences
遷移
創(chuàng)建映射關系
構建 DataStore 并傳入 shardPrefsMigration
執(zhí)行一次讀取或者寫入操作
SuperApp引入
SuperApp 當前使用 SP 實現小數據存取椒袍,具體由 IPCConfig
工具類封裝 SP 提供靜態(tài)方法供各處使用。鑒于 DataStore
的各項改進及遷移非常方便藻茂,可以考慮從 SP 遷移到 DataStore
驹暑。
Proto DataStore
雖然有更多優(yōu)勢,但需要引入Protocol Buffers
辨赐,同時開發(fā)者需要如 proto
語法等更多的學習成本优俘,使用和遷移也會稍微麻煩些∠菩颍考慮到現在暫時沒有 Proto DataStore
對應的使用場景帆焕,可以先遷移到 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 {
// 當讀取數據遇到錯誤時叶雹,如果是IOException異常,發(fā)送一個emptyPreferences重新使用
// 但是如果是其他的異常换吧,最好將它拋出去折晦,不要隱藏問題
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
}
}
}
}
遷移前后文件結構:
測試可正常使用。
這樣修改可以只改變一個文件沾瓦,各調用處無需變動满着,就完成到 Preferences DataStore
的遷移,但是 get
方法都是 runBlocking
同步方法贯莺,沒有使用到 DataStore
的全部功能风喇。這里只是為了簡單驗證下遷移的可行性和便捷性,后續(xù)可以繼續(xù)優(yōu)化充分利用好 DataStore
的優(yōu)勢乖篷。