深入 Android 动态性能框架 ADPF 全链路:从 Thermal API 到 Performance Hint 的自适应性能调度

我在一个 3D 游戏里把帧率锁在 60,渲染线程的帧间隔仍会从 16ms 突然拉到 30-40ms。查 Trace 看到 CPU 频率断崖式下跌,代码里没有任何逻辑变化。这就是热降频:芯片温度突破阈值后,调度器先降频再恢复,整个过程里应用层只能”挨打”。

ADPF(Android Dynamic Performance Framework)把三个能力串起来:Thermal API 报告热状态和余量、Performance Hint API 让系统按目标帧预算调频、Game Mode API 提供用户级的性能取向。三者配合后,应用可以在过热降频发生前主动降载,把掉帧曲线拉平。

先感知:热状态与热余量

Thermal API 有两层。第一层是状态监听,PowerManager 提供从 THERMAL_STATUS_NONE 到 THERMAL_STATUS_SHUTDOWN 的多级状态:

val pm = context.getSystemService(PowerManager::class.java)
val executor = Executors.newSingleThreadExecutor()

pm.addThermalStatusListener(executor) { status ->
    when (status) {
        PowerManager.THERMAL_STATUS_LIGHT -> renderer.qualityScale = 0.85f
        PowerManager.THERMAL_STATUS_MODERATE -> renderer.qualityScale = 0.65f
        PowerManager.THERMAL_STATUS_SEVERE -> renderer.qualityScale = 0.5f
        else -> renderer.qualityScale = 1.0f
    }
}

这个回调的缺点是事件式:只有跨过阈值才触发,而且不少机型的状态跳变很粗,LIGHT 之后可能直接跳到 SEVERE。用它做降载,动作往往已经慢了一拍。

第二层 getThermalHeadroom 回答”未来会不会热”:

val headroom = pm.getThermalHeadroom(forecastSeconds = 10)
if (!headroom.isNaN() && headroom >= 0.7f) {
    renderer.enableLowPowerPath()
}

headroom 是一个从 0 起步、可能超过 1.0 的余量估计值:越接近或超过 1.0,表示越接近严重热节流(throttling),是危险信号;越接近 0,表示散热余量越充足、越安全。forecastSeconds 传 0 获取当前值,传 10 获取 10 秒后的预测。预测靠厂商实现,返回 NaN 表示不支持。我在 Pixel 和几台国产旗舰上验证,10 秒预测基本能提前覆盖一次高负载场景的升温过程,比状态回调早 3-5 秒。

再执行:用 Performance Hint 对齐调频

感知到热之后,降载不能只改画面质量。如果调度器继续误判渲染线程的真实耗时,系统可能还在按 60 帧的高负载目标给 CPU 提频,降载效果会打折。

PerformanceHintManager 提供会话机制,让应用告诉系统”我下一帧的预算是多少、实际花了多少”:

val hintManager = context.getSystemService(PerformanceHintManager::class.java)
val session = hintManager.createHintSession(
    intArrayOf(renderThread.id, gameThread.id),
    16L * 1_000_000L
)

// 每帧渲染结束后上报实际耗时
session.reportActualWorkDuration(actualFrameCostMillis * 1_000_000L)

创建会话时传入的线程必须是长期存活的渲染或逻辑线程,系统会观察这些线程的负载模式。API 的时间单位是纳秒,所以毫秒值要乘 1_000_000。目标时长是核心输入:16ms 对应 60 帧,22ms 对应 45 帧,33ms 对应 30 帧。热状态变化时动态调整目标:

fun adaptToThermal(headroom: Float, status: Int) {
    val target = when {
        status >= PowerManager.THERMAL_STATUS_SEVERE -> 33L
        headroom >= 0.7f -> 22L
        else -> 16L
    }
    hintSession.updateTargetWorkDuration(target * 1_000_000L)
}

有一个容易忽略的点:updateTargetWorkDuration 不是改渲染线程的等待时间,它改的是系统对这批线程的频率与核心选择预期。真正的降载还是要渲染器同步把帧预算降下来,两边对齐才有效。

定基线:Game Mode 是策略入口

Game Mode API 是 ADPF 里偏向用户策略的一层。玩家在系统界面选”性能模式”或”省电模式”后,应用通过 GameManager 读取:

val gameManager = context.getSystemService(GameManager::class.java)
val baseBudget = when (gameManager.gameMode) {
    GameManager.GAME_MODE_PERFORMANCE -> 16L
    GameManager.GAME_MODE_BATTERY -> 33L
    else -> 22L
}

我倾向于把 Game Mode 作为基线,而不是热状态下的自动开关。性能模式给 16ms 的目标,省电模式直接给 33ms;热状态在这个基线上做收紧。不要反过来在过热时调用 setGameMode 切换系统模式,部分厂商会弹确认框,而且用户明确选的模式不该被应用偷偷改写。

串起来后的三个落地点

把三套 API 串起来后,我项目里的调度逻辑收敛成一条:

fun onFrameStart() {
    val modeBudget = gameModeBudget(gameManager.gameMode)
    val headroom = pm.getThermalHeadroom(0)
    val status = pm.currentThermalStatus

    val budget = when {
        status >= PowerManager.THERMAL_STATUS_SEVERE -> 33L
        headroom >= 0.7f -> 22L
        else -> modeBudget
    }

    if (budget != lastBudget) {
        hintSession.updateTargetWorkDuration(budget * 1_000_000L)
        renderer.applyQualityByBudget(budget)
        lastBudget = budget
    }
}

实际落地有 3 条经验。第一,headroom 要采样,不要只在回调里读:状态回调在部分设备上过慢,按 1 秒周期采样 headroom 反而更稳。第二,预算和画质必须同时降:只改 updateTargetWorkDuration 不改渲染负载,等于让系统追着一个不存在的帧预算跑。第三,用 22ms 做中间档:直接 60 跳 30 会带来肉眼可见的顿挫,45 帧的中间态让玩家更不容易察觉性能回收。