Android 计算滑动帧率 笔记

我们可以利用 ViewTreeObserver 来实现滑动帧率的监控,但需要明确一点:ViewTreeObserver 本身并不直接提供全局滑动监听,而是通过其 OnScrollChangedListener 来监听单个 View 的滚动事件 。因此,我们可以通过将监听器附加到滑动容器(如 ScrollView、NestedScrollView、ListView 等)上,来感知滑动的开始和结束,进而结合 Choreographer 计算滑动期间的帧率。

下面将介绍一种统一的实现思路,并提供可复用的代码。


1. 核心思路

  1. 检测滑动状态 :通过目标 View 的 ViewTreeObserver 注册 OnScrollChangedListener,在滚动回调中判断滑动状态的变化。
  2. 利用 Choreographer 统计帧:在滑动开始时开启帧回调,统计滑动期间每一帧的到达时间;滑动结束时计算平均帧率和掉帧数。
  3. 统一封装:创建一个工具类,可以应用到任何支持滚动监听的 View 上,自动管理生命周期。

注意 :RecyclerView 并不通过 ViewTreeObserver 触发滚动回调,它有自己的 addOnScrollListener 方法。因此,我们的统一方案需要同时兼容两种场景,或者让调用者根据 View 类型选择合适的方式。这里我们提供一个抽象,并针对常用容器分别处理。


2. 代码实现

2.1 定义滑动帧率监听器接口

java 复制代码
public interface ScrollFpsListener {
    void onScrollFps(double avgFps, int droppedFrames, long scrollDurationMs);
}

2.2 核心监控类(利用 Choreographer)

java 复制代码
public class ScrollFpsTracker {
    private Choreographer choreographer;
    private Choreographer.FrameCallback frameCallback;
    private boolean isTracking = false;      // 是否正在跟踪(Choreographer是否注册)
    private boolean isScrolling = false;     // 是否正在滑动中

    // 统计数据
    private long lastFrameTimeNs;
    private int frameCount;
    private long totalDurationNs;
    private long scrollStartTimeNs;

    private ScrollFpsListener listener;

    public ScrollFpsTracker(ScrollFpsListener listener) {
        this.choreographer = Choreographer.getInstance();
        this.listener = listener;
        initFrameCallback();
    }

    private void initFrameCallback() {
        frameCallback = new Choreographer.FrameCallback() {
            @Override
            public void doFrame(long frameTimeNanos) {
                if (!isTracking || !isScrolling) {
                    // 如果不在跟踪状态或未在滑动,不统计,但仍需继续注册以便后续恢复
                    if (isTracking) {
                        choreographer.postFrameCallback(this);
                    }
                    lastFrameTimeNs = frameTimeNanos;
                    return;
                }

                if (lastFrameTimeNs != 0) {
                    long intervalNs = frameTimeNanos - lastFrameTimeNs;
                    totalDurationNs += intervalNs;
                    frameCount++;
                }
                lastFrameTimeNs = frameTimeNanos;

                // 继续监听下一帧
                choreographer.postFrameCallback(this);
            }
        };
    }

    // 开始跟踪(通常在View附加到窗口时调用)
    public void startTracking() {
        if (!isTracking) {
            isTracking = true;
            resetStats();
            choreographer.postFrameCallback(frameCallback);
        }
    }

    // 停止跟踪(View从窗口移除时调用)
    public void stopTracking() {
        if (isTracking) {
            isTracking = false;
            choreographer.removeFrameCallback(frameCallback);
            resetStats();
        }
    }

    // 滑动开始
    public void onScrollStarted() {
        if (!isTracking) return;
        isScrolling = true;
        resetStats();
        scrollStartTimeNs = System.nanoTime();
    }

