From 9cd530d223d5284ee66635b9d1d3e0426b65c37a Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:15:03 +0800 Subject: [PATCH] Batch parse and save subscription configs --- .../com/v2ray/ang/handler/AngConfigManager.kt | 98 ++++++++++++++----- .../java/com/v2ray/ang/handler/MmkvManager.kt | 10 ++ 2 files changed, 84 insertions(+), 24 deletions(-) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt index d0f8f343..ff41f884 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt @@ -11,6 +11,7 @@ import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.SubscriptionCache import com.v2ray.ang.dto.SubscriptionItem import com.v2ray.ang.enums.EConfigType +import com.v2ray.ang.extension.isNotNullEmpty import com.v2ray.ang.fmt.CustomFmt import com.v2ray.ang.fmt.Hysteria2Fmt import com.v2ray.ang.fmt.ShadowsocksFmt @@ -237,23 +238,77 @@ object AngConfigManager { } val subItem = MmkvManager.decodeSubscription(subid) - var count = 0 + + // Parse all configs first (no I/O during parsing) + val configs = mutableListOf() servers.lines() .distinct() .reversed() .forEach { - val resId = parseConfig(it, subid, subItem, removedSelectedServer) - if (resId == 0) { - count++ + val config = parseConfig(it, subid, subItem) + if (config != null) { + configs.add(config) } } - return count + + // Batch save all parsed configs (only one serverList read/write) + if (configs.isNotEmpty()) { + val keys = batchSaveConfigs(configs, subid) + + // Handle removed selected server + removedSelectedServer?.let { removed -> + val matchKey = keys.find { key -> + val savedConfig = MmkvManager.decodeServerConfig(key) + savedConfig != null && + savedConfig.server == removed.server && + savedConfig.serverPort == removed.serverPort + } + matchKey?.let { MmkvManager.setSelectServer(it) } + } + } + + return configs.size } catch (e: Exception) { Log.e(AppConfig.TAG, "Failed to parse batch config", e) } return 0 } + /** + * Batch save configurations to reduce serverList read/write operations. + * Reads serverList once, saves all configs, then writes serverList once. + * + * @param configs The list of ProfileItem to save. + * @param subid The subscription ID. + * @return The list of generated keys. + */ + private fun batchSaveConfigs(configs: List, subid: String): List { + val keys = mutableListOf() + + // Read serverList once + val serverList = MmkvManager.decodeServerList(subid) + var needSetSelected = MmkvManager.getSelectServer().isNullOrBlank() + + configs.forEach { config -> + val key = Utils.getUuid() + // Save profile directly without updating serverList + MmkvManager.encodeProfileDirect(key, JsonUtil.toJson(config)) + + if (!serverList.contains(key)) { + serverList.add(0, key) + if (needSetSelected) { + MmkvManager.setSelectServer(key) + needSetSelected = false + } + } + keys.add(key) + } + + // Write serverList once + MmkvManager.encodeServerList(serverList, subid) + return keys + } + /** * Parses a custom configuration server. * @@ -319,22 +374,21 @@ object AngConfigManager { /** * Parses the configuration from a QR code or string. + * Only parses and returns ProfileItem, does not save. * * @param str The configuration string. * @param subid The subscription ID. * @param subItem The subscription item. - * @param removedSelectedServer The removed selected server. - * @return The result code. + * @return The parsed ProfileItem or null if parsing fails or filtered out. */ private fun parseConfig( str: String?, subid: String, - subItem: SubscriptionItem?, - removedSelectedServer: ProfileItem? - ): Int { + subItem: SubscriptionItem? + ): ProfileItem? { try { if (str == null || TextUtils.isEmpty(str)) { - return R.string.toast_none_data + return null } val config = if (str.startsWith(EConfigType.VMESS.protocolScheme)) { @@ -356,28 +410,24 @@ object AngConfigManager { } if (config == null) { - return R.string.toast_incorrect_protocol + return null } - //filter - if (subItem?.filter != null && subItem.filter?.isNotEmpty() == true && config.remarks.isNotEmpty()) { - val matched = Regex(pattern = subItem.filter ?: "") + + // Apply filter + if (subItem?.filter.isNotNullEmpty() && config.remarks.isNotNullEmpty()) { + val matched = Regex(pattern = subItem?.filter.orEmpty()) .containsMatchIn(input = config.remarks) - if (!matched) return -1 + if (!matched) return null } config.subscriptionId = subid config.description = generateDescription(config) - val guid = MmkvManager.encodeServerConfig("", config) - if (removedSelectedServer != null && - config.server == removedSelectedServer.server && config.serverPort == removedSelectedServer.serverPort - ) { - MmkvManager.setSelectServer(guid) - } + + return config } catch (e: Exception) { Log.e(AppConfig.TAG, "Failed to parse config", e) - return -1 + return null } - return 0 } /** diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt index c05b0664..9dbbfa4d 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt @@ -225,6 +225,16 @@ object MmkvManager { return key } + /** + * Encodes the server configuration directly without updating serverList. + * + * @param key The server GUID. + * @param configJson The server configuration JSON string. + */ + fun encodeProfileDirect(key: String, configJson: String) { + profileFullStorage.encode(key, configJson) + } + /** * Removes the server configuration. *