From 217561832c43c1943c1cbd00f213f5812f452cdf Mon Sep 17 00:00:00 2001 From: ItosEO Date: Thu, 16 Apr 2026 19:48:45 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E2=80=9C?= =?UTF-8?q?=E7=B2=BE=E7=A1=AE=E6=81=AF=E5=B1=8F=E8=AE=B0=E5=BD=95=E2=80=9D?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=B9=B6=E5=BC=95=E5=85=A5=E5=94=A4=E9=86=92?= =?UTF-8?q?=E9=94=81=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 `Monitor` 中实现精确息屏记录逻辑,通过 `PARTIAL_WAKE_LOCK` 确保息屏下的采样精度。 - 新增 `preciseScreenOffRecordEnabled` 配置项及其对应的 UI 设置界面与中英文描述。 - 优化唤醒锁管理逻辑,支持按需申请与释放,并处理 `FakeContext` 初始化时的线程安全问题。 - 在 `AndroidManifest.xml` 中添加 `WAKE_LOCK` 权限。 - 更新配置编解码及各组件的日志打印,支持新配置项。 --- app/src/main/AndroidManifest.xml | 1 + .../java/yangfentuozi/batteryrecorder/App.kt | 2 +- .../settings/sections/ServerSection.kt | 9 + .../ui/model/SettingsActions.kt | 1 + .../ui/model/SettingsUiState.kt | 2 + .../ui/screens/settings/SettingsScreen.kt | 2 + .../ui/viewmodel/SettingsViewModel.kt | 16 +- app/src/main/res/values-zh-rCN/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + .../batteryrecorder/server/Server.kt | 3 +- .../server/recorder/Monitor.kt | 163 +++++++++++++++++- .../shared/config/ConfigUtil.kt | 2 +- .../shared/config/ServerSettingsCodec.kt | 7 + .../shared/config/SettingsConstants.kt | 7 + .../shared/config/dataclass/ServerSettings.kt | 3 + 15 files changed, 217 insertions(+), 5 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b846e26..6372718 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + Unit, val setBatchSize: (Int) -> Unit, val setScreenOffRecordEnabled: (Boolean) -> Unit, + val setPreciseScreenOffRecordEnabled: (Boolean) -> Unit, val setAlwaysPollingScreenStatusEnabled: (Boolean) -> Unit, val setSegmentDurationMin: (Long) -> Unit, val setRootBootAutoStartEnabled: (Boolean) -> Unit diff --git a/app/src/main/java/yangfentuozi/batteryrecorder/ui/model/SettingsUiState.kt b/app/src/main/java/yangfentuozi/batteryrecorder/ui/model/SettingsUiState.kt index e765bea..b9ef740 100644 --- a/app/src/main/java/yangfentuozi/batteryrecorder/ui/model/SettingsUiState.kt +++ b/app/src/main/java/yangfentuozi/batteryrecorder/ui/model/SettingsUiState.kt @@ -49,6 +49,8 @@ data class SettingsUiState( val batchSize: Int = ServerSettings().batchSize, /** 息屏记录 */ val recordScreenOffEnabled: Boolean = ServerSettings().screenOffRecordEnabled, + /** 精确息屏记录 */ + val preciseScreenOffRecordEnabled: Boolean = ServerSettings().preciseScreenOffRecordEnabled, /** 轮询获取息屏状态 */ val alwaysPollingScreenStatusEnabled: Boolean = ServerSettings().alwaysPollingScreenStatusEnabled, /** 自动分段时间 */ diff --git a/app/src/main/java/yangfentuozi/batteryrecorder/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/yangfentuozi/batteryrecorder/ui/screens/settings/SettingsScreen.kt index 29f369f..c320236 100644 --- a/app/src/main/java/yangfentuozi/batteryrecorder/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/yangfentuozi/batteryrecorder/ui/screens/settings/SettingsScreen.kt @@ -67,6 +67,7 @@ fun SettingsScreen( setWriteLatencyMs = settingsViewModel::setWriteLatencyMs, setBatchSize = settingsViewModel::setBatchSize, setScreenOffRecordEnabled = settingsViewModel::setScreenOffRecordEnabled, + setPreciseScreenOffRecordEnabled = settingsViewModel::setPreciseScreenOffRecordEnabled, setAlwaysPollingScreenStatusEnabled = settingsViewModel::setAlwaysPollingScreenStatusEnabled, setSegmentDurationMin = settingsViewModel::setSegmentDurationMin, setRootBootAutoStartEnabled = settingsViewModel::setRootBootAutoStartEnabled @@ -101,6 +102,7 @@ fun SettingsScreen( writeLatencyMs = serverSettings.writeLatencyMs, batchSize = serverSettings.batchSize, recordScreenOffEnabled = serverSettings.screenOffRecordEnabled, + preciseScreenOffRecordEnabled = serverSettings.preciseScreenOffRecordEnabled, alwaysPollingScreenStatusEnabled = serverSettings.alwaysPollingScreenStatusEnabled, segmentDurationMin = serverSettings.segmentDurationMin, rootBootAutoStartEnabled = appSettings.rootBootAutoStartEnabled, diff --git a/app/src/main/java/yangfentuozi/batteryrecorder/ui/viewmodel/SettingsViewModel.kt b/app/src/main/java/yangfentuozi/batteryrecorder/ui/viewmodel/SettingsViewModel.kt index dbef6d6..74b953b 100644 --- a/app/src/main/java/yangfentuozi/batteryrecorder/ui/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/yangfentuozi/batteryrecorder/ui/viewmodel/SettingsViewModel.kt @@ -122,7 +122,7 @@ class SettingsViewModel : ViewModel() { LoggerX.d( TAG, - "[设置] loadSettings 完成: notification=${currentServerSettings.notificationEnabled} compatMode=${currentServerSettings.notificationCompatModeEnabled} dualCell=${currentServerSettings.dualCellEnabled} calibration=${currentServerSettings.calibrationValue} intervalMs=${currentServerSettings.recordIntervalMs} writeLatencyMs=${currentServerSettings.writeLatencyMs} batchSize=${currentServerSettings.batchSize} screenOffRecord=${currentServerSettings.screenOffRecordEnabled} polling=${currentServerSettings.alwaysPollingScreenStatusEnabled} logLevel=${currentServerSettings.logLevel}" + "[设置] loadSettings 完成: notification=${currentServerSettings.notificationEnabled} compatMode=${currentServerSettings.notificationCompatModeEnabled} dualCell=${currentServerSettings.dualCellEnabled} calibration=${currentServerSettings.calibrationValue} intervalMs=${currentServerSettings.recordIntervalMs} writeLatencyMs=${currentServerSettings.writeLatencyMs} batchSize=${currentServerSettings.batchSize} screenOffRecord=${currentServerSettings.screenOffRecordEnabled} preciseScreenOffRecord=${currentServerSettings.preciseScreenOffRecordEnabled} polling=${currentServerSettings.alwaysPollingScreenStatusEnabled} logLevel=${currentServerSettings.logLevel}" ) } @@ -236,6 +236,20 @@ class SettingsViewModel : ViewModel() { } } + /** + * 更新精确息屏记录开关并下发到运行中的服务端。 + * + * @param enabled `true` 表示允许 Server 在息屏记录阶段持有唤醒锁;`false` 表示恢复默认自然息屏采样。 + * @return 无。 + */ + fun setPreciseScreenOffRecordEnabled(enabled: Boolean) { + updateServerSettings( + message = "[设置] 更新精确息屏记录并准备下发: enabled=$enabled" + ) { current -> + current.copy(preciseScreenOffRecordEnabled = enabled) + } + } + fun setAlwaysPollingScreenStatusEnabled(enabled: Boolean) { updateServerSettings( message = "[设置] 更新轮询亮屏状态并准备下发: enabled=$enabled" diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 8930efb..f257e48 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -95,6 +95,8 @@ 实时功率通知 通知兼容模式 息屏记录 + 精确息屏记录 + 可能导致息屏耗电增高 轮询获取息屏状态 采样间隔 写入延迟 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d2ce007..d248f2d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -95,6 +95,8 @@ Realtime power notification Notification compatibility mode Record when screen is off + Precise screen-off recording + May increase screen-off power draw. Poll screen state Sampling interval Write latency diff --git a/server/src/main/java/yangfentuozi/batteryrecorder/server/Server.kt b/server/src/main/java/yangfentuozi/batteryrecorder/server/Server.kt index be8fcf6..10279dd 100644 --- a/server/src/main/java/yangfentuozi/batteryrecorder/server/Server.kt +++ b/server/src/main/java/yangfentuozi/batteryrecorder/server/Server.kt @@ -86,7 +86,7 @@ class Server internal constructor() : IService.Stub() { Handlers.common.post { LoggerX.d( tag, - "updateConfig: 应用配置, notification=${settings.notificationEnabled} compatMode=${settings.notificationCompatModeEnabled} dualCell=${settings.dualCellEnabled} calibration=${settings.calibrationValue} intervalMs=${settings.recordIntervalMs} writeLatencyMs=${settings.writeLatencyMs} batchSize=${settings.batchSize} screenOffRecord=${settings.screenOffRecordEnabled} segmentDurationMin=${settings.segmentDurationMin} logLevel=${settings.logLevel} polling=${settings.alwaysPollingScreenStatusEnabled}" + "updateConfig: 应用配置, notification=${settings.notificationEnabled} compatMode=${settings.notificationCompatModeEnabled} dualCell=${settings.dualCellEnabled} calibration=${settings.calibrationValue} intervalMs=${settings.recordIntervalMs} writeLatencyMs=${settings.writeLatencyMs} batchSize=${settings.batchSize} screenOffRecord=${settings.screenOffRecordEnabled} preciseScreenOffRecord=${settings.preciseScreenOffRecordEnabled} segmentDurationMin=${settings.segmentDurationMin} logLevel=${settings.logLevel} polling=${settings.alwaysPollingScreenStatusEnabled}" ) LoggerX.maxHistoryDays = settings.maxHistoryDays LoggerX.logLevel = settings.logLevel @@ -102,6 +102,7 @@ class Server internal constructor() : IService.Stub() { monitor.alwaysPollingScreenStatusEnabled = settings.alwaysPollingScreenStatusEnabled monitor.recordIntervalMs = settings.recordIntervalMs monitor.screenOffRecord = settings.screenOffRecordEnabled + monitor.preciseScreenOffRecordEnabled = settings.preciseScreenOffRecordEnabled monitor.notifyLock() writer.flushIntervalMs = settings.writeLatencyMs diff --git a/server/src/main/java/yangfentuozi/batteryrecorder/server/recorder/Monitor.kt b/server/src/main/java/yangfentuozi/batteryrecorder/server/recorder/Monitor.kt index ebf507f..5d1c5a6 100644 --- a/server/src/main/java/yangfentuozi/batteryrecorder/server/recorder/Monitor.kt +++ b/server/src/main/java/yangfentuozi/batteryrecorder/server/recorder/Monitor.kt @@ -9,11 +9,13 @@ import android.hardware.display.IDisplayManager import android.hardware.display.IDisplayManagerCallback import android.os.Handler import android.os.IPowerManager +import android.os.PowerManager import android.os.RemoteCallbackList import android.os.RemoteException import android.os.ServiceManager import android.system.Os import androidx.annotation.Keep +import yangfentuozi.batteryrecorder.server.fakecontext.FakeContext import yangfentuozi.batteryrecorder.server.notification.LocalNotificationUtil import yangfentuozi.batteryrecorder.server.notification.NotificationInfo import yangfentuozi.batteryrecorder.server.notification.NotificationUtil @@ -68,6 +70,23 @@ class Monitor( @Volatile var screenOffRecord: Boolean = SettingsConstants.screenOffRecordEnabled.def + set(value) { + val oldValue = field + if (oldValue == value) return + field = value + LoggerX.d(tag, "screenOffRecord: 配置变更, $oldValue -> $value") + updatePreciseScreenOffWakeLockState() + } + + @Volatile + var preciseScreenOffRecordEnabled: Boolean = SettingsConstants.preciseScreenOffRecordEnabled.def + set(value) { + val oldValue = field + if (oldValue == value) return + field = value + LoggerX.d(tag, "preciseScreenOffRecordEnabled: 配置变更, $oldValue -> $value") + updatePreciseScreenOffWakeLockState() + } @Volatile var notificationUtil: NotificationUtil? = null @@ -107,6 +126,8 @@ class Monitor( @Volatile private var stopped = false + private var preciseScreenOffWakeLock: PowerManager.WakeLock? = null + private var lastPreciseScreenOffWakeLockDecisionReason: String? = null private val lock = ReentrantLock() private val condition = lock.newCondition() private val callbackHandler: Handler @@ -127,6 +148,7 @@ class Monitor( LoggerX.d(tag, "@thread: 亮屏状态变化, $oldIsInteractive -> $latestIsInteractive") } isInteractive = latestIsInteractive + updatePreciseScreenOffWakeLockState() } val record = LineRecord( timestamp, @@ -195,6 +217,7 @@ class Monitor( while (!stopped && !screenOffRecord && !isInteractive && alwaysPollingScreenStatusEnabled) { condition.await(recordIntervalMs, TimeUnit.MILLISECONDS) isInteractive = iPowerManager.isInteractive + updatePreciseScreenOffWakeLockState() } } else { LoggerX.d(tag, "@thread: 暂停采样, 等待亮屏事件") @@ -208,8 +231,9 @@ class Monitor( fun start() { LoggerX.d(tag, - "start: alwaysPollingScreenStatusEnabled=$alwaysPollingScreenStatusEnabled screenOffRecord=$screenOffRecord" + "start: alwaysPollingScreenStatusEnabled=$alwaysPollingScreenStatusEnabled screenOffRecord=$screenOffRecord preciseScreenOffRecordEnabled=$preciseScreenOffRecordEnabled" ) + preparePreciseScreenOffWakeLock() try { iActivityTaskManager.registerTaskStackListener(taskStackListener) if (!alwaysPollingScreenStatusEnabled) { @@ -226,10 +250,27 @@ class Monitor( throw RuntimeException("start: 获取当前焦点任务信息失败", e) } isInteractive = iPowerManager.isInteractive + updatePreciseScreenOffWakeLockState() LoggerX.d(tag, "start: initial isInteractive=$isInteractive") thread.start() } + /** + * 在 Server 主线程预创建精确息屏记录的唤醒锁实例,避免首次息屏回调落在 Binder 线程时 + * 触发 `FakeContext.systemContext` 的惰性初始化,进而命中无 Looper 线程创建 ActivityThread 的崩溃。 + * + * @return 无;预创建失败时仅记日志,保留后续再次尝试的机会。 + */ + private fun preparePreciseScreenOffWakeLock() { + runCatching { requirePreciseScreenOffWakeLock() } + .onSuccess { + LoggerX.d(tag, "preparePreciseScreenOffWakeLock: 唤醒锁实例预创建完成") + } + .onFailure { error -> + LoggerX.e(tag, "preparePreciseScreenOffWakeLock: 唤醒锁实例预创建失败", tr = error) + } + } + private fun registerDisplayEventCallback() { if (displayCallbackRegistered) { LoggerX.v(tag, "registerDisplayEventCallback: 已注册, skip") @@ -246,6 +287,7 @@ class Monitor( val oldIsInteractive = isInteractive val latestIsInteractive = iPowerManager.isInteractive isInteractive = latestIsInteractive + updatePreciseScreenOffWakeLockState() LoggerX.d(tag, "onDisplayEvent: displayId=$displayId event=$event interactive $oldIsInteractive -> $latestIsInteractive paused=$paused" ) @@ -271,6 +313,7 @@ class Monitor( fun stop() { stopped = true notifyLock() + releasePreciseScreenOffWakeLockIfHeld("服务停止") try { iActivityTaskManager.unregisterTaskStackListener(taskStackListener) } catch (e: RemoteException) { @@ -355,8 +398,126 @@ class Monitor( } } + /** + * 按当前屏幕状态与配置同步精确息屏记录的唤醒锁状态。 + * + * 这里只在“开启精确息屏记录 + 开启息屏记录 + 当前确实处于息屏”时持锁, + * 避免把亮屏阶段也变成常驻保活,额外抬高非目标场景功耗。 + * + * @return 无;状态不满足时会主动释放已持有的唤醒锁。 + */ + @Synchronized + private fun updatePreciseScreenOffWakeLockState() { + val shouldHoldWakeLock = + preciseScreenOffRecordEnabled && + screenOffRecord && + !isInteractive && + !stopped + val currentReason = when { + stopped -> "服务停止" + isInteractive -> "屏幕亮起" + !screenOffRecord -> "息屏记录关闭" + !preciseScreenOffRecordEnabled -> "精确息屏记录关闭" + else -> "满足持锁条件" + } + + if (shouldHoldWakeLock) { + val wakeLock = requirePreciseScreenOffWakeLock() + if (!wakeLock.isHeld) { + LoggerX.d( + tag, + "updatePreciseScreenOffWakeLockState: 准备持有唤醒锁, preciseScreenOffRecordEnabled=$preciseScreenOffRecordEnabled screenOffRecord=$screenOffRecord isInteractive=$isInteractive stopped=$stopped" + ) + wakeLock.acquire() + if (wakeLock.isHeld) { + LoggerX.i(tag, "updatePreciseScreenOffWakeLockState: 已持有唤醒锁") + } else { + LoggerX.w(tag, "updatePreciseScreenOffWakeLockState: acquire() 返回后仍未持有唤醒锁") + } + } else { + LoggerX.d(tag, "updatePreciseScreenOffWakeLockState: 唤醒锁已处于持有状态,跳过重复 acquire") + } + lastPreciseScreenOffWakeLockDecisionReason = currentReason + return + } + + val wakeLock = preciseScreenOffWakeLock + if (wakeLock?.isHeld == true) { + releasePreciseScreenOffWakeLockIfHeld(currentReason) + lastPreciseScreenOffWakeLockDecisionReason = currentReason + return + } + if (lastPreciseScreenOffWakeLockDecisionReason != currentReason) { + LoggerX.d( + tag, + "updatePreciseScreenOffWakeLockState: 当前不持有唤醒锁, reason=$currentReason preciseScreenOffRecordEnabled=$preciseScreenOffRecordEnabled screenOffRecord=$screenOffRecord isInteractive=$isInteractive stopped=$stopped" + ) + lastPreciseScreenOffWakeLockDecisionReason = currentReason + } + } + + /** + * 释放当前已持有的精确息屏记录唤醒锁。 + * + * @param reason 本次释放的直接原因,用于日志定位。 + * @return 无;若尚未创建或当前未持锁则直接返回。 + */ + @Synchronized + private fun releasePreciseScreenOffWakeLockIfHeld(reason: String) { + val wakeLock = preciseScreenOffWakeLock ?: return + if (!wakeLock.isHeld) return + LoggerX.d(tag, "releasePreciseScreenOffWakeLockIfHeld: 准备释放唤醒锁, reason=$reason") + wakeLock.release() + if (!wakeLock.isHeld) { + LoggerX.i(tag, "releasePreciseScreenOffWakeLockIfHeld: 已释放唤醒锁, reason=$reason") + } else { + LoggerX.w(tag, "releasePreciseScreenOffWakeLockIfHeld: release() 返回后仍处于持有状态, reason=$reason") + } + } + + /** + * 获取精确息屏记录使用的部分唤醒锁实例。 + * + * @return 返回单例 `PARTIAL_WAKE_LOCK`;若系统 `PowerManager` 不可用则直接抛错暴露问题。 + */ + private fun requirePreciseScreenOffWakeLock(): PowerManager.WakeLock { + preciseScreenOffWakeLock?.let { + LoggerX.d( + tag, + "requirePreciseScreenOffWakeLock: 复用已有唤醒锁实例, held=${it.isHeld}" + ) + return it + } + try { + LoggerX.d(tag, "requirePreciseScreenOffWakeLock: 开始创建唤醒锁实例") + LoggerX.d(tag, "requirePreciseScreenOffWakeLock: 开始获取 systemContext") + val systemContext = FakeContext.systemContext + LoggerX.d(tag, "requirePreciseScreenOffWakeLock: 获取 systemContext 成功, context=${systemContext.javaClass.name}") + LoggerX.d(tag, "requirePreciseScreenOffWakeLock: 开始获取 PowerManager") + val powerManager = systemContext.getSystemService(PowerManager::class.java) + ?: throw IllegalStateException("获取 PowerManager 失败") + LoggerX.d(tag, "requirePreciseScreenOffWakeLock: 获取 PowerManager 成功, service=${powerManager.javaClass.name}") + LoggerX.d(tag, "requirePreciseScreenOffWakeLock: 开始调用 newWakeLock, tag=$PRECISE_SCREEN_OFF_WAKE_LOCK_TAG") + return powerManager.newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, + PRECISE_SCREEN_OFF_WAKE_LOCK_TAG + ).apply { + setReferenceCounted(false) + preciseScreenOffWakeLock = this + LoggerX.d( + tag, + "requirePreciseScreenOffWakeLock: 唤醒锁实例创建完成, tag=$PRECISE_SCREEN_OFF_WAKE_LOCK_TAG held=$isHeld" + ) + } + } catch (t: Throwable) { + LoggerX.e(tag, "requirePreciseScreenOffWakeLock: 创建唤醒锁失败", tr = t) + throw t + } + } + companion object { private const val POWER_SCALE_DIVISOR = 1_000_000_000_000.0 + private const val PRECISE_SCREEN_OFF_WAKE_LOCK_TAG = "BatteryRecorder:PreciseScreenOffRecord" fun computeNotificationPowerMultiplier( dualCellEnabled: Boolean, diff --git a/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/ConfigUtil.kt b/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/ConfigUtil.kt index 1a1d54d..eff70f2 100644 --- a/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/ConfigUtil.kt +++ b/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/ConfigUtil.kt @@ -116,7 +116,7 @@ object ConfigUtil { private fun logServerSettings(source: String, settings: ServerSettings) { LoggerX.d( TAG, - "$source: notification=${settings.notificationEnabled} compatMode=${settings.notificationCompatModeEnabled} dualCell=${settings.dualCellEnabled} calibration=${settings.calibrationValue} intervalMs=${settings.recordIntervalMs} batchSize=${settings.batchSize} writeLatencyMs=${settings.writeLatencyMs} screenOffRecord=${settings.screenOffRecordEnabled} polling=${settings.alwaysPollingScreenStatusEnabled} logLevel=${settings.logLevel}" + "$source: notification=${settings.notificationEnabled} compatMode=${settings.notificationCompatModeEnabled} dualCell=${settings.dualCellEnabled} calibration=${settings.calibrationValue} intervalMs=${settings.recordIntervalMs} batchSize=${settings.batchSize} writeLatencyMs=${settings.writeLatencyMs} screenOffRecord=${settings.screenOffRecordEnabled} preciseScreenOffRecord=${settings.preciseScreenOffRecordEnabled} polling=${settings.alwaysPollingScreenStatusEnabled} logLevel=${settings.logLevel}" ) } } diff --git a/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/ServerSettingsCodec.kt b/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/ServerSettingsCodec.kt index 58a416f..dc1bf6d 100644 --- a/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/ServerSettingsCodec.kt +++ b/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/ServerSettingsCodec.kt @@ -85,6 +85,10 @@ object ServerSettingsCodec { SettingsConstants.screenOffRecordEnabled.key, settings.screenOffRecordEnabled ) + editor.putBoolean( + SettingsConstants.preciseScreenOffRecordEnabled.key, + settings.preciseScreenOffRecordEnabled + ) editor.putLong(SettingsConstants.segmentDurationMin.key, settings.segmentDurationMin) editor.putLong(SettingsConstants.logMaxHistoryDays.key, settings.maxHistoryDays) editor.putInt( @@ -123,6 +127,9 @@ object ServerSettingsCodec { screenOffRecordEnabled = source.boolean(SettingsConstants.screenOffRecordEnabled.key) ?: SettingsConstants.screenOffRecordEnabled.def, + preciseScreenOffRecordEnabled = + source.boolean(SettingsConstants.preciseScreenOffRecordEnabled.key) + ?: SettingsConstants.preciseScreenOffRecordEnabled.def, segmentDurationMin = source.long(SettingsConstants.segmentDurationMin.key) ?: SettingsConstants.segmentDurationMin.def, diff --git a/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/SettingsConstants.kt b/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/SettingsConstants.kt index a36ecb4..b47a471 100644 --- a/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/SettingsConstants.kt +++ b/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/SettingsConstants.kt @@ -87,6 +87,13 @@ object SettingsConstants { def = true ) + /** 是否启用精确息屏记录;开启后 Server 会在息屏记录阶段持有唤醒锁。 */ + val preciseScreenOffRecordEnabled = + BooleanConfigItem( + key = "precise_screen_off_record_enabled", + def = false + ) + /** 数据分段时长(分钟) */ val segmentDurationMin = LongConfigItem( diff --git a/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/dataclass/ServerSettings.kt b/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/dataclass/ServerSettings.kt index b924fc6..aab132b 100644 --- a/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/dataclass/ServerSettings.kt +++ b/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/dataclass/ServerSettings.kt @@ -33,6 +33,9 @@ data class ServerSettings( val writeLatencyMs: Long = SettingsConstants.writeLatencyMs.def, /** 息屏后是否继续采样与记录。 */ val screenOffRecordEnabled: Boolean = SettingsConstants.screenOffRecordEnabled.def, + /** 是否在息屏记录阶段持有唤醒锁,提升采样定时精度。 */ + val preciseScreenOffRecordEnabled: Boolean = + SettingsConstants.preciseScreenOffRecordEnabled.def, /** 单个记录文件按时间自动分段的时长,单位分钟,0 表示不分段。 */ val segmentDurationMin: Long = SettingsConstants.segmentDurationMin.def, /** 日志文件保留天数。 */ From 18f55b656d083ba1f237f955edc04f50d33ff969 Mon Sep 17 00:00:00 2001 From: ItosEO Date: Thu, 16 Apr 2026 21:04:52 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E7=B2=BE=E7=A1=AE?= =?UTF-8?q?=E6=81=AF=E5=B1=8F=E8=AE=B0=E5=BD=95=E7=9A=84=E5=94=A4=E9=86=92?= =?UTF-8?q?=E9=94=81=E5=88=9D=E5=A7=8B=E5=8C=96=E4=B8=8E=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 调整 `preparePreciseScreenOffWakeLock` 的触发时机,仅在功能启用时进行预加载。 - 引入 `preciseScreenOffWakeLockDisabledForCurrentServer` 标志位,在初始化失败时静默禁用该功能,避免在 Binder 回调线程中产生异常。 - 优化唤醒锁状态更新逻辑,增加对初始化状态的校验并完善日志记录。 - 修复 `requirePreciseScreenOffWakeLock` 的异常处理,防止因系统服务不可用导致的潜在崩溃。 --- .../server/recorder/Monitor.kt | 49 +++++++++++++------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/yangfentuozi/batteryrecorder/server/recorder/Monitor.kt b/server/src/main/java/yangfentuozi/batteryrecorder/server/recorder/Monitor.kt index 5d1c5a6..2db8e1c 100644 --- a/server/src/main/java/yangfentuozi/batteryrecorder/server/recorder/Monitor.kt +++ b/server/src/main/java/yangfentuozi/batteryrecorder/server/recorder/Monitor.kt @@ -85,6 +85,9 @@ class Monitor( if (oldValue == value) return field = value LoggerX.d(tag, "preciseScreenOffRecordEnabled: 配置变更, $oldValue -> $value") + if (!oldValue && value) { + preparePreciseScreenOffWakeLock() + } updatePreciseScreenOffWakeLockState() } @@ -127,6 +130,8 @@ class Monitor( @Volatile private var stopped = false private var preciseScreenOffWakeLock: PowerManager.WakeLock? = null + @Volatile + private var preciseScreenOffWakeLockDisabledForCurrentServer = false private var lastPreciseScreenOffWakeLockDecisionReason: String? = null private val lock = ReentrantLock() private val condition = lock.newCondition() @@ -233,7 +238,9 @@ class Monitor( LoggerX.d(tag, "start: alwaysPollingScreenStatusEnabled=$alwaysPollingScreenStatusEnabled screenOffRecord=$screenOffRecord preciseScreenOffRecordEnabled=$preciseScreenOffRecordEnabled" ) - preparePreciseScreenOffWakeLock() + if (preciseScreenOffRecordEnabled) { + preparePreciseScreenOffWakeLock() + } try { iActivityTaskManager.registerTaskStackListener(taskStackListener) if (!alwaysPollingScreenStatusEnabled) { @@ -256,19 +263,16 @@ class Monitor( } /** - * 在 Server 主线程预创建精确息屏记录的唤醒锁实例,避免首次息屏回调落在 Binder 线程时 + * 在非 Binder 回调线程预创建精确息屏记录的唤醒锁实例,避免首次息屏回调落在 Binder 线程时 * 触发 `FakeContext.systemContext` 的惰性初始化,进而命中无 Looper 线程创建 ActivityThread 的崩溃。 * - * @return 无;预创建失败时仅记日志,保留后续再次尝试的机会。 + * @return 无;预创建失败时会在当前 Server 生命周期内静默禁用该功能。 */ private fun preparePreciseScreenOffWakeLock() { - runCatching { requirePreciseScreenOffWakeLock() } - .onSuccess { - LoggerX.d(tag, "preparePreciseScreenOffWakeLock: 唤醒锁实例预创建完成") - } - .onFailure { error -> - LoggerX.e(tag, "preparePreciseScreenOffWakeLock: 唤醒锁实例预创建失败", tr = error) - } + val wakeLock = requirePreciseScreenOffWakeLock() + if (wakeLock != null) { + LoggerX.d(tag, "preparePreciseScreenOffWakeLock: 唤醒锁实例预创建完成") + } } private fun registerDisplayEventCallback() { @@ -410,19 +414,26 @@ class Monitor( private fun updatePreciseScreenOffWakeLockState() { val shouldHoldWakeLock = preciseScreenOffRecordEnabled && + !preciseScreenOffWakeLockDisabledForCurrentServer && screenOffRecord && !isInteractive && !stopped val currentReason = when { stopped -> "服务停止" + preciseScreenOffWakeLockDisabledForCurrentServer -> "当前服务已禁用精确息屏记录" isInteractive -> "屏幕亮起" !screenOffRecord -> "息屏记录关闭" !preciseScreenOffRecordEnabled -> "精确息屏记录关闭" else -> "满足持锁条件" } + val wakeLock = preciseScreenOffWakeLock if (shouldHoldWakeLock) { - val wakeLock = requirePreciseScreenOffWakeLock() + if (wakeLock == null) { + LoggerX.e(tag, "updatePreciseScreenOffWakeLockState: 满足持锁条件但唤醒锁尚未初始化") + lastPreciseScreenOffWakeLockDecisionReason = "唤醒锁未初始化" + return + } if (!wakeLock.isHeld) { LoggerX.d( tag, @@ -441,7 +452,6 @@ class Monitor( return } - val wakeLock = preciseScreenOffWakeLock if (wakeLock?.isHeld == true) { releasePreciseScreenOffWakeLockIfHeld(currentReason) lastPreciseScreenOffWakeLockDecisionReason = currentReason @@ -478,9 +488,15 @@ class Monitor( /** * 获取精确息屏记录使用的部分唤醒锁实例。 * - * @return 返回单例 `PARTIAL_WAKE_LOCK`;若系统 `PowerManager` 不可用则直接抛错暴露问题。 + * 初始化失败时仅记录错误,并在当前 Server 生命周期内静默禁用该功能,避免在回调线程反复抛异常。 + * + * @return 返回单例 `PARTIAL_WAKE_LOCK`;当前服务已禁用该能力时返回 `null`。 */ - private fun requirePreciseScreenOffWakeLock(): PowerManager.WakeLock { + private fun requirePreciseScreenOffWakeLock(): PowerManager.WakeLock? { + if (preciseScreenOffWakeLockDisabledForCurrentServer) { + LoggerX.d(tag, "requirePreciseScreenOffWakeLock: 当前服务已禁用精确息屏记录,跳过唤醒锁初始化") + return null + } preciseScreenOffWakeLock?.let { LoggerX.d( tag, @@ -511,7 +527,10 @@ class Monitor( } } catch (t: Throwable) { LoggerX.e(tag, "requirePreciseScreenOffWakeLock: 创建唤醒锁失败", tr = t) - throw t + preciseScreenOffWakeLockDisabledForCurrentServer = true + preciseScreenOffWakeLock = null + LoggerX.w(tag, "requirePreciseScreenOffWakeLock: 当前服务已静默禁用精确息屏记录") + return null } } From ec164cfacf5c39f1b8b16d42f53099b60a402117 Mon Sep 17 00:00:00 2001 From: ItosEO Date: Fri, 17 Apr 2026 08:20:03 +0800 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=BA=94=E7=94=A8=E9=80=BB=E8=BE=91=EF=BC=8C=E6=8F=90?= =?UTF-8?q?=E5=8F=96=E5=86=85=E9=83=A8=E6=96=B9=E6=B3=95=E4=BB=A5=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=88=9D=E5=A7=8B=E5=8C=96=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 提取 `applyConfigInternal` 私有方法,统一管理服务端配置的应用逻辑。 - 优化 `init` 流程,在初始化时直接调用内部方法应用配置,避免不必要的线程切换。 - 在配置应用日志中增加 `source` 参数,便于追踪配置来源。 --- .../batteryrecorder/server/Server.kt | 62 +++++++++++-------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/server/src/main/java/yangfentuozi/batteryrecorder/server/Server.kt b/server/src/main/java/yangfentuozi/batteryrecorder/server/Server.kt index 10279dd..2d540d0 100644 --- a/server/src/main/java/yangfentuozi/batteryrecorder/server/Server.kt +++ b/server/src/main/java/yangfentuozi/batteryrecorder/server/Server.kt @@ -82,32 +82,43 @@ class Server internal constructor() : IService.Stub() { monitor.unregisterRecordListener(listener) } - override fun updateConfig(settings: ServerSettings) { - Handlers.common.post { - LoggerX.d( - tag, - "updateConfig: 应用配置, notification=${settings.notificationEnabled} compatMode=${settings.notificationCompatModeEnabled} dualCell=${settings.dualCellEnabled} calibration=${settings.calibrationValue} intervalMs=${settings.recordIntervalMs} writeLatencyMs=${settings.writeLatencyMs} batchSize=${settings.batchSize} screenOffRecord=${settings.screenOffRecordEnabled} preciseScreenOffRecord=${settings.preciseScreenOffRecordEnabled} segmentDurationMin=${settings.segmentDurationMin} logLevel=${settings.logLevel} polling=${settings.alwaysPollingScreenStatusEnabled}" - ) - LoggerX.maxHistoryDays = settings.maxHistoryDays - LoggerX.logLevel = settings.logLevel + /** + * 应用一份服务端配置到当前运行时实例。 + * + * @param settings 要生效的服务端配置。 + * @param source 本次配置应用来源,仅用于日志定位。 + * @return 无。 + */ + private fun applyConfigInternal(settings: ServerSettings, source: String) { + LoggerX.d( + tag, + "$source: 应用配置, notification=${settings.notificationEnabled} compatMode=${settings.notificationCompatModeEnabled} dualCell=${settings.dualCellEnabled} calibration=${settings.calibrationValue} intervalMs=${settings.recordIntervalMs} writeLatencyMs=${settings.writeLatencyMs} batchSize=${settings.batchSize} screenOffRecord=${settings.screenOffRecordEnabled} preciseScreenOffRecord=${settings.preciseScreenOffRecordEnabled} segmentDurationMin=${settings.segmentDurationMin} logLevel=${settings.logLevel} polling=${settings.alwaysPollingScreenStatusEnabled}" + ) + LoggerX.maxHistoryDays = settings.maxHistoryDays + LoggerX.logLevel = settings.logLevel - unlockOPlusSampleTimeLimit(settings.recordIntervalMs.coerceAtLeast(200)) + unlockOPlusSampleTimeLimit(settings.recordIntervalMs.coerceAtLeast(200)) - monitor.notificationPowerMultiplier = computeNotificationPowerMultiplier( - dualCellEnabled = settings.dualCellEnabled, - calibrationValue = settings.calibrationValue, - ) - monitor.setNotificationCompatModeEnabled(settings.notificationCompatModeEnabled) - monitor.setNotificationEnabled(settings.notificationEnabled) - monitor.alwaysPollingScreenStatusEnabled = settings.alwaysPollingScreenStatusEnabled - monitor.recordIntervalMs = settings.recordIntervalMs - monitor.screenOffRecord = settings.screenOffRecordEnabled - monitor.preciseScreenOffRecordEnabled = settings.preciseScreenOffRecordEnabled - monitor.notifyLock() - - writer.flushIntervalMs = settings.writeLatencyMs - writer.batchSize = settings.batchSize - writer.maxSegmentDurationMs = settings.segmentDurationMin * 60 * 1000L + monitor.notificationPowerMultiplier = computeNotificationPowerMultiplier( + dualCellEnabled = settings.dualCellEnabled, + calibrationValue = settings.calibrationValue, + ) + monitor.setNotificationCompatModeEnabled(settings.notificationCompatModeEnabled) + monitor.setNotificationEnabled(settings.notificationEnabled) + monitor.alwaysPollingScreenStatusEnabled = settings.alwaysPollingScreenStatusEnabled + monitor.recordIntervalMs = settings.recordIntervalMs + monitor.screenOffRecord = settings.screenOffRecordEnabled + monitor.preciseScreenOffRecordEnabled = settings.preciseScreenOffRecordEnabled + monitor.notifyLock() + + writer.flushIntervalMs = settings.writeLatencyMs + writer.batchSize = settings.batchSize + writer.maxSegmentDurationMs = settings.segmentDurationMin * 60 * 1000L + } + + override fun updateConfig(settings: ServerSettings) { + Handlers.common.post { + applyConfigInternal(settings, "updateConfig") } } @@ -474,7 +485,8 @@ class Server internal constructor() : IService.Stub() { LoggerX.i(tag, "init: 通过 ConfigProvider 读取配置") ConfigUtil.getServerSettingsByContentProvider() } - serverSettings?.let(::updateConfig) ?: LoggerX.w(tag, "init: 未读取到配置, 使用当前默认值") + serverSettings?.let { applyConfigInternal(it, "init") } + ?: LoggerX.w(tag, "init: 未读取到配置, 使用当前默认值") monitor.start() LoggerX.i(tag, "init: Monitor 已启动, 进入消息循环")