基于 Choreographer 回调 设计卡顿监控

为了设计一个基于 Choreographer 回调的卡顿监控方案,可以通过监听帧的绘制时间来检测可能的卡顿(掉帧)。Choreographer 是 Android 中用于协调屏幕绘制的类,它提供了一个接口,可以在每一帧绘制时调用。下面是一个基于 Choreographer 回调的卡顿监控方案的实现步骤:

  1. 创建 Choreographer.FrameCallback 实现类:创建一个类,实现 Choreographer.FrameCallback 接口,用于接收每一帧的回调。

  2. 在每一帧绘制时记录时间戳:在 FrameCallback 的 doFrame() 方法中记录每一帧的时间戳,并计算两帧之间的时间差。如果时间差超过一定阈值(例如 16ms,即60帧每秒),则认为发生了卡顿。

  3. 记录和处理卡顿信息:当检测到卡顿时,可以记录相关信息,例如发生时间、持续时间等,或者执行相应的处理逻辑,例如上报监控数据。

  4. 定期更新 FrameCallback:为了持续监控帧率,需要在每次回调中重新安排下一个回调。

示例代码

import android.os.Handler;
import android.os.Looper;
import android.view.Choreographer;

public class FrameRateMonitor {

    private long frameIntervalMillis;

    private Choreographer choreographer;
    private long lastFrameTimeNanos = 0;
    private FrameCallback frameCallback;

    public FrameRateMonitor() {
        choreographer = Choreographer.getInstance();
        frameCallback = new FrameCallback();
        frameIntervalMillis = getFrameIntervalMillis(getAppContext());
    }
    
    private long getFrameIntervalMillis(Activity activity) {
        Display display;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            display = activity.getDisplay();
        } else {
            WindowManager windowManager = (WindowManager) activity.getSystemService(Activity.WINDOW_SERVICE);
            display = windowManager.getDefaultDisplay();
        }

        float refreshRate = display.getRefreshRate();
        return (long) (1000 / refreshRate);
    }
    

    public void start() {
        choreographer.postFrameCallback(frameCallback);
    }

    public void stop() {
        choreographer.removeFrameCallback(frameCallback);
    }

    private class FrameCallback implements Choreographer.FrameCallback {
        @Override
        public void doFrame(long frameTimeNanos) {
            if (lastFrameTimeNanos != 0) {
                long timeDiffNanos = frameTimeNanos - lastFrameTimeNanos;
                if (timeDiffNanos > frameIntervalMillis) {
                    long droppedFrames = timeDiffNanos / frameIntervalMillis;
                    onFrameDropped(droppedFrames);
                }
            }
            lastFrameTimeNanos = frameTimeNanos;
            choreographer.postFrameCallback(this);
        }
    }

    private void onFrameDropped(long droppedFrames) {
        // 处理卡顿逻辑,例如记录日志或上报监控数据
        System.out.println("Frame dropped: " + droppedFrames);
    }
}

关键点说明

  • Choreographer.getInstance() 获取 Choreographer 实例。
  • FrameCallback 实现 Choreographer.FrameCallback 接口。
  • doFrame 方法中计算两帧之间的时间差,如果超过 16ms(60fps),则认为发生了卡顿。
  • 通过 choreographer.postFrameCallback(this) 在每一帧回调中重新安排下一次回调,以确保持续监控。

基于 Looper 自定义 Printer 设计卡顿监控

为了设计一个基于 Looper 自定义 Printer 的卡顿监控方案,可以通过监控主线程消息处理的时间来检测卡顿。Looper 是 Android 中用于管理线程消息队列的类,可以通过自定义 Printer 拦截消息的处理时间,从而监控卡顿情况。

实现步骤

  1. 自定义 Printer 类:创建一个自定义的 Printer 类,用于拦截和记录消息的处理时间。

  2. 在主线程中设置自定义 Printer:将自定义的 Printer 设置到主线程的 Looper 中,以便监控主线程消息的处理。

  3. 记录和处理卡顿信息:在自定义 Printer 中记录每条消息的处理开始和结束时间,并计算处理时间。如果处理时间超过一定阈值(例如 100ms),则认为发生了卡顿。

示例代码

import android.os.Handler;
import android.os.Looper;
import android.os.MessageQueue;
import android.util.Printer;

public class LooperMonitor {

    private static final long BLOCK_THRESHOLD_MS = 100; // 卡顿阈值,单位毫秒

    private Handler handler = new Handler(Looper.getMainLooper());
    private long startTime = 0;

    public void start() {
        Looper.getMainLooper().setMessageLogging(new Printer() {
            @Override
            public void println(String x) {
                if (x.startsWith(">>>>> Dispatching to")) {
                    startTime = System.currentTimeMillis();
                } else if (x.startsWith("<<<<< Finished to")) {
                    long endTime = System.currentTimeMillis();
                    long duration = endTime - startTime;
                    if (duration > BLOCK_THRESHOLD_MS) {
                        onBlockDetected(duration);
                    }
                }
            }
        });
    }

    private void onBlockDetected(long duration) {
        // 处理卡顿逻辑,例如记录日志或上报监控数据
        System.out.println("UI thread blocked for " + duration + " ms");
    }

    public void stop() {
        Looper.getMainLooper().setMessageLogging(null);
    }
}

