DataStore

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)入SharedPreferencesImplloadFromDisk方法:

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)行移除。

另一方面,在 ActivityServicehandleStopService() 、 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ì)比圖

SharedPreferencesDataStore、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 遷移

遷移 SharedPreferencesDataStore 只需要 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ì)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • DataStore出現(xiàn)的原因 Jetpack DataStore is a data storage soluti...
    不做android閱讀 1,587評(píng)論 1 0
  • 今天感恩節(jié)哎,感謝一直在我身邊的親朋好友。感恩相遇!感恩不離不棄。 中午開(kāi)了第一次的黨會(huì),身份的轉(zhuǎn)變要...
    余生動(dòng)聽(tīng)閱讀 10,805評(píng)論 0 11
  • 彩排完,天已黑
    劉凱書(shū)法閱讀 4,467評(píng)論 1 3
  • 沒(méi)事就多看看書(shū),因?yàn)楦褂性?shī)書(shū)氣自華,讀書(shū)萬(wàn)卷始通神。沒(méi)事就多出去旅游,別因?yàn)闆](méi)錢而找借口,因?yàn)橹灰闶〕詢€用,來(lái)...
    向陽(yáng)之心閱讀 4,970評(píng)論 3 11
  • 表情是什么,我認(rèn)為表情就是表現(xiàn)出來(lái)的情緒。表情可以傳達(dá)很多信息。高興了當(dāng)然就笑了,難過(guò)就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 129,509評(píng)論 2 7

友情鏈接更多精彩內(nèi)容