    // 滑动结束
    public void onScrollStopped() {
        if (!isTracking || !isScrolling) return;
        isScrolling = false;

        long now = System.nanoTime();
        long scrollDurationNs = now - scrollStartTimeNs;
        if (scrollDurationNs > 0 && frameCount > 0) {
            double avgFps = (double) frameCount / (scrollDurationNs / 1_000_000_000.0);
            // 假设目标帧率为60FPS,计算掉帧数
            long expectedFrames = (long) (scrollDurationNs / (1_000_000_000.0 / 60));
            int droppedFrames = (int) (expectedFrames - frameCount);
            if (listener != null) {
                listener.onScrollFps(avgFps, Math.max(0, droppedFrames), scrollDurationNs / 1_000_000);
            }
        }

        resetStats();
    }

    private void resetStats() {
        lastFrameTimeNs = 0;
        frameCount = 0;
        totalDurationNs = 0;
    }
}

2.3 统一适配器:根据 View 类型添加滑动监听

我们需要一个辅助类,能够智能地为不同的滑动容器添加监听,并将状态变化转发给 ScrollFpsTracker。

java 复制代码
public class ScrollFpsMonitor {
    private ScrollFpsTracker tracker;
    private View targetView;
    private Object scrollListener; // 用于保存可能注册的监听器,方便移除

    public ScrollFpsMonitor(View view, ScrollFpsListener listener) {
        this.targetView = view;
        this.tracker = new ScrollFpsTracker(listener);
    }

    // 开始监控(通常在 onResume 或视图可见时调用)
    public void start() {
        tracker.startTracking();
        attachScrollListener();
    }

    // 停止监控(通常在 onPause 或视图销毁时调用)
    public void stop() {
        tracker.stopTracking();
        detachScrollListener();
    }

    private void attachScrollListener() {
        if (targetView == null) return;

        if (targetView instanceof RecyclerView) {
            RecyclerView recyclerView = (RecyclerView) targetView;
            RecyclerView.OnScrollListener listener = new RecyclerView.OnScrollListener() {
                @Override
                public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
                    if (newState == RecyclerView.SCROLL_STATE_DRAGGING || newState == RecyclerView.SCROLL_STATE_SETTLING) {
                        tracker.onScrollStarted();
                    } else if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                        tracker.onScrollStopped();
                    }
                }
            };
            recyclerView.addOnScrollListener(listener);
            scrollListener = listener; // 保存以便移除
        } else {
            // 对于 ScrollView、NestedScrollView、ListView 等,使用 ViewTreeObserver.OnScrollChangedListener
            // 注意:这些 View 的滚动事件需要配合 View.getScrollY() 的变化来判断开始和结束。
            // 这里简单起见,我们使用一个延迟任务来模拟滑动结束(实际复杂场景需要更精确的判断)。
            // 更精确的方法是:在 onScrollChanged 中记录上次滚动时间,配合 handler 判断是否停止。
            ViewTreeObserver.OnScrollChangedListener onScrollChangedListener = new ViewTreeObserver.OnScrollChangedListener() {
                private int lastScrollY = targetView.getScrollY();
                private boolean scrolling = false;
                private Runnable stopCheck = new Runnable() {
                    @Override
                    public void run() {
                        if (scrolling) {
                            scrolling = false;
                            tracker.onScrollStopped();
                        }
                    }
                };
                private Handler handler = new Handler(Looper.getMainLooper());

                @Override
                public void onScrollChanged() {
                    int scrollY = targetView.getScrollY();
                    if (scrollY != lastScrollY) {
                        if (!scrolling) {
                            scrolling = true;
                            tracker.onScrollStarted();
                        }
                        lastScrollY = scrollY;
                        // 延迟重置停止状态
                        handler.removeCallbacks(stopCheck);
                        handler.postDelayed(stopCheck, 100); // 100ms 无滚动则认为停止
                    }
                }
            };
            targetView.getViewTreeObserver().addOnScrollChangedListener(onScrollChangedListener);
            scrollListener = onScrollChangedListener;
        }
    }

    private void detachScrollListener() {
        if (targetView == null || scrollListener == null) return;

        if (targetView instanceof RecyclerView && scrollListener instanceof RecyclerView.OnScrollListener) {
            ((RecyclerView) targetView).removeOnScrollListener((RecyclerView.OnScrollListener) scrollListener);
        } else if (scrollListener instanceof ViewTreeObserver.OnScrollChangedListener) {
            targetView.getViewTreeObserver().removeOnScrollChangedListener((ViewTreeObserver.OnScrollChangedListener) scrollListener);
        }
        scrollListener = null;
    }
}