关键点说明

  1. 自定义 Printer:在自定义的 Printer 中,通过 println 方法拦截消息处理的开始和结束时间。

    • >>>>> Dispatching to 表示开始处理消息。
    • <<<<< Finished to 表示完成消息处理。
  2. 记录消息处理时间:在处理开始时记录当前时间,在处理结束时计算处理持续时间。如果持续时间超过预设的卡顿阈值(如 100ms),则认为发生了卡顿。

  3. 处理卡顿信息:在 onBlockDetected 方法中处理卡顿逻辑,例如记录日志或上报监控数据。

  4. 启动和停止监控:提供 startstop 方法,用于启动和停止卡顿监控。

优化和扩展

  • 上报监控数据:可以将卡顿信息上报到服务器,以便进行统计分析。
  • 更精细的监控:可以进一步细化监控逻辑,例如记录卡顿发生的具体时间、频率等。
  • 结合其他监控手段:可以结合 Choreographer 回调监控帧率,提供更全面的性能监控方案。

基于 FrameMetrics 接口 设计卡顿监控

为了设计一个基于 FrameMetrics 接口的卡顿监控方案,可以利用 FrameMetrics 提供的帧渲染时间来监控帧率,并检测可能的卡顿。FrameMetrics 是 Android 7.0 (API 24) 引入的一个类,它可以捕捉帧的各种性能指标,包括开始时间、结束时间、绘制时间等。

实现步骤

  1. 注册 FrameMetricsListener:在活动窗口的 Window 上注册 FrameMetrics 的监听器,捕捉每一帧的性能数据。
  2. 监听帧渲染事件:通过 FrameMetrics 获取每一帧的渲染时间,计算是否有卡顿。
  3. 记录和处理卡顿信息:当检测到卡顿时,记录相关信息或者执行相应的处理逻辑,例如上报监控数据。

示例代码

import android.app.Activity;
import android.os.Build;
import android.util.Log;
import android.view.FrameMetrics;
import android.view.Window;
import android.view.Window.OnFrameMetricsAvailableListener;

public class FrameMetricsMonitor {

    private static final long FRAME_TIME_MS = 16; // 60fps, 单位:毫秒

    public FrameMetricsMonitor(Activity activity) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            Window window = activity.getWindow();
            window.addOnFrameMetricsAvailableListener(new OnFrameMetricsAvailableListener() {
                @Override
                public void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics, int dropCountSinceLastInvocation) {
                    long totalDuration = frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION);
                    long totalDurationMs = totalDuration / 1_000_000; // 纳秒转毫秒

                    if (totalDurationMs > FRAME_TIME_MS) {
                        long droppedFrames = totalDurationMs / FRAME_TIME_MS;
                        onFrameDropped(droppedFrames);
                    }
                }
            }, null);
        }
    }

    private void onFrameDropped(long droppedFrames) {
        // 处理卡顿逻辑,例如记录日志或上报监控数据
        Log.d("FrameMetricsMonitor", "Frame dropped: " + droppedFrames);
    }
}

关键点说明

  1. 注册 FrameMetrics 监听器

    • 在活动的 Window 上注册 FrameMetrics 监听器,捕捉每一帧的性能数据。
    • 使用 addOnFrameMetricsAvailableListener 方法注册监听器。
  2. 监听帧渲染事件

    • onFrameMetricsAvailable 回调方法中,通过 FrameMetrics 获取每一帧的总渲染时间 (TOTAL_DURATION)。
    • 将渲染时间从纳秒转换为毫秒,并与设定的帧时间阈值(如 16ms)进行比较。
  3. 处理卡顿信息

    • 如果总渲染时间超过设定的帧时间阈值,则认为发生了卡顿,并计算掉帧数量。
    • onFrameDropped 方法中处理卡顿逻辑,例如记录日志或上报监控数据。

优化和扩展

  • 上报监控数据:可以将卡顿信息上报到服务器,以便进行统计分析。
  • 更精细的监控:可以进一步细化监控逻辑,例如记录卡顿发生的具体时间、频率等。
  • 结合其他监控手段:可以结合 LooperChoreographer 回调监控帧率,提供更全面的性能监控方案。

JankStats 卡顿检测

JankStats 用来追踪和分析应用性能,发现 Jank 卡顿问题,它最低向下兼容到 API 16,可以在绝大多数机器设备上使用,有了它我们不必再求助 BlockCanery 等三方工具了。

implementation "androidx.metrics:metrics-performance:1.0.0-beta01"

我们需要为每个 Window 创建一个 JankStats 实例,并通过 OnFrameListener 回调获取包含是否卡顿在内的帧信息,示例如下:

kotlin 代码解读复制代码class JankLoggingActivity : AppCompatActivity() {

    private lateinit var jankStats: JankStats

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
        // metricsStateHolder可以收集环境信息,跟随帧信息返回
        val metricsStateHolder = PerformanceMetricsState.getForHierarchy(binding.root)

        // 基于当前 Window 创建 JankStats 实例
        jankStats = JankStats.createAndTrack(
            window,
            Dispatchers.Default.asExecutor(),
            jankFrameListener,
        )

        // 设置 Activity 名字到环境信息
        metricsStateHolder.state?.addState("Activity", javaClass.simpleName)
        // ...
    }

    private val jankFrameListener = JankStats.OnFrameListener { frameData ->
        // 监听到的帧信息
        Log.v("JankStatsSample", frameData.toString())
    }
}

PerformanceMetricsState 用来收集你希望跟随 frameData 一起返回的状态信息,比如上面例子中设置了当前 Activity 名称,下面是 frameData 的打印日志:

JankStats.OnFrameListener: FrameData(frameStartNanos=827233150542009, frameDurationUiNanos=27779985, frameDurationCpuNanos=31296985, isJank=false, states=[Activity: JankLoggingActivity])