安卓 sharedpreferences可以被其它activity读取_Google|再见 SharedPreferences 拥抱 Jetpack DataStore...
Google 新增加了一個新 Jetpack 的成員 DataStore,主要用來替換 SharedPreferences, DataStore 應(yīng)該是開發(fā)者期待已久的庫,DataStore 是基于 Flow 實現(xiàn)的,一種新的數(shù)據(jù)存儲方案,它提供了兩種實現(xiàn)方式:
- Proto DataStore:存儲類的對象(typed objects ),通過 protocol buffers 將對象序列化存儲在本地,protocol buffers 現(xiàn)在已經(jīng)應(yīng)用的非常廣泛,無論是微信還是阿里等等大廠都在使用,我們在部分業(yè)務(wù)場景中也用到了 protocol buffers,會在后續(xù)的文章詳細(xì)分析
- Preferences DataStore:以鍵值對的形式存儲在本地和 SharedPreferences 類似,但是 DataStore 是基于 Flow 實現(xiàn)的,不會阻塞主線程,并且保證類型安全
Jetpack DataStore 將會分為至少 2 篇文章來分析,今天這篇文章主要來介紹 Jetpack DataStore 其中一種實現(xiàn)方式 Preferences DataStore,文章中的示例代碼,已經(jīng)上傳到 GitHub 歡迎前去查看 AndroidX-Jetpack-Practice/DataStoreSimple。
GitHub 地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice
這篇文章會涉及到 Koltin flow 相關(guān)內(nèi)容,如果不了解可以先去看另外一篇文章 Kotlin Flow 是什么?Channel 是什么?
通過這篇文章你將學(xué)習(xí)到以下內(nèi)容:
- 那些年我們所經(jīng)歷的 SharedPreferences 坑?
- 為什么需要 DataStore?它為我們解決了什么問題?
- 如何在項目中使用 DataStore?
- 如何遷移 SharedPreferences 到 DataStore?
- MMKV、DataStore、SharedPreferences 的不同之處?
一個新庫的出現(xiàn)必定為我們解決了一些問題,那么 Jetpack DataStore 為我們解決什么問題呢,在分析之前,我們需要先來了解 SharedPreferences 都有那些坑。
那些年我們所經(jīng)歷的 SharedPreferences 坑
SharedPreference 是一個輕量級的數(shù)據(jù)存儲方式,使用起來也非常方便,以鍵值對的形式存儲在本地,初始化 SharedPreference 的時候,會將整個文件內(nèi)容加載內(nèi)存中,因此會帶來以下問題:
- 通過 getXXX() 方法獲取數(shù)據(jù),可能會導(dǎo)致主線程阻塞
- SharedPreference 不能保證類型安全
- SharedPreference 加載的數(shù)據(jù)會一直留在內(nèi)存中,浪費(fèi)內(nèi)存
- apply() 方法雖然是異步的,可能會發(fā)生 ANR,在 8.0 之前和 8.0 之后實現(xiàn)各不相同
- apply() 方法無法獲取到操作成功或者失敗的結(jié)果
接下來我們逐個來分析一下 SharedPreferences 帶來的這些問題,在文章中 SharedPreference 簡稱 SP。
getXXX() 方法可能會導(dǎo)致主線程阻塞
所有 getXXX() 方法都是同步的,在主線程調(diào)用 get 方法,必須等待 SP 加載完畢,會導(dǎo)致主線程阻塞,下面的代碼,我相信小伙伴們并不陌生。
val sp = getSharedPreferences("ByteCode", Context.MODE_PRIVATE) // 異步加載 SP 文件內(nèi)容 sp.getString("jetpack", ""); // 等待 SP 加載完畢調(diào)用 getSharedPreferences() 方法,最終會調(diào)用 SharedPreferencesImpl#startLoadFromDisk() 方法開啟一個線程異步讀取數(shù)據(jù)。 frameworks/base/core/java/android/app/SharedPreferencesImpl.java
private final Object mLock = new Object(); private boolean mLoaded = false; private void startLoadFromDisk() {synchronized (mLock) {mLoaded = false;}new Thread("SharedPreferencesImpl-load") {public void run() {loadFromDisk();}}.start(); }正如你所看到的,開啟一個線程異步讀取數(shù)據(jù),當(dāng)我們正在讀取一個比較大的數(shù)據(jù),還沒讀取完,接著調(diào)用 getXXX() 方法。
public String getString(String key, @Nullable String defValue) {synchronized (mLock) {awaitLoadedLocked();String v = (String)mMap.get(key);return v != null ? v : defValue;} }private void awaitLoadedLocked() {......while (!mLoaded) {try {mLock.wait();} catch (InterruptedException unused) {}}...... }在同步方法內(nèi)調(diào)用了 wait() 方法,會一直等待 getSharedPreferences() 方法開啟的線程讀取完數(shù)據(jù)才能繼續(xù)往下執(zhí)行,如果讀取幾 KB 的數(shù)據(jù)還好,假設(shè)讀取一個大的文件,勢必會造成主線程阻塞。
SP 不能保證類型安全
調(diào)用 getXXX() 方法的時候,可能會出現(xiàn) ClassCastException 異常,因為使用相同的 key 進(jìn)行操作的時候,putXXX 方法可以使用不同類型的數(shù)據(jù)覆蓋掉相同的 key。
val key = "jetpack" val sp = getSharedPreferences("ByteCode", Context.MODE_PRIVATE) // 異步加載 SP 文件內(nèi)容sp.edit { putInt(key, 0) } // 使用 Int 類型的數(shù)據(jù)覆蓋相同的 key sp.getString(key, ""); // 使用相同的 key 讀取 Sting 類型的數(shù)據(jù)使用 Int 類型的數(shù)據(jù)覆蓋掉相同的 key,然后使用相同的 key 讀取 Sting 類型的數(shù)據(jù),編譯正常,但是運(yùn)行會出現(xiàn)以下異常。
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.StringSP 加載的數(shù)據(jù)會一直留在內(nèi)存中
通過 getSharedPreferences() 方法加載的數(shù)據(jù),最后會將數(shù)據(jù)存儲在靜態(tài)的成員變量中。
// 調(diào)用 getSharedPreferences 方法,最后會調(diào)用 getSharedPreferencesCacheLocked 方法 public SharedPreferences getSharedPreferences(File file, int mode) {......final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();return sp; }// 通過靜態(tài)的 ArrayMap 緩存 SP 加載的數(shù)據(jù) private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;// 將數(shù)據(jù)保存在 sSharedPrefsCache 中 private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {......ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);if (packagePrefs == null) {packagePrefs = new ArrayMap<>();sSharedPrefsCache.put(packageName, packagePrefs);}return packagePrefs; }通過靜態(tài)的 ArrayMap 緩存每一個 SP 文件,而每個 SP 文件內(nèi)容通過 Map 緩存鍵值對數(shù)據(jù),這樣數(shù)據(jù)會一直留在內(nèi)存中,浪費(fèi)內(nèi)存。
apply() 方法是異步的,可能會發(fā)生 ANR
apply() 方法是異步的,為什么還會造成 ANR 呢?曾今的字節(jié)跳動就出現(xiàn)過這個問題,具體詳情可以點擊這里前去查看 剖析 SharedPreference apply 引起的 ANR 問題 而且 Google 也明確指出了 apply() 的問題。
簡單總結(jié)一下:apply() 方法是異步的,本身是不會有任何問題,但是當(dāng)生命周期處于 handleStopService() 、 handlePauseActivity() 、 handleStopActivity() 的時候會一直等待 apply() 方法將數(shù)據(jù)保存成功,否則會一直等待,從而阻塞主線程造成 ANR,一起來分析一下為什么異步方法還會阻塞主線程,先來看看 apply() 方法的實現(xiàn)。 frameworks/base/core/java/android/app/SharedPreferencesImpl.java
public void apply() {final long startTime = System.currentTimeMillis();final MemoryCommitResult mcr = commitToMemory();final Runnable awaitCommit = new Runnable() {@Overridepublic void run() {mcr.writtenToDiskLatch.await(); // 等待......}};// 將 awaitCommit 添加到隊列 QueuedWork 中QueuedWork.addFinisher(awaitCommit);Runnable postWriteRunnable = new Runnable() {@Overridepublic void run() {awaitCommit.run();QueuedWork.removeFinisher(awaitCommit);}};// 8.0 之前加入到一個單線程的線程池中執(zhí)行// 8.0 之后加入 HandlerThread 中執(zhí)行寫入任務(wù)SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); }- 將一個 awaitCommit 的 Runnable 任務(wù),添加到隊列 QueuedWork 中,在 awaitCommit 中會調(diào)用 await() 方法等待,在 handleStopService 、 handleStopActivity 等等生命周期會以這個作為判斷條件,等待任務(wù)執(zhí)行完畢
- 將一個 postWriteRunnable 的 Runnable 寫任務(wù),通過 enqueueDiskWrite 方法,將寫入任務(wù)加入到隊列中,而寫入任務(wù)在一個線程中執(zhí)行
注意:在 8.0 之前和 8.0 之后 enqueueDiskWrite() 方法實現(xiàn)邏輯各不相同
在 8.0 之前調(diào)用 enqueueDiskWrite() 方法,將寫入任務(wù)加入到 單個線程的線程池 中執(zhí)行,如果 apply() 多次的話,任務(wù)將會依次執(zhí)行,效率很低,android-7.0.0_r34 源碼如下所示。
// android-7.0.0_r34: frameworks/base/core/java/android/app/SharedPreferencesImpl.java private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {......QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable); }// android-7.0.0_r34: frameworks/base/core/java/android/app/QueuedWork.java public static ExecutorService singleThreadExecutor() {synchronized (QueuedWork.class) {if (sSingleThreadExecutor == null) {sSingleThreadExecutor = Executors.newSingleThreadExecutor();}return sSingleThreadExecutor;} }通過 Executors.newSingleThreadExecutor() 方法創(chuàng)建了一個 單個線程的線程池,因此任務(wù)是串行的,通過 apply() 方法創(chuàng)建的任務(wù),都會添加到這個線程池內(nèi)。
在 8.0 之后將寫入任務(wù)加入到 LinkedList 鏈表中,在 HandlerThread 中執(zhí)行寫入任務(wù),android-10.0.0_r14 源碼如下所示。
// android-10.0.0_r14: frameworks/base/core/java/android/app/SharedPreferencesImpl.java private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {......QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); }// android-10.0.0_r14: frameworks/base/core/java/android/app/QueuedWork.javaprivate static final LinkedList<Runnable> sWork = new LinkedList<>();public static void queue(Runnable work, boolean shouldDelay) {Handler handler = getHandler(); // 獲取 handlerThread.getLooper() 生成 Handler 對象synchronized (sLock) {sWork.add(work); // 將寫入任務(wù)加入到 LinkedList 鏈表中if (shouldDelay && sCanDelay) {handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);} else {handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);}} }在 8.0 之后通過調(diào)用 handlerThread.getLooper() 方法生成 Handler,任務(wù)都會在 HandlerThread 中執(zhí)行,所有通過 apply() 方法創(chuàng)建的任務(wù),都會添加到 LinkedList 鏈表中。
當(dāng)生命周期處于 handleStopService() 、 handlePauseActivity() 、 handleStopActivity() 的時候會調(diào)用 QueuedWork.waitToFinish() 會等待寫入任務(wù)執(zhí)行完畢,我們以其中 handlePauseActivity() 方法為例。
public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,int configChanges, PendingTransactionActions pendingActions, String reason) {......// 確保寫任務(wù)都已經(jīng)完成QueuedWork.waitToFinish();......} }正如你所看到的在 handlePauseActivity() 方法中,調(diào)用了 QueuedWork.waitToFinish() 方法,會等待所有的寫入執(zhí)行完畢,Google 在 8.0 之后對這個方法做了很大的優(yōu)化,一起來看一下 8.0 之前和 8.0 之后的區(qū)別。
注意:在 8.0 之前和 8.0 之后 waitToFinish() 方法實現(xiàn)邏輯各不相同
在 8.0 之前 waitToFinish() 方法只做了一件事,會一直等待寫入任務(wù)執(zhí)行完畢,我先來看看在 android-7.0.0_r34 源碼實現(xiàn)。
android-7.0.0_r34: frameworks/base/core/java/android/app/QueuedWork.java
private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers =new ConcurrentLinkedQueue<Runnable>();public static void waitToFinish() {Runnable toFinish;while ((toFinish = sPendingWorkFinishers.poll()) != null) {toFinish.run(); // 相當(dāng)于調(diào)用 `mcr.writtenToDiskLatch.await()` 方法} }- sPendingWorkFinishers 是 ConcurrentLinkedQueue 實例,apply 方法會將寫入任務(wù)添加到 sPendingWorkFinishers 隊列中,在 單個線程的線程池 中執(zhí)行寫入任務(wù),線程的調(diào)度并不由程序來控制,也就是說當(dāng)生命周期切換的時候,任務(wù)不一定處于執(zhí)行狀態(tài)
- toFinish.run() 方法,相當(dāng)于調(diào)用 mcr.writtenToDiskLatch.await() 方法,會一直等待
- waitToFinish() 方法就做了一件事,會一直等待寫入任務(wù)執(zhí)行完畢,其它什么都不做,當(dāng)有很多寫入任務(wù),會依次執(zhí)行,當(dāng)文件很大時,效率很低,造成 ANR 就不奇怪了,尤其像字節(jié)跳動這種大規(guī)模的 App
在 8.0 之后 waitToFinish() 方法做了很大的優(yōu)化,當(dāng)生命周期切換的時候,會主動觸發(fā)任務(wù)的執(zhí)行,而不是一直在等著,我們來看看 android-10.0.0_r14 源碼實現(xiàn)。
android-10.0.0_r14: frameworks/base/core/java/android/app/QueuedWork.java
private static final LinkedList<Runnable> sFinishers = new LinkedList<>(); public static void waitToFinish() {......try {processPendingWork(); // 主動觸發(fā)任務(wù)的執(zhí)行} finally {StrictMode.setThreadPolicy(oldPolicy);}try {// 等待任務(wù)執(zhí)行完畢while (true) {Runnable finisher;synchronized (sLock) {finisher = sFinishers.poll(); // 從 LinkedList 中取出任務(wù)}if (finisher == null) { // 當(dāng) LinkedList 中沒有任務(wù)時會跳出循環(huán)break;}finisher.run(); // 相當(dāng)于調(diào)用 `mcr.writtenToDiskLatch.await()`}} ...... }在 waitToFinish() 方法中會主動調(diào)用 processPendingWork() 方法觸發(fā)任務(wù)的執(zhí)行,在 HandlerThread 中執(zhí)行寫入任務(wù)。
另外還做了一個很重要的優(yōu)化,當(dāng)調(diào)用 apply() 方法的時候,執(zhí)行磁盤寫入,都是全量寫入,在 8.0 之前,調(diào)用 N 次 apply() 方法,就會執(zhí)行 N 次磁盤寫入,在 8.0 之后,apply() 方法調(diào)用了多次,只會執(zhí)行最后一次寫入,通過版本號來控制的。
SharedPreferences 的另外一個缺點就是 apply() 方法無法獲取到操作成功或者失敗的結(jié)果,而 commit() 方法是可以接收 MemoryCommitResult 里面的一個 boolean 參數(shù)作為結(jié)果,來看一下它們的方法簽名。
public void apply() { ... }public boolean commit() { ... }SP 不能用于跨進(jìn)程通信
我們在創(chuàng)建 SP 實例的時候,需要傳入一個 mode,如下所示:
val sp = getSharedPreferences("ByteCode", Context.MODE_PRIVATE)Context 內(nèi)部還有一個 mode 是 MODE_MULTI_PROCESS,我們來看一下這個 mode 做了什么
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; }在這里就做了一件事,當(dāng)遇到 MODE_MULTI_PROCESS 的時候,會重新讀取 SP 文件內(nèi)容,并不能用 SP 來做跨進(jìn)程通信。
到這里關(guān)于 SharedPreferences 部分分析完了,接下來分析一下 DataStore 為我們解決什么問題?
DataStore 解決了什么問題
Preferences DataStore 主要用來替換 SharedPreferences,Preferences DataStore 解決了 SharedPreferences 帶來的所有問題
Preferences DataStore 相比于 SharedPreferences 優(yōu)點
- DataStore 是基于 Flow 實現(xiàn)的,所以保證了在主線程的安全性
- 以事務(wù)方式處理更新數(shù)據(jù),事務(wù)有四大特性(原子性、一致性、 隔離性、持久性)
- 沒有 apply() 和 commit() 等等數(shù)據(jù)持久的方法
- 自動完成 SharedPreferences 遷移到 DataStore,保證數(shù)據(jù)一致性,不會造成數(shù)據(jù)損壞
- 可以監(jiān)聽到操作成功或者失敗結(jié)果
另外 Jetpack DataStore 提供了 Proto DataStore 方式,用于存儲類的對象(typed objects ),通過 protocol buffers 將對象序列化存儲在本地,protocol buffers 現(xiàn)在已經(jīng)應(yīng)用的非常廣泛,無論是微信還是阿里等等大廠都在使用,我們在部分場景中也使用了 protocol buffers,在后續(xù)的文章會詳細(xì)的分析。
注意:
Preferences DataStore 只支持 Int , Long , Boolean , Float , String 鍵值對數(shù)據(jù),適合存儲簡單、小型的數(shù)據(jù),并且不支持局部更新,如果修改了其中一個值,整個文件內(nèi)容將會被重新序列化,可以運(yùn)行 AndroidX-Jetpack-Practice/DataStoreSimple 體驗一下,如果需要局部更新,建議使用 Room。
在項目中使用 Preferences DataStore
Preferences DataStore 主要應(yīng)用在 MVVM 當(dāng)中的 Repository 層,在項目中使用 Preferences DataStore 非常簡單,只需要 4 步。
1. 需要添加 Preferences DataStore 依賴
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01"2. 構(gòu)建 DataStore
private val PREFERENCE_NAME = "DataStore" var dataStore: DataStore<Preferences> = context.createDataStore(name = PREFERENCE_NAME3. 從 Preferences DataStore 中讀取數(shù)據(jù)
Preferences DataStore 以鍵值對的形式存儲在本地,所以首先我們應(yīng)該定義一個 Key.
val KEY_BYTE_CODE = preferencesKey<Boolean>("ByteCode")這里和我們之前使用 SharedPreferences 的有點不一樣,在 Preferences DataStore 中 Key 是一個 Preferences.Key<T> 類型,只支持 Int , Long , Boolean , Float , String,源碼如下所示:
inline fun <reified T : Any> preferencesKey(name: String): Preferences.Key<T> {return when (T::class) {Int::class -> {Preferences.Key<T>(name)}String::class -> {Preferences.Key<T>(name)}Boolean::class -> {Preferences.Key<T>(name)}Float::class -> {Preferences.Key<T>(name)}Long::class -> {Preferences.Key<T>(name)}...... // 如果是其他類型就會拋出異常} }當(dāng)我們定義好 Key 之后,就可以通過 dataStore.data 來獲取數(shù)據(jù)
override fun readData(key: Preferences.Key<Boolean>): Flow<Boolean> =dataStore.data.catch {// 當(dāng)讀取數(shù)據(jù)遇到錯誤時,如果是 `IOException` 異常,發(fā)送一個 emptyPreferences 來重新使用// 但是如果是其他的異常,最好將它拋出去,不要隱藏問題if (it is IOException) {it.printStackTrace()emit(emptyPreferences())} else {throw it}}.map { preferences ->preferences[key] ?: false}- Preferences DataStore 是基于 Flow 實現(xiàn)的,所以通過 dataStore.data 會返回一個 Flow<T>,每當(dāng)數(shù)據(jù)變化的時候都會重新發(fā)出
- catch 用來捕獲異常,當(dāng)讀取數(shù)據(jù)出現(xiàn)異常時會拋出一個異常,如果是 IOException 異常,會發(fā)送一個 emptyPreferences() 來重新使用,如果是其他異常,最好將它拋出去
4. 向 Preferences DataStore 中寫入數(shù)據(jù)
在 Preferences DataStore 中是通過 DataStore.edit() 寫入數(shù)據(jù)的,DataStore.edit() 是一個 suspend 函數(shù),所以只能在協(xié)程體內(nèi)使用,每當(dāng)遇到 suspend 函數(shù)以掛起的方式運(yùn)行,并不會阻塞主線程。
以掛起的方式運(yùn)行,不會阻塞主線程 :也就是協(xié)程作用域被掛起, 當(dāng)前線程中協(xié)程作用域之外的代碼不會阻塞。
首先我們需要創(chuàng)建一個 suspend 函數(shù),然后調(diào)用 DataStore.edit() 寫入數(shù)據(jù)即可。
override suspend fun saveData(key: Preferences.Key<Boolean>) {dataStore.edit { mutablePreferences ->val value = mutablePreferences[key] ?: falsemutablePreferences[key] = !value} }到這里關(guān)于 Preferences DataStore 讀取數(shù)據(jù)和寫入數(shù)據(jù)就已經(jīng)分析完了,接下來分析一下如何遷移 SharedPreferences 到 DataStore。
遷移 SharedPreferences 到 DataStore
遷移 SharedPreferences 到 DataStore 只需要 2 步。
- 在構(gòu)建 DataStore 的時候,需要傳入一個 SharedPreferencesMigration
- 當(dāng) DataStore 對象構(gòu)建完了之后,需要執(zhí)行一次讀取或者寫入操作,即可完成 SharedPreferences 遷移到 DataStore,當(dāng)遷移成功之后,會自動刪除 SharedPreferences 使用的文件
注意: 只從 SharedPreferences 遷移一次,因此一旦遷移成功之后,應(yīng)該停止使用 SharedPreferences。
相比于 MMKV 有什么不同之處
最后用一張表格來對比一下 MMKV、DataStore、SharedPreferences 的不同之處,如果發(fā)現(xiàn)錯誤,或者有其他不同之處,期待你來一起完善。
另外在附上一張 Google 分析的 SharedPreferences 和 DataStore 的區(qū)別
全文到這里就結(jié)束了,這篇文章主要分析了 SharedPreferences 和 DataStore 的優(yōu)缺點,以及為什么需要引入 DataStore 和如何使用 DataStore,為了節(jié)省篇幅源碼分析部分會在后續(xù)的文章中分析。
關(guān)于 SharedPreferences 和 DataStore 相關(guān)的代碼,已經(jīng)上傳到了 GitHub 歡迎前去查看 AndroidX-Jetpack-Practice/DataStoreSimple ,可以運(yùn)行一下示例項目,體驗一下 SharedPreferences 和 DataStore 效果。
- GitHub 地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice
參考文獻(xiàn)
- Preferences DataStore codelab
- Now in Android #25
- Prefer Storing Data with Jetpack DataStore
- 剖析 SharedPreference 引起的 ANR 問題
- SharedPreferences 問題分析和解決
結(jié)語
我梳理了 LeetCode / 劍指 offer 及國內(nèi)外大廠面試題解,截止到目前為止我已經(jīng)在 LeetCode 上 AC 了 124+ 題,每題都會用 Java 和 kotlin 去實現(xiàn),并且每題都有多種解法、解題思路、時間復(fù)雜度、空間復(fù)雜度分析,題庫逐漸完善中,歡迎前去查看。
- 劍指 offer 及國內(nèi)外大廠面試題解:在線閱讀
- LeetCode 系列題解:在線閱讀
最后推薦我一直在更新維護(hù)的項目和網(wǎng)站:
- 計劃建立一個最全、最新的 AndroidX Jetpack 相關(guān)組件的實戰(zhàn)項目 以及 相關(guān)組件原理分析文章,正在逐漸增加 Jetpack 新成員,倉庫持續(xù)更新,歡迎前去查看:
- LeetCode / 劍指 Offer / 國內(nèi)外大廠面試題,涵蓋: 多線程、數(shù)組、棧、隊列、字符串、鏈表、樹,查找算法、搜索算法、位運(yùn)算、排序等等,每道題目都會用 Java 和 kotlin 去實現(xiàn),倉庫持續(xù)更新,歡迎前去查看
- 劍指 offer 及國內(nèi)外大廠面試題解:在線閱讀
- LeetCode 系列題解:在線閱讀
- 最新 Android 10 源碼分析系列文章,了解系統(tǒng)源碼,不僅有助于分析問題,在面試過程中,對我們也是非常有幫助的,倉庫持續(xù)更新,歡迎前去查看 Android10-Source-Analysis
- 整理和翻譯一系列精選國外的技術(shù)文章,每篇文章都會有譯者思考部分,對原文的更加深入的解讀,倉庫持續(xù)更新,歡迎前去查看
- 「為互聯(lián)網(wǎng)人而設(shè)計,國內(nèi)國外名站導(dǎo)航」涵括新聞、體育、生活、娛樂、設(shè)計、產(chǎn)品、運(yùn)營、前端開發(fā)、Android 開發(fā)等等網(wǎng)址,歡迎前去查看
總結(jié)
以上是生活随笔為你收集整理的安卓 sharedpreferences可以被其它activity读取_Google|再见 SharedPreferences 拥抱 Jetpack DataStore...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 定时器和promise_手写Promis
- 下一篇: qt 定时器_Qt开源作品23-颜色拾取