Просмотр исходного кода

feat: SP 写路径退役,DataStore 成为唯一持久化真源

PR-4: putPref*/removePref 删 SP edit(保留 !isInitialized 兜底),
PrefDelegate 同步/异步合并,删除 postMigrationSync,restart 加 flush,
DsSync 增加 awaitPendingWrites 与耗时观测。

- ContextExtensions: putPref*/removePref 主路径仅写 AppConfigStore
  (!isInitialized 时走 SP 兜底),putPrefStringSync 退化为 putPrefString
- PrefDelegate: 删除 sync 分支区分,String? 统一走 putPrefString
- SettingsRepository: 删除 postMigrationSync(DS→SP 回灌)、remove 中的 removePref
- App.kt: 删除 postMigrationSync 调用
- ContextExtensions.restart(): 添加 DsSync.awaitPendingWrites() 确保落盘
- DsSync: 新增 awaitPendingWrites(timeoutMs=3000)、pendingWriteCount、
  launchWrite 耗时观测(>500ms 记日志)
5151561 3 месяцев назад
Родитель
Сommit
eceff51298

+ 0 - 5
app/src/main/java/io/legado/app/App.kt

@@ -34,7 +34,6 @@ import io.legado.app.data.entities.rule.BookInfoRule
 import io.legado.app.data.entities.rule.ContentRule
 import io.legado.app.data.entities.rule.ExploreRule
 import io.legado.app.data.entities.rule.SearchRule
-import io.legado.app.data.repository.SettingsRepository
 import io.legado.app.di.appDatabaseModule
 import io.legado.app.di.appModule
 import io.legado.app.help.AppFreezeMonitor
@@ -143,10 +142,6 @@ class App : Application(), ImageLoaderFactory {
             LogUtils.init(this@App)
             LogUtils.d("App", "onCreate")
             LogUtils.logDeviceInfo()
-            // 确保 DataStore 迁移后 SP 中的主题配置值未丢失
-            kotlin.runCatching {
-                get<SettingsRepository>().postMigrationSync()
-            }
             //预下载Cronet so
             Cronet.preDownload()
             createNotificationChannels()

+ 0 - 42
app/src/main/java/io/legado/app/data/repository/SettingsRepository.kt

@@ -16,11 +16,8 @@ import androidx.datastore.preferences.core.stringPreferencesKey
 import androidx.datastore.preferences.core.stringSetPreferencesKey
 import androidx.datastore.preferences.preferencesDataStore
 import io.legado.app.constant.PreferKey
-import io.legado.app.utils.defaultSharedPreferences
-import io.legado.app.utils.removePref
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.catch
-import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.map
 import java.io.IOException
 
@@ -161,44 +158,6 @@ class SettingsRepository(private val context: Context) {
         }
     }
 
-    /**
-     * DataStore 迁移后同步校验:确保 SP 中的主题配置值未被迁移过程丢失。
-     *
-     * SharedPreferencesMigration 在首次访问 DataStore 时运行,将 SP 值复制到 DataStore。
-     * 在某些场景下(进程被杀、ROM 行为等),迁移可能导致 SP 中的值丢失。
-     * 此方法从 DataStore 读取关键主题配置,如果 SP 中缺失则补写回去。
-     */
-    suspend fun postMigrationSync() {
-        val prefs = dataStore.data.first()
-        val sp = context.defaultSharedPreferences
-        val themeKeys = listOf(
-            PreferKey.composeEngine,
-            PreferKey.appTheme,
-            PreferKey.themeMode,
-            PreferKey.paletteStyle,
-            PreferKey.materialVersion,
-            PreferKey.customContrast,
-            PreferKey.customMode,
-        )
-        var needsWrite = false
-        val editor = mutableMapOf<String, String>()
-        for (key in themeKeys) {
-            if (!sp.contains(key)) {
-                val dsValue = runCatching { prefs[stringPreferencesKey(key)] }.getOrNull()
-                if (dsValue != null) {
-                    editor[key] = dsValue
-                    needsWrite = true
-                }
-            }
-        }
-        if (needsWrite) {
-            sp.edit {
-                editor.forEach { (key, value) ->
-                    putString(key, value)
-                }
-            }
-        }
-    }
 
     // 移除配置
     suspend fun remove(key: String) {
@@ -209,6 +168,5 @@ class SettingsRepository(private val context: Context) {
             preferences.remove(longPreferencesKey(key))
             preferences.remove(floatPreferencesKey(key))
         }
-        context.removePref(key)
     }
 }

+ 39 - 7
app/src/main/java/io/legado/app/help/config/DsSync.kt

@@ -7,19 +7,22 @@ import androidx.datastore.preferences.core.intPreferencesKey
 import androidx.datastore.preferences.core.longPreferencesKey
 import androidx.datastore.preferences.core.stringPreferencesKey
 import io.legado.app.data.repository.dataStore
+import io.legado.app.utils.LogUtils
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeoutOrNull
 import splitties.init.appCtx
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.measureTime
 
 /**
- * DataStore 写入辅助对象。
- *
- * 让 [io.legado.app.ui.config.PrefDelegate] 在写入 SP 的同时同步写入 DataStore,
- * 保证两边数据一致,避免 DataStore 迁移/读取时出现值缺失。
+ * DataStore 写入序列化队列,所有经 [AppConfigStore] 的异步写入按提交顺序串行落盘。
  */
 object DsSync {
 
@@ -28,14 +31,43 @@ object DsSync {
     @OptIn(ExperimentalCoroutinesApi::class)
     private val writeDispatcher = Dispatchers.IO.limitedParallelism(1)
 
+    private val pendingCount = AtomicInteger(0)
+
+    /** 当前排队的写入任务数(含正在执行的那一个)。*/
+    val pendingWriteCount: Int get() = pendingCount.get()
+
     /**
      * 串行写队列:所有经 [AppConfigStore] 的写入按提交顺序落盘。
      * 写入失败仅吞掉异常(与旧 PrefDelegate 行为一致),此时值仍在内存快照中,重启后丢失。
      */
-    fun launchWrite(block: suspend () -> Unit): Job =
-        scope.launch(writeDispatcher) {
-            runCatching { block() }
+    fun launchWrite(block: suspend () -> Unit): Job {
+        pendingCount.incrementAndGet()
+        return scope.launch(writeDispatcher) {
+            try {
+                val elapsed = measureTime { runCatching { block() } }
+                if (elapsed > 500.milliseconds) {
+                    LogUtils.d(
+                        "DsSync",
+                        "单次 edit 耗时 ${elapsed.inWholeMilliseconds}ms, 队列剩余 ${pendingCount.get() - 1}"
+                    )
+                }
+            } finally {
+                pendingCount.decrementAndGet()
+            }
+        }
+    }
+
+    /**
+     * 等待所有排队的写入落盘完成(最长等待 [timeoutMs] 毫秒)。
+     * 用于 restart 等必须确保"写入已持久化"的场景。
+     */
+    suspend fun awaitPendingWrites(timeoutMs: Long = 3000) {
+        withContext(writeDispatcher) {
+            withTimeoutOrNull(timeoutMs) {
+                // 在串行 dispatcher 上执行一个空任务:排在前面的写入全部完成后才会轮到它
+            }
         }
+    }
 
     suspend fun putString(key: String, value: String?) {
         appCtx.dataStore.edit { prefs ->

+ 1 - 2
app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt

@@ -98,9 +98,8 @@ fun <T> prefDelegate(
         override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
             if (currentValue != value) {
                 updateValue(value)
-                // 门面双写:SP(向后兼容,未迁移代码仍直接读 SP/靠 SP 监听)+ 快照/DataStore
                 when (value) {
-                    is String? -> if (sync) appCtx.putPrefStringSync(key, value) else appCtx.putPrefString(key, value)
+                    is String? -> appCtx.putPrefString(key, value)
                     is Int -> appCtx.putPrefInt(key, value)
                     is Boolean -> appCtx.putPrefBoolean(key, value)
                     is Long -> appCtx.putPrefLong(key, value)

+ 41 - 20
app/src/main/java/io/legado/app/utils/ContextExtensions.kt

@@ -186,10 +186,8 @@ fun Context.startForegroundServiceCompat(intent: Intent) {
 val Context.defaultSharedPreferences: SharedPreferences
     get() = PreferenceManager.getDefaultSharedPreferences(this)
 
-// getPref*/putPref* 门面:读取走 AppConfigStore 内存快照(DataStore),
-// DS 缺值时回退 SP 并补写 DS(迁移遗漏补偿,过渡期逻辑,后续删除);
-// 写入仍保持 SP 双写(SP 监听器与直读 SP 的代码尚未迁移完),同时进快照保证写后立即读一致。
-// AppConfigStore 未初始化时(App.onCreate 之前,如 ContentProvider)直接走 SP 旧路径。
+// getPref*/putPref* 门面:读写统一走 AppConfigStore 内存快照(DataStore 唯一真源),
+// 主线程零 IO。AppConfigStore 未初始化时(App.onCreate 之前,如 ContentProvider)走 SP 旧路径。
 
 fun Context.getPrefBoolean(key: String, defValue: Boolean = false): Boolean {
     if (!AppConfigStore.isInitialized) return defaultSharedPreferences.getBoolean(key, defValue)
@@ -201,8 +199,11 @@ fun Context.getPrefBoolean(key: String, defValue: Boolean = false): Boolean {
 }
 
 fun Context.putPrefBoolean(key: String, value: Boolean = false) {
-    defaultSharedPreferences.edit { putBoolean(key, value) }
-    if (AppConfigStore.isInitialized) AppConfigStore.putBoolean(key, value)
+    if (!AppConfigStore.isInitialized) {
+        defaultSharedPreferences.edit { putBoolean(key, value) }
+        return
+    }
+    AppConfigStore.putBoolean(key, value)
 }
 
 fun Context.getPrefInt(key: String, defValue: Int = 0): Int {
@@ -215,8 +216,11 @@ fun Context.getPrefInt(key: String, defValue: Int = 0): Int {
 }
 
 fun Context.putPrefInt(key: String, value: Int) {
-    defaultSharedPreferences.edit { putInt(key, value) }
-    if (AppConfigStore.isInitialized) AppConfigStore.putInt(key, value)
+    if (!AppConfigStore.isInitialized) {
+        defaultSharedPreferences.edit { putInt(key, value) }
+        return
+    }
+    AppConfigStore.putInt(key, value)
 }
 
 fun Context.getPrefLong(key: String, defValue: Long = 0L): Long {
@@ -236,8 +240,11 @@ fun Context.getPrefLong(key: String, defValue: Long = 0L): Long {
 }
 
 fun Context.putPrefLong(key: String, value: Long) {
-    defaultSharedPreferences.edit { putLong(key, value) }
-    if (AppConfigStore.isInitialized) AppConfigStore.putLong(key, value)
+    if (!AppConfigStore.isInitialized) {
+        defaultSharedPreferences.edit { putLong(key, value) }
+        return
+    }
+    AppConfigStore.putLong(key, value)
 }
 
 fun Context.getPrefFloat(key: String, defValue: Float = 0f): Float {
@@ -257,8 +264,11 @@ fun Context.getPrefFloat(key: String, defValue: Float = 0f): Float {
 }
 
 fun Context.putPrefFloat(key: String, value: Float) {
-    defaultSharedPreferences.edit { putFloat(key, value) }
-    if (AppConfigStore.isInitialized) AppConfigStore.putFloat(key, value)
+    if (!AppConfigStore.isInitialized) {
+        defaultSharedPreferences.edit { putFloat(key, value) }
+        return
+    }
+    AppConfigStore.putFloat(key, value)
 }
 
 fun Context.getPrefString(key: String, defValue: String? = null): String? {
@@ -271,13 +281,15 @@ fun Context.getPrefString(key: String, defValue: String? = null): String? {
 }
 
 fun Context.putPrefString(key: String, value: String?) {
-    defaultSharedPreferences.edit { putString(key, value) }
-    if (AppConfigStore.isInitialized) AppConfigStore.putString(key, value)
+    if (!AppConfigStore.isInitialized) {
+        defaultSharedPreferences.edit { putString(key, value) }
+        return
+    }
+    AppConfigStore.putString(key, value)
 }
 
 fun Context.putPrefStringSync(key: String, value: String?) {
-    defaultSharedPreferences.edit(commit = true) { putString(key, value) }
-    if (AppConfigStore.isInitialized) AppConfigStore.putString(key, value)
+    putPrefString(key, value)
 }
 
 fun Context.getPrefStringSet(
@@ -293,13 +305,19 @@ fun Context.getPrefStringSet(
 }
 
 fun Context.putPrefStringSet(key: String, value: MutableSet<String>) {
-    defaultSharedPreferences.edit { putStringSet(key, value) }
-    if (AppConfigStore.isInitialized) AppConfigStore.putStringSet(key, value.toSet())
+    if (!AppConfigStore.isInitialized) {
+        defaultSharedPreferences.edit { putStringSet(key, value) }
+        return
+    }
+    AppConfigStore.putStringSet(key, value.toSet())
 }
 
 fun Context.removePref(key: String) {
-    defaultSharedPreferences.edit { remove(key) }
-    if (AppConfigStore.isInitialized) AppConfigStore.remove(key)
+    if (!AppConfigStore.isInitialized) {
+        defaultSharedPreferences.edit { remove(key) }
+        return
+    }
+    AppConfigStore.remove(key)
 }
 
 
@@ -321,6 +339,9 @@ fun Context.restart() {
                     or Intent.FLAG_ACTIVITY_CLEAR_TASK
                     or Intent.FLAG_ACTIVITY_CLEAR_TOP
         )
+        kotlinx.coroutines.runBlocking {
+            io.legado.app.help.config.DsSync.awaitPendingWrites()
+        }
         startActivity(intent)
         //杀掉以前进程
         Process.killProcess(Process.myPid())