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

refactor(R4.5): 排版配置的状态本体搬进 ReadStyleConfigStore

R4.4 之后 ReadBookConfig 只剩「选哪一份 + 怎么读」,但 configList/shareConfig
连同那把 @Synchronized 锁还挂在这个全局 object 上——状态和保护它的锁分居两处。

新增 ReadStyleConfigStore 作为配置列表的唯一所有者:上游是文件层
ReadStyleRepository,下游是 gateway 实现(写)与 ReadBookConfig(只读投影)。
它不知道当前选中哪一份——styleSelect/shareLayout 是设置项,仍由 ReadBookConfig
解析成下标后再来取用。

顺带修掉一个真 bug:原 ReadBookConfig.save() 写的是
Coroutine.async { synchronized(this) { ... } },而 async 的 block 是
suspend CoroutineScope.() -> T,那个 this 锁的是 CoroutineScope 不是
ReadBookConfig,等于没锁——GSON 在 IO 线程序列化 configList 本体时,主线程可以
同时 add/removeAt。现在 store 的 save() 在锁内先取快照再交给文件层。

消费方切换:
- ReadBookStyleConfigRepository 构造注入 store,不再穿透 ReadBookConfig 的
  internal 状态 API
- Backup 改用新增的 ReadStyleGateway.allBackgroundImagePaths()
- App 改用新增的 ReadStyleGateway.clearUnusedBackgrounds()
- Restore 的两处 initConfigs()/initShareConfig() 合并成两个文件都落地后一次
  refresh()。分开重读会让 shareConfig 的兜底(configList[5])取到还没被覆盖的
  旧列表;合并后顺带补上原本缺失的 publishState()

ReadBookConfig 1164 → 1067 行,不再依赖 ReadStyleRepository,可变状态归零。
一并删掉转发到该依赖上的死代码:export()/import()/saveBackgroundImage()/
configCount/configNames()。

护栏 ReadBookConfigDependencyDirectionTest 加第三条「不再持有排版状态本体」,
注入回归反证过会红。单测 450 全绿。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5151561 3 месяцев назад
Родитель
Сommit
59b7cb1878

+ 3 - 2
app/src/main/java/io/legado/app/App.kt

@@ -38,6 +38,7 @@ import io.legado.app.di.appModule
 import io.legado.app.domain.gateway.AppLocaleGateway
 import io.legado.app.domain.gateway.AppShellSettingsGateway
 import io.legado.app.domain.gateway.BackupSettingsGateway
+import io.legado.app.domain.gateway.ReadStyleGateway
 import io.legado.app.help.AppFreezeMonitor
 import io.legado.app.help.AppWebDav
 import io.legado.app.help.CrashHandler
@@ -120,7 +121,7 @@ class App : Application(), ImageLoaderFactory {
             exportGateway = get(),
         )
         ReadBookConfig.initialize(
-            readStyleRepository = get(),
+            configStore = get(),
             readSettingsGateway = get(),
         )
         if (legacyLanguage != null) {
@@ -214,7 +215,7 @@ class App : Application(), ImageLoaderFactory {
             RuleBigDataHelp.clearInvalid()
             BookHelp.clearInvalidCache()
             Backup.clearCache()
-            ReadBookConfig.clearBgAndCache()
+            get<ReadStyleGateway>().clearUnusedBackgrounds()
             ThemeConfigStore.clearBg()
             //初始化简繁转换引擎
             when (AppConfig.chineseConverterType) {

+ 18 - 10
app/src/main/java/io/legado/app/data/repository/ReadBookStyleConfigRepository.kt

@@ -31,6 +31,7 @@ import java.util.concurrent.atomic.AtomicLong
 class ReadBookStyleConfigRepository(
     private val readStyleRepository: ReadStyleRepository,
     private val highlightRuleRepository: HighlightRuleRepository,
+    private val configStore: ReadStyleConfigStore,
 ) : ReadStyleGateway {
     private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
     private val saveQueue = ReadStyleSaveQueue(
@@ -48,8 +49,8 @@ class ReadBookStyleConfigRepository(
     override val currentState: ReadStyleState get() = _state.value
 
     override fun refresh() {
-        ReadBookConfig.initConfigs()
-        ReadBookConfig.initShareConfig()
+        configStore.initConfigs()
+        configStore.initShareConfig()
         publishState()
     }
 
@@ -61,8 +62,8 @@ class ReadBookStyleConfigRepository(
         publishState()
         saveQueue.submit(
             ReadStyleSaveSnapshot(
-                configs = ReadBookConfig.configsSnapshot(),
-                shareConfig = ReadBookConfig.shareConfigSnapshot(),
+                configs = configStore.configsSnapshot(),
+                shareConfig = configStore.shareConfigSnapshot(),
             )
         )
     }
@@ -95,14 +96,15 @@ class ReadBookStyleConfigRepository(
     }
 
     override fun addStyle(): Int {
-        val index = ReadBookConfig.addConfig(ReadBookConfig.Config())
+        val index = configStore.addConfig(ReadBookConfig.Config())
         save()
         return index
     }
 
     override fun deleteCurrentStyle(): Boolean {
         val deletedConfigName = ReadBookConfig.durConfig.name
-        val removedIndex = ReadBookConfig.deleteDur()
+        val removedIndex = ReadBookConfig.styleSelect
+            .takeIf { configStore.deleteConfigAt(it) }
         if (removedIndex != null) {
             val readIndex = AppConfigStore.getInt(PreferKey.readStyleSelect) ?: 0
             val comicIndex = AppConfigStore.getInt(PreferKey.comicStyleSelect) ?: readIndex
@@ -128,7 +130,7 @@ class ReadBookStyleConfigRepository(
     }
 
     override fun importOrReplaceStyle(bytes: ByteArray): String {
-        val name = ReadBookConfig.importOrReplaceConfig(readStyleRepository.import(bytes))
+        val name = configStore.importOrReplaceConfig(readStyleRepository.import(bytes))
         save()
         return name
     }
@@ -159,9 +161,15 @@ class ReadBookStyleConfigRepository(
         save()
     }
 
-    override fun exportConfigsJson(): String = GSON.toJson(ReadBookConfig.configsSnapshot())
+    override fun exportConfigsJson(): String = GSON.toJson(configStore.configsSnapshot())
 
-    override fun exportShareConfigJson(): String = GSON.toJson(ReadBookConfig.shareConfigSnapshot())
+    override fun exportShareConfigJson(): String = GSON.toJson(configStore.shareConfigSnapshot())
+
+    override fun allBackgroundImagePaths(): List<String> = configStore.allPicBgStr()
+
+    override fun clearUnusedBackgrounds() {
+        configStore.clearBgAndCache()
+    }
 
     private fun publishState() {
         _state.value = buildState()
@@ -284,7 +292,7 @@ class ReadBookStyleConfigRepository(
 
     private fun buildState(): ReadStyleState = ReadStyleState(
         revision = stateRevision.incrementAndGet(),
-        items = ReadBookConfig.configsSnapshot().map { config ->
+        items = configStore.configsSnapshot().map { config ->
             ReadStyleItem(
                 name = config.name,
                 bgType = config.bgType,

+ 111 - 0
app/src/main/java/io/legado/app/data/repository/ReadStyleConfigStore.kt

@@ -0,0 +1,111 @@
+package io.legado.app.data.repository
+
+import io.legado.app.help.DefaultData
+import io.legado.app.help.config.ReadBookConfig
+import io.legado.app.help.coroutine.Coroutine
+
+/**
+ * 排版配置列表的**唯一所有者**。
+ *
+ * R4.4 把写面收进 gateway 实现之后,`ReadBookConfig` 只剩「选哪一份 + 怎么读」这两件事,
+ * 但列表本体和那把 `@Synchronized` 锁还挂在它身上——状态和保护它的锁分居两处,
+ * 谁都能顺手再往那个全局 object 上塞新状态。本类把两者放回一起:
+ * 上游是文件层 [ReadStyleRepository],下游是 gateway 实现(写)与 `ReadBookConfig`(只读投影)。
+ *
+ * 本类**不知道当前选中哪一份**:`styleSelect` / `shareLayout` 是设置项,
+ * 由 `ReadBookConfig` 解析成下标后再来取用。
+ */
+class ReadStyleConfigStore(private val readStyleRepository: ReadStyleRepository) {
+
+    private val lock = Any()
+    private val configList = arrayListOf<ReadBookConfig.Config>()
+    private lateinit var shareConfigRef: ReadBookConfig.Config
+
+    val configFilePath: String get() = readStyleRepository.configFilePath
+    val shareConfigFilePath: String get() = readStyleRepository.shareConfigFilePath
+
+    /**
+     * 共享排版那一份。返回的是本体不是副本——gateway 实现就地改它的字段,
+     * 发一份副本出去会让写入落到空气里。
+     */
+    val shareConfig: ReadBookConfig.Config get() = shareConfigRef
+
+    fun initConfigs() {
+        val configs = readStyleRepository.readConfigs()
+        synchronized(lock) {
+            configList.clear()
+            configList.addAll(configs)
+        }
+    }
+
+    fun initShareConfig() {
+        val fallback = synchronized(lock) { configList.getOrNull(5) } ?: ReadBookConfig.Config()
+        shareConfigRef = readStyleRepository.readShareConfig(fallback)
+    }
+
+    /** 按下标取用。配置文件缺斤少两时先恢复默认,保证 5 份预设始终在。 */
+    fun configAt(index: Int): ReadBookConfig.Config = synchronized(lock) {
+        if (configList.size < 5) {
+            resetAllLocked()
+        }
+        configList.getOrNull(index) ?: configList[0]
+    }
+
+    /** 整份替换(应用预设 / 导入)。共享排版开着时,共享那份跟着换。 */
+    fun replaceConfigAt(index: Int, config: ReadBookConfig.Config, alsoShare: Boolean) {
+        synchronized(lock) {
+            configList[index] = config
+        }
+        if (alsoShare) {
+            shareConfigRef = config
+        }
+    }
+
+    fun configsSnapshot(): List<ReadBookConfig.Config> = synchronized(lock) {
+        configList.map { it.copy() }
+    }
+
+    fun shareConfigSnapshot(): ReadBookConfig.Config = shareConfigRef.copy()
+
+    fun addConfig(config: ReadBookConfig.Config): Int = synchronized(lock) {
+        configList.add(config)
+        configList.lastIndex
+    }
+
+    fun importOrReplaceConfig(config: ReadBookConfig.Config): String = synchronized(lock) {
+        val index = configList.indexOfFirst { it.name == config.name }
+        if (index >= 0) {
+            configList[index] = config
+        } else {
+            configList.add(config)
+        }
+        config.name
+    }
+
+    /** 5 份预设不允许删。 */
+    fun deleteConfigAt(index: Int): Boolean = synchronized(lock) {
+        if (configList.size <= 5) return@synchronized false
+        configList.removeAt(index)
+        true
+    }
+
+    /** 备份要把排版引用到的背景图一起带走。 */
+    fun allPicBgStr(): List<String> = readStyleRepository.getAllPicBgStr(configsSnapshot())
+
+    /** 清掉没有任何排版引用的背景图,以及导入导出留下的缓存。 */
+    fun clearBgAndCache() {
+        readStyleRepository.clearBgAndCache(configsSnapshot())
+    }
+
+    private fun resetAllLocked() {
+        configList.clear()
+        configList.addAll(DefaultData.readConfigs)
+        save()
+    }
+
+    private fun save() {
+        Coroutine.async {
+            readStyleRepository.save(configsSnapshot(), shareConfigRef)
+        }
+    }
+}

+ 1 - 1
app/src/main/java/io/legado/app/data/repository/ReadStyleRepository.kt

@@ -58,7 +58,7 @@ class ReadStyleRepository(
         configs: List<ReadBookConfig.Config>,
         shareConfig: ReadBookConfig.Config
     ) {
-        // 原子写:两条保存通道(ReadBookConfig.save 与 ReadStyleSaveQueue)会并发落到这里,
+        // 原子写:两条保存通道(ReadStyleConfigStore.save 与 ReadStyleSaveQueue)会并发落到这里,
         // 「删掉再写」既有杀进程丢配置的窗口,也会让两者互相看到对方写了一半的文件。
         FileUtils.writeTextAtomic(configFilePath, GSON.toJson(configs))
         FileUtils.writeTextAtomic(shareConfigFilePath, GSON.toJson(shareConfig))

+ 2 - 0
app/src/main/java/io/legado/app/di/appModule.kt

@@ -68,6 +68,7 @@ import io.legado.app.data.repository.ReadAloudVoiceRepository
 import io.legado.app.data.repository.ReadBookStyleConfigRepository
 import io.legado.app.data.repository.ReadRecordRepository
 import io.legado.app.data.repository.ReadSettingsRepository
+import io.legado.app.data.repository.ReadStyleConfigStore
 import io.legado.app.data.repository.ReadStyleRepository
 import io.legado.app.data.repository.RemoteBookRepository
 import io.legado.app.data.repository.ReplaceRuleRepository
@@ -369,6 +370,7 @@ val appModule = module {
     singleOf(::HighlightRuleRepository)
     singleOf(::HighlightTagRuleRepository)
     singleOf(::ReadStyleRepository)
+    singleOf(::ReadStyleConfigStore)
     singleOf(::ReadBookStyleConfigRepository)
     single<ReadStyleGateway> { get<ReadBookStyleConfigRepository>() }
     singleOf(::ExploreBooksUseCase)

+ 6 - 0
app/src/main/java/io/legado/app/domain/gateway/ReadStyleGateway.kt

@@ -35,4 +35,10 @@ interface ReadStyleGateway {
     fun setCurrentBackgroundImageForMode(path: String, isNight: Boolean)
     fun exportConfigsJson(): String
     fun exportShareConfigJson(): String
+
+    /** 备份要把所有排版引用到的背景图一起带走。 */
+    fun allBackgroundImagePaths(): List<String>
+
+    /** 清掉没有任何排版引用的背景图,以及导入导出留下的缓存。 */
+    fun clearUnusedBackgrounds()
 }

+ 12 - 103
app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt

@@ -9,17 +9,14 @@ import io.legado.app.R
 import io.legado.app.constant.PageAnim
 import io.legado.app.constant.ReadTipType
 import io.legado.app.data.entities.HighlightRule
-import io.legado.app.data.repository.ReadStyleRepository
+import io.legado.app.data.repository.ReadStyleConfigStore
 import io.legado.app.domain.gateway.ReadSettingsGateway
-import io.legado.app.help.DefaultData
-import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.model.ReadSessionState
 import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.GSON
 import io.legado.app.utils.fromJsonObject
 import io.legado.app.utils.hexString
 import splitties.init.appCtx
-import java.io.InputStream
 
 /**
  * 阅读界面配置
@@ -27,58 +24,33 @@ import java.io.InputStream
 @Suppress("ConstPropertyName")
 @Keep
 object ReadBookConfig {
-    private lateinit var readStyleRepository: ReadStyleRepository
+    private lateinit var configStore: ReadStyleConfigStore
     private lateinit var readSettingsGateway: ReadSettingsGateway
     private val readSettings get() = readSettingsGateway.currentSettings
 
     internal fun initialize(
-        readStyleRepository: ReadStyleRepository,
+        configStore: ReadStyleConfigStore,
         readSettingsGateway: ReadSettingsGateway,
     ) {
-        this.readStyleRepository = readStyleRepository
+        this.configStore = configStore
         this.readSettingsGateway = readSettingsGateway
-        initConfigs()
-        initShareConfig()
+        configStore.initConfigs()
+        configStore.initShareConfig()
     }
 
 
     const val configFileName = "readConfig.json"
     const val shareConfigFileName = "shareReadConfig.json"
-    val configFilePath: String get() = readStyleRepository.configFilePath
-    val shareConfigFilePath: String get() = readStyleRepository.shareConfigFilePath
-    private val configList: ArrayList<Config> = arrayListOf()
-    private lateinit var shareConfig: Config
+    val configFilePath: String get() = configStore.configFilePath
+    val shareConfigFilePath: String get() = configStore.shareConfigFilePath
 
-    internal fun configsSnapshot(): List<Config> = synchronized(this) {
-        configList.map { it.copy() }
-    }
-
-    internal fun shareConfigSnapshot(): Config = synchronized(this) { shareConfig.copy() }
+    /** 共享排版那一份,`config` 与 `getExportConfig()` 要用。 */
+    private val shareConfig: Config get() = configStore.shareConfig
 
-    internal fun addConfig(config: Config): Int = synchronized(this) {
-        configList.add(config)
-        configList.lastIndex
-    }
-
-    internal fun importOrReplaceConfig(config: Config): String = synchronized(this) {
-        val index = configList.indexOfFirst { it.name == config.name }
-        if (index >= 0) {
-            configList[index] = config
-        } else {
-            configList.add(config)
-        }
-        config.name
-    }
-
-    internal val configCount: Int get() = synchronized(this) { configList.size }
-    internal fun configNames(): List<String> = synchronized(this) { configList.map { it.name } }
     var durConfig
-        get() = getConfig(styleSelect)
+        get() = configStore.configAt(styleSelect)
         set(value) {
-            configList[styleSelect] = value
-            if (shareLayout) {
-                shareConfig = value
-            }
+            configStore.replaceConfigAt(styleSelect, value, alsoShare = shareLayout)
         }
 
     val textColor: Int get() = durConfig.curTextColor()
@@ -91,57 +63,6 @@ object ReadBookConfig {
     val textAccentColor: Int get() = durConfig.curTextAccentColor()
     val textShadowColor: Int get() = durConfig.curTextShadowColor()
     val menuColor: Int get() = readMenuAccentColor
-    @Synchronized
-    fun getConfig(index: Int): Config {
-        if (configList.size < 5) {
-            resetAll()
-        }
-        return configList.getOrNull(index) ?: configList[0]
-    }
-
-    fun initConfigs() {
-        readStyleRepository.readConfigs().let {
-            configList.clear()
-            configList.addAll(it)
-        }
-    }
-
-    fun initShareConfig() {
-        shareConfig = readStyleRepository.readShareConfig(configList.getOrNull(5) ?: Config())
-    }
-
-    fun save() {
-        Coroutine.async {
-            synchronized(this) {
-                readStyleRepository.save(configList, shareConfig)
-            }
-        }
-    }
-
-    fun getAllPicBgStr(): ArrayList<String> {
-        return readStyleRepository.getAllPicBgStr(configList)
-    }
-
-    internal fun deleteDur(): Int? {
-        if (configList.size > 5) {
-            val removeIndex = styleSelect
-            configList.removeAt(removeIndex)
-            return removeIndex
-        }
-        return null
-    }
-
-    fun clearBgAndCache() {
-        readStyleRepository.clearBgAndCache(configList)
-    }
-
-    private fun resetAll() {
-        DefaultData.readConfigs.let {
-            configList.clear()
-            configList.addAll(it)
-            save()
-        }
-    }
 
     // DataStore 标量已归入 ReadSettings;这里仅保留旧渲染层所需的同步只读快照。
     val readBodyToLh get() = readSettings.readBodyToLh
@@ -625,18 +546,6 @@ object ReadBookConfig {
         return exportConfig
     }
 
-    fun export(): ByteArray {
-        return readStyleRepository.export(getExportConfig())
-    }
-
-    fun import(byteArray: ByteArray): Config {
-        return readStyleRepository.import(byteArray)
-    }
-
-    fun saveBackgroundImage(inputStream: InputStream, displayName: String?): String {
-        return readStyleRepository.saveBackgroundImage(inputStream, displayName)
-    }
-
     @Keep
     data class Config(
         var name: String = "",

+ 1 - 1
app/src/main/java/io/legado/app/help/storage/Backup.kt

@@ -308,7 +308,7 @@ object Backup {
         FileUtils.delete(backupPath)
         FileUtils.delete(zipFilePath)
         currentCoroutineContext().ensureActive()
-        ReadBookConfig.getAllPicBgStr().map {
+        readStyleGateway.allBackgroundImagePaths().map {
             if (it.contains(File.separator)) {
                 File(it)
             } else {

+ 4 - 2
app/src/main/java/io/legado/app/help/storage/Restore.kt

@@ -34,6 +34,7 @@ import io.legado.app.data.entities.readRecord.ReadRecord
 import io.legado.app.data.entities.readRecord.ReadRecordDetail
 import io.legado.app.data.entities.readRecord.ReadRecordSession
 import io.legado.app.domain.gateway.AppLocaleGateway
+import io.legado.app.domain.gateway.ReadStyleGateway
 import io.legado.app.help.DirectLinkUpload
 import io.legado.app.help.LauncherIconHelp
 import io.legado.app.help.book.isLocal
@@ -341,7 +342,6 @@ object Restore : KoinComponent {
                 it.exists()
             }?.runCatching {
                 FileUtils.copyFileAtomic(this, ReadBookConfig.configFilePath)
-                ReadBookConfig.initConfigs()
             }?.onFailure {
                 AppLog.put("恢复阅读界面出错\n${it.localizedMessage}", it)
             }
@@ -349,10 +349,12 @@ object Restore : KoinComponent {
                 it.exists()
             }?.runCatching {
                 FileUtils.copyFileAtomic(this, ReadBookConfig.shareConfigFilePath)
-                ReadBookConfig.initShareConfig()
             }?.onFailure {
                 AppLog.put("恢复阅读界面出错\n${it.localizedMessage}", it)
             }
+            // 两个文件都落地后再整体重读:分开重读会让 shareConfig 的兜底
+            // (configList[5])取到还没被覆盖的旧列表。refresh 顺带发布 state。
+            get<ReadStyleGateway>().refresh()
         }
         // 恢复配置文件 (手动解析 XML,替代反射逻辑)
         val configFile = File(path, "config.xml")

+ 29 - 1
app/src/test/java/io/legado/app/help/config/ReadBookConfigDependencyDirectionTest.kt

@@ -13,7 +13,7 @@ import java.io.File
  * 谁是权威就说不清了,也让两边都没法单独测。
  *
  * 现在方向是单向的:消费方(`ChapterProvider`)直接找 gateway,`ReadBookConfig` 只向下
- * 依赖 `ReadStyleRepository`(文件读写)。
+ * 依赖 `ReadStyleConfigStore`(状态本体,R4.5 之前是 `ReadStyleRepository`)。
  *
  * 注意 `readSettingsGateway` 不在此列——那是**只读**上游设置,不构成环。
  *
@@ -68,7 +68,35 @@ class ReadBookConfigDependencyDirectionTest {
         )
     }
 
+    @Test
+    fun `ReadBookConfig 不再持有排版状态本体`() {
+        val source = stripComments(readBookConfigSource())
+        val violations = buildList {
+            Regex("""^    (?:private )?(?:val|var) (\w+)\s*:\s*(?:Array|Mutable)?List<Config>""", MULTILINE)
+                .findAll(source)
+                .forEach { add("${it.groupValues[1]}(配置列表)") }
+            Regex("""^    (?:private )?lateinit var (\w+)\s*:\s*Config\b""", MULTILINE)
+                .findAll(source)
+                .forEach { add("${it.groupValues[1]}(共享排版那一份)") }
+            Regex("""^    (?:private )?(?:val|var) (\w+)\s*=\s*(?:arrayListOf|mutableListOf|mutableMapOf|hashMapOf)""", MULTILINE)
+                .findAll(source)
+                .forEach { add("${it.groupValues[1]}(可变集合)") }
+        }
+
+        assertTrue(
+            "ReadBookConfig 又把排版状态本体收回去了:${violations.joinToString()}。\n" +
+                "R4.5 已经把 configList / shareConfig 连同那把锁一起搬进 ReadStyleConfigStore——" +
+                "状态和保护它的锁必须待在一起,否则下一个人只会照着现成的字段继续往这个全局 " +
+                "object 上堆。ReadBookConfig 只做两件事:按设置解析出「选哪一份」,以及把 " +
+                "Config 的字段投影成只读属性。\n" +
+                "需要新的排版状态,请加在 ReadStyleConfigStore 上。",
+            violations.isEmpty(),
+        )
+    }
+
     private companion object {
+        val MULTILINE = RegexOption.MULTILINE
+
         /** `durConfig` 是整份配置的替换入口(应用预设 / 导入),只有 gateway 实现用得到。 */
         val WRITABLE_ALLOWLIST = setOf("durConfig")
 

+ 1 - 1
app/src/test/java/io/legado/app/utils/FileUtilsAtomicWriteTest.kt

@@ -12,7 +12,7 @@ import java.io.File
  *
  * 原实现是「先 `FileUtils.delete`,再 `createFileIfNotExist().writeText()`」:两步之间目标
  * 文件根本不存在,进程在此时被杀,用户的整套排版方案就没了。两条保存通道
- * (`ReadBookConfig.save()` 与 `ReadStyleSaveQueue`)还会并发落到同一个文件上。
+ * (`ReadStyleConfigStore.save()` 与 `ReadStyleSaveQueue`)还会并发落到同一个文件上。
  */
 class FileUtilsAtomicWriteTest {