From 6dcf46446fb7c446d7426b249afea813f43b69ee Mon Sep 17 00:00:00 2001 From: lnx Date: Sun, 26 Jul 2026 20:57:40 +0800 Subject: [PATCH 1/7] feat: add auto-destroy UI option to save memory Add a setting toggle that automatically kills the UI process 3 seconds after the app goes to background, freeing UI memory while ClashService keeps running in the :background process. - UiStore: add autoDestroyUI preference - AppSettingsDesign: add toggle below "hide from recents" - MainApplication: observe app visibility and kill UI process - ApplicationObserver: support multiple visibility listeners (was overwritten by Remote.launch, breaking our callback) --- .../com/github/kr328/clash/MainApplication.kt | 26 +++++++++++++++++++ .../github/kr328/clash/util/Application.kt | 11 +++++--- .../kr328/clash/design/AppSettingsDesign.kt | 7 +++++ .../kr328/clash/design/store/UiStore.kt | 5 ++++ design/src/main/res/values-zh/strings.xml | 2 ++ design/src/main/res/values/strings.xml | 2 ++ 6 files changed, 50 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 9242d72299..11e2f76bd7 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -2,12 +2,18 @@ package com.github.kr328.clash import android.app.Application import android.content.Context +import android.os.Process import com.github.kr328.clash.common.Global import com.github.kr328.clash.common.compat.currentProcessName import com.github.kr328.clash.common.log.Log +import com.github.kr328.clash.design.store.UiStore import com.github.kr328.clash.remote.Remote import com.github.kr328.clash.service.util.sendServiceRecreated +import com.github.kr328.clash.util.ApplicationObserver import com.github.kr328.clash.util.clashDir +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import java.io.File import java.io.FileOutputStream @@ -29,6 +35,26 @@ class MainApplication : Application() { Log.d("Process $processName started") if (processName == packageName) { + ApplicationObserver.attach(this) + + // Setup auto destroy UI observer + val uiStore = UiStore(this) + var autoDestroyJob: Job? = null + + ApplicationObserver.onVisibleChanged { visible -> + if (!visible && uiStore.autoDestroyUI) { + autoDestroyJob?.cancel() + autoDestroyJob = Global.launch { + delay(3000) + Log.d("AutoDestroyUI: Killing UI process to free memory") + Process.killProcess(Process.myPid()) + } + } else if (visible) { + autoDestroyJob?.cancel() + autoDestroyJob = null + } + } + Remote.launch() } else { sendServiceRecreated() diff --git a/app/src/main/java/com/github/kr328/clash/util/Application.kt b/app/src/main/java/com/github/kr328/clash/util/Application.kt index 1fb6336781..25dfdec35f 100644 --- a/app/src/main/java/com/github/kr328/clash/util/Application.kt +++ b/app/src/main/java/com/github/kr328/clash/util/Application.kt @@ -12,20 +12,25 @@ object ApplicationObserver { private val _createdActivities: MutableSet = mutableSetOf() private val _visibleActivities: MutableSet = mutableSetOf() - private var visibleChanged: (Boolean) -> Unit = {} + private val visibleChangedListeners: MutableList<(Boolean) -> Unit> = mutableListOf() private var appVisible = false private set(value) { if (field != value) { field = value - visibleChanged(value) + visibleChangedListeners.forEach { listener -> + listener(value) + } } } val createdActivities: Set get() = _createdActivities + val isAppVisible: Boolean + get() = appVisible + private val activityObserver = object : Application.ActivityLifecycleCallbacks { @Synchronized override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { @@ -55,7 +60,7 @@ object ApplicationObserver { } fun onVisibleChanged(visibleChanged: (Boolean) -> Unit) { - this.visibleChanged = visibleChanged + visibleChangedListeners.add(visibleChanged) } fun attach(application: Application) { diff --git a/design/src/main/java/com/github/kr328/clash/design/AppSettingsDesign.kt b/design/src/main/java/com/github/kr328/clash/design/AppSettingsDesign.kt index dc43d00a29..4495051032 100644 --- a/design/src/main/java/com/github/kr328/clash/design/AppSettingsDesign.kt +++ b/design/src/main/java/com/github/kr328/clash/design/AppSettingsDesign.kt @@ -88,6 +88,13 @@ class AppSettingsDesign( } } + switch( + value = uiStore::autoDestroyUI, + icon = R.drawable.ic_baseline_hide, + title = R.string.auto_destroy_ui_title, + summary = R.string.auto_destroy_ui_desc, + ) + category(R.string.service) switch( diff --git a/design/src/main/java/com/github/kr328/clash/design/store/UiStore.kt b/design/src/main/java/com/github/kr328/clash/design/store/UiStore.kt index ddc9d600fd..3eca036234 100644 --- a/design/src/main/java/com/github/kr328/clash/design/store/UiStore.kt +++ b/design/src/main/java/com/github/kr328/clash/design/store/UiStore.kt @@ -41,6 +41,11 @@ class UiStore(context: Context) { defaultValue = false, ) + var autoDestroyUI: Boolean by store.boolean( + key = "auto_destroy_ui", + defaultValue = false, + ) + var proxyExcludeNotSelectable by store.boolean( key = "proxy_exclude_not_selectable", defaultValue = false, diff --git a/design/src/main/res/values-zh/strings.xml b/design/src/main/res/values-zh/strings.xml index be62f50f54..f94f33d7fa 100644 --- a/design/src/main/res/values-zh/strings.xml +++ b/design/src/main/res/values-zh/strings.xml @@ -41,6 +41,8 @@ 可以在拨号盘输入 *#*#252746382#*#* 打开应用 从最近任务隐藏 在最近任务中隐藏应用 + 自动销毁界面 + 应用进入后台3秒后自动销毁界面以节省内存 历史 从文件导入 从 URL 导入 diff --git a/design/src/main/res/values/strings.xml b/design/src/main/res/values/strings.xml index 2fe54fd226..8a6ee26474 100644 --- a/design/src/main/res/values/strings.xml +++ b/design/src/main/res/values/strings.xml @@ -361,6 +361,8 @@ You can dial *#*#252746382#*#* to open this App Hide from Recents Hide app from the Recent apps screen + Auto Destroy UI + Automatically destroy UI after 3 seconds when app goes to background Camera access is restricted. Please enable it in Settings. An unhandled system exception occurred. From d47eadc86b94c753071360f0483baddf6ac60a59 Mon Sep 17 00:00:00 2001 From: lnx Date: Sun, 26 Jul 2026 21:00:46 +0800 Subject: [PATCH 2/7] fix: use distinct icon for auto-destroy UI toggle ic_baseline_hide was already used by hideAppIcon switch right above; use ic_baseline_close to avoid duplicate icons on adjacent settings. --- .../java/com/github/kr328/clash/design/AppSettingsDesign.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design/src/main/java/com/github/kr328/clash/design/AppSettingsDesign.kt b/design/src/main/java/com/github/kr328/clash/design/AppSettingsDesign.kt index 4495051032..de1ef20c92 100644 --- a/design/src/main/java/com/github/kr328/clash/design/AppSettingsDesign.kt +++ b/design/src/main/java/com/github/kr328/clash/design/AppSettingsDesign.kt @@ -90,7 +90,7 @@ class AppSettingsDesign( switch( value = uiStore::autoDestroyUI, - icon = R.drawable.ic_baseline_hide, + icon = R.drawable.ic_baseline_close, title = R.string.auto_destroy_ui_title, summary = R.string.auto_destroy_ui_desc, ) From 4351de1d0e8efcd3c425abcab0e03cdca4436253 Mon Sep 17 00:00:00 2001 From: lnx Date: Sun, 26 Jul 2026 22:27:41 +0800 Subject: [PATCH 3/7] refactor: finish activities instead of killing process Replace Process.killProcess with finishing all activities and triggering GC. This avoids bypassing Android lifecycle (Activity.onDestroy, Service .onDestroy) and resolves LogcatService state leak issues while still releasing UI memory (View trees, design objects). --- .../java/com/github/kr328/clash/MainApplication.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 11e2f76bd7..86f058568b 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -2,7 +2,6 @@ package com.github.kr328.clash import android.app.Application import android.content.Context -import android.os.Process import com.github.kr328.clash.common.Global import com.github.kr328.clash.common.compat.currentProcessName import com.github.kr328.clash.common.log.Log @@ -11,9 +10,11 @@ import com.github.kr328.clash.remote.Remote import com.github.kr328.clash.service.util.sendServiceRecreated import com.github.kr328.clash.util.ApplicationObserver import com.github.kr328.clash.util.clashDir +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.File import java.io.FileOutputStream @@ -46,8 +47,13 @@ class MainApplication : Application() { autoDestroyJob?.cancel() autoDestroyJob = Global.launch { delay(3000) - Log.d("AutoDestroyUI: Killing UI process to free memory") - Process.killProcess(Process.myPid()) + Log.d("AutoDestroyUI: Cleaning up UI to free memory") + withContext(Dispatchers.Main) { + ApplicationObserver.createdActivities.toList().forEach { activity -> + activity.finish() + } + } + System.gc() } } else if (visible) { autoDestroyJob?.cancel() From 74c6aeb6cee57210677e05a5984ddae9a1fa56c0 Mon Sep 17 00:00:00 2001 From: lnx Date: Sun, 26 Jul 2026 22:33:23 +0800 Subject: [PATCH 4/7] revert: kill UI process to fully release memory Core services (ClashService, TunService, RemoteService) and VPN run in :background process, unaffected by killing UI process. LogcatService only starts when user opens logs screen; static 'running' flag resets on new process launch. Killing UI process is the cleanest way to release all UI memory (Activities, Views, Design objects, Application singletons). --- .../java/com/github/kr328/clash/MainApplication.kt | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 86f058568b..11e2f76bd7 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -2,6 +2,7 @@ package com.github.kr328.clash import android.app.Application import android.content.Context +import android.os.Process import com.github.kr328.clash.common.Global import com.github.kr328.clash.common.compat.currentProcessName import com.github.kr328.clash.common.log.Log @@ -10,11 +11,9 @@ import com.github.kr328.clash.remote.Remote import com.github.kr328.clash.service.util.sendServiceRecreated import com.github.kr328.clash.util.ApplicationObserver import com.github.kr328.clash.util.clashDir -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.io.File import java.io.FileOutputStream @@ -47,13 +46,8 @@ class MainApplication : Application() { autoDestroyJob?.cancel() autoDestroyJob = Global.launch { delay(3000) - Log.d("AutoDestroyUI: Cleaning up UI to free memory") - withContext(Dispatchers.Main) { - ApplicationObserver.createdActivities.toList().forEach { activity -> - activity.finish() - } - } - System.gc() + Log.d("AutoDestroyUI: Killing UI process to free memory") + Process.killProcess(Process.myPid()) } } else if (visible) { autoDestroyJob?.cancel() From f5b1e000483d409edb0362ac332ec299d9ac802c Mon Sep 17 00:00:00 2001 From: lnx Date: Sun, 26 Jul 2026 22:36:03 +0800 Subject: [PATCH 5/7] refactor: minimize changes to UI-only scope - Remove redundant ApplicationObserver.attach (Remote.launch already calls it); register our listener before Remote.launch so we don't miss the first visibility event. - Remove unused isAppVisible getter. Keep changes strictly limited to UI process memory management. --- app/src/main/java/com/github/kr328/clash/MainApplication.kt | 5 ++--- app/src/main/java/com/github/kr328/clash/util/Application.kt | 3 --- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 11e2f76bd7..91de5eb6c7 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -35,9 +35,8 @@ class MainApplication : Application() { Log.d("Process $processName started") if (processName == packageName) { - ApplicationObserver.attach(this) - - // Setup auto destroy UI observer + // Setup auto destroy UI observer (must register before Remote.launch + // so our callback is in the listener list before any visibility event) val uiStore = UiStore(this) var autoDestroyJob: Job? = null diff --git a/app/src/main/java/com/github/kr328/clash/util/Application.kt b/app/src/main/java/com/github/kr328/clash/util/Application.kt index 25dfdec35f..dfad2c12da 100644 --- a/app/src/main/java/com/github/kr328/clash/util/Application.kt +++ b/app/src/main/java/com/github/kr328/clash/util/Application.kt @@ -28,9 +28,6 @@ object ApplicationObserver { val createdActivities: Set get() = _createdActivities - val isAppVisible: Boolean - get() = appVisible - private val activityObserver = object : Application.ActivityLifecycleCallbacks { @Synchronized override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { From 3122c550c5b00f4088ea66c06849c7e4447d0e9e Mon Sep 17 00:00:00 2001 From: lnx Date: Sun, 26 Jul 2026 22:44:00 +0800 Subject: [PATCH 6/7] fix: skip auto-destroy UI when logcat is running --- app/src/main/java/com/github/kr328/clash/MainApplication.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 91de5eb6c7..9f5a6b6187 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -41,7 +41,7 @@ class MainApplication : Application() { var autoDestroyJob: Job? = null ApplicationObserver.onVisibleChanged { visible -> - if (!visible && uiStore.autoDestroyUI) { + if (!visible && uiStore.autoDestroyUI && !LogcatService.running) { autoDestroyJob?.cancel() autoDestroyJob = Global.launch { delay(3000) From feb0efdc1b22e276ed2ba0b49b59342cdab85b00 Mon Sep 17 00:00:00 2001 From: lnx Date: Mon, 27 Jul 2026 09:40:16 +0800 Subject: [PATCH 7/7] feat: enhance auto-destroy UI with protection and logging - Add protected activities whitelist to prevent killing during edits - Add pendingExternalRequest flag to protect VPN authorization flow - Add polling-based detection (wait when protected, kill when idle) - Add auto_destroy.log for persistent self-destroy tracking - Fix type inference for protectedActivities Set - Complete translations for 6 additional languages --- .../com/github/kr328/clash/MainActivity.kt | 27 +++++++++---- .../com/github/kr328/clash/MainApplication.kt | 39 ++++++++++++++++--- design/src/main/res/values-ja-rJP/strings.xml | 2 + design/src/main/res/values-ko-rKR/strings.xml | 2 + design/src/main/res/values-ru/strings.xml | 2 + design/src/main/res/values-vi/strings.xml | 2 + design/src/main/res/values-zh-rHK/strings.xml | 2 + design/src/main/res/values-zh-rTW/strings.xml | 2 + 8 files changed, 65 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index ea8254cb40..e944aef480 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -30,6 +30,14 @@ import java.util.concurrent.TimeUnit import com.github.kr328.clash.design.R as DesignR class MainActivity : BaseActivity() { + + companion object { + @Volatile + @JvmStatic + var pendingExternalRequest: Boolean = false + private set + } + override suspend fun main() { val design = MainDesign(this) @@ -129,13 +137,18 @@ class MainActivity : BaseActivity() { try { if (vpnRequest != null) { - val result = startActivityForResult( - ActivityResultContracts.StartActivityForResult(), - vpnRequest - ) - - if (result.resultCode == RESULT_OK) - startClashService() + pendingExternalRequest = true + try { + val result = startActivityForResult( + ActivityResultContracts.StartActivityForResult(), + vpnRequest + ) + + if (result.resultCode == RESULT_OK) + startClashService() + } finally { + pendingExternalRequest = false + } } } catch (e: Exception) { design?.showToast(DesignR.string.unable_to_start_vpn, ToastDuration.Long) diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 9f5a6b6187..237081db3e 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -40,17 +40,44 @@ class MainApplication : Application() { val uiStore = UiStore(this) var autoDestroyJob: Job? = null + // 含未保存编辑或正在等待外部结果(文件选择器、VPN 授权等)的 Activity, + // 存活时不允许杀 UI 进程 + val protectedActivities: Set> = setOf( + NewProfileActivity::class.java, + FilesActivity::class.java, + LogcatActivity::class.java, + MetaFeatureSettingsActivity::class.java, + AccessControlActivity::class.java, + OverrideSettingsActivity::class.java, + PropertiesActivity::class.java, + ) + ApplicationObserver.onVisibleChanged { visible -> - if (!visible && uiStore.autoDestroyUI && !LogcatService.running) { - autoDestroyJob?.cancel() + autoDestroyJob?.cancel() + autoDestroyJob = null + if (!visible && uiStore.autoDestroyUI) { autoDestroyJob = Global.launch { - delay(3000) + // 持续监测:有等待回调/编辑中则等待,无保护条件持续 3 秒才杀 + var idleMs = 0 + while (idleMs < 3000) { + delay(500) + val protectedNow = LogcatService.running || + ApplicationObserver.createdActivities.any { it.javaClass in protectedActivities } || + MainActivity.pendingExternalRequest + if (protectedNow) { + idleMs = 0 + } else { + idleMs += 500 + } + } Log.d("AutoDestroyUI: Killing UI process to free memory") + try { + File(clashDir, "auto_destroy.log").appendText( + "${java.util.Date()} AutoDestroyUI: Killing UI process\n" + ) + } catch (e: Exception) { } Process.killProcess(Process.myPid()) } - } else if (visible) { - autoDestroyJob?.cancel() - autoDestroyJob = null } } diff --git a/design/src/main/res/values-ja-rJP/strings.xml b/design/src/main/res/values-ja-rJP/strings.xml index e38b8bf030..028991524c 100644 --- a/design/src/main/res/values-ja-rJP/strings.xml +++ b/design/src/main/res/values-ja-rJP/strings.xml @@ -264,4 +264,6 @@ Override Destination カメラのアクセスが制限されています。設定から有効にしてください。 システムで予期しない例外が発生しました。 + UI自動破棄 + アプリがバックグラウンドに移行して3秒後にメモリ節約のためUIを自動的に破棄します diff --git a/design/src/main/res/values-ko-rKR/strings.xml b/design/src/main/res/values-ko-rKR/strings.xml index bc22550af8..82e9bf686c 100644 --- a/design/src/main/res/values-ko-rKR/strings.xml +++ b/design/src/main/res/values-ko-rKR/strings.xml @@ -264,4 +264,6 @@ Override Destination 카메라 접근이 제한되었습니다. 설정에서 허용해 주세요. 처리되지 않은 시스템 예외가 발생했습니다. + UI 자동 종료 + 앱이 백그라운드로 전환된 후 3초 후 메모리 절약을 위해 UI를 자동으로 종료합니다 diff --git a/design/src/main/res/values-ru/strings.xml b/design/src/main/res/values-ru/strings.xml index 4f680de2c5..83dcc9dc0f 100644 --- a/design/src/main/res/values-ru/strings.xml +++ b/design/src/main/res/values-ru/strings.xml @@ -328,4 +328,6 @@ Override Destination Доступ к камере ограничен. Разрешите его в настройках. Произошла не обрабатываемая системная ошибка. + Автоуничтожение UI + Автоматически уничтожать UI через 3 секунды после перехода приложения в фон для экономии памяти diff --git a/design/src/main/res/values-vi/strings.xml b/design/src/main/res/values-vi/strings.xml index 2abca48934..befb8f1691 100644 --- a/design/src/main/res/values-vi/strings.xml +++ b/design/src/main/res/values-vi/strings.xml @@ -250,4 +250,6 @@ Nhập từ Mã QR Quyền truy cập camera bị hạn chế. Vui lòng bật trong Cài đặt. Đã xảy ra ngoại lệ hệ thống không xử lý được. + Tự động hủy UI + Tự động hủy UI sau 3 giây khi ứng dụng chuyển sang nền để tiết kiệm bộ nhớ diff --git a/design/src/main/res/values-zh-rHK/strings.xml b/design/src/main/res/values-zh-rHK/strings.xml index a63dff70a4..d87a1bb18e 100644 --- a/design/src/main/res/values-zh-rHK/strings.xml +++ b/design/src/main/res/values-zh-rHK/strings.xml @@ -261,4 +261,6 @@ Override Destination 相機權限受限,請前往設定開啟。 發生系統未知異常,操作失敗。 + 自動銷毀介面 + 應用程式進入背景3秒後自動銷毀介面以節省記憶體 diff --git a/design/src/main/res/values-zh-rTW/strings.xml b/design/src/main/res/values-zh-rTW/strings.xml index 4bc185b77d..14cf639b6a 100644 --- a/design/src/main/res/values-zh-rTW/strings.xml +++ b/design/src/main/res/values-zh-rTW/strings.xml @@ -261,4 +261,6 @@ Override Destination 相機權限受限,請前往設定開啟。 發生系統未知異常,操作失敗。 + 自動銷毀介面 + 應用程式進入背景3秒後自動銷毀介面以節省記憶體