2.4 使用示例

在 Activity 或 Fragment 中,对目标滑动容器进行监控:

java 复制代码
public class MainActivity extends AppCompatActivity {
    private ScrollFpsMonitor fpsMonitor;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 假设我们有一个 NestedScrollView
        NestedScrollView scrollView = findViewById(R.id.scroll_view);
        fpsMonitor = new ScrollFpsMonitor(scrollView, new ScrollFpsListener() {
            @Override
            public void onScrollFps(double avgFps, int droppedFrames, long scrollDurationMs) {
                Log.i("ScrollFps", String.format("平均FPS: %.2f, 掉帧: %d, 滑动时长: %dms",
                        avgFps, droppedFrames, scrollDurationMs));
            }
        });
    }

    @Override
    protected void onResume() {
        super.onResume();
        fpsMonitor.start();
    }

    @Override
    protected void onPause() {
        super.onPause();
        fpsMonitor.stop();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (fpsMonitor != null) {
            fpsMonitor.stop();
            fpsMonitor = null;
        }
    }
}

3. 原理说明

  • Choreographer.FrameCallback:每次系统准备渲染新帧时回调,我们利用它记录帧的时间戳,从而计算帧间隔和帧数。
  • 滑动状态检测 :
    • 对于 RecyclerView,使用其自带的 OnScrollListener,状态清晰。
    • 对于其他基于 View 的滚动容器(如 ScrollView、NestedScrollView),通过 ViewTreeObserver.OnScrollChangedListener 监听滚动变化,并根据 scrollY 的变化和停滞时间判断滑动开始和结束。
  • 统计范围 :只在 isScrolling = true 期间记录帧,避免静止页面的帧率为0影响平均值。

4. 优缺点

优点:

  • 统一接口,可适用于多种滑动容器。
  • 无需侵入业务代码,只需在页面生命周期内启动/停止监控。
  • 可以获取每次滑动片段的帧率数据,便于精细化分析。

缺点:

  • 对于非 RecyclerView 的滑动结束判断依赖于定时器,可能存在一定误差(如快速连续滑动可能被误判为多次滑动)。
  • 无法区分惯性滑动和手指拖动,但两者都属于滑动过程,通常我们希望统计整个滑动过程。
  • ViewTreeObserver 监听需要谨慎管理,避免内存泄漏(示例中已处理移除)。

5. 优化建议

  • 更精确的滑动结束检测:可以使用 View.OnScrollChangeListener (API 23+) 结合 VelocityTracker 等,但复杂度增加。
  • 对于 RecyclerView,可以直接复用 RecyclerView.OnScrollListener,无需 ViewTreeObserver。
  • 如果希望监控整个页面的所有滑动(包括嵌套滚动),可以递归遍历视图树添加监听,但需注意性能。

通过上述方案,你可以利用 ViewTreeObserver 并结合 Choreographer 统一实现对滑动帧率的监控。根据实际场景选择适合的滑动检测方式即可。

相关推荐
三少爷的鞋11 小时前
AI 时代,我们都将成为通才型开发者:只懂 Android,已经不够了
android
Dovis(誓平步青云)15 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
晚风叙码17 小时前
MySQL 数据类型详解:从数值到字符串,一篇讲透
android·mysql·adb
传奇开心果编程17 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程20 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程21 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程1 天前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程1 天前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓1 天前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程1 天前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer