我们可以利用 ViewTreeObserver 来实现滑动帧率的监控,但需要明确一点:ViewTreeObserver 本身并不直接提供全局滑动监听,而是通过其 OnScrollChangedListener 来监听单个 View 的滚动事件 。因此,我们可以通过将监听器附加到滑动容器(如 ScrollView、NestedScrollView、ListView 等)上,来感知滑动的开始和结束,进而结合 Choreographer 计算滑动期间的帧率。
下面将介绍一种统一的实现思路,并提供可复用的代码。
1. 核心思路
- 检测滑动状态 :通过目标 View 的
ViewTreeObserver注册OnScrollChangedListener,在滚动回调中判断滑动状态的变化。 - 利用 Choreographer 统计帧:在滑动开始时开启帧回调,统计滑动期间每一帧的到达时间;滑动结束时计算平均帧率和掉帧数。
- 统一封装:创建一个工具类,可以应用到任何支持滚动监听的 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 统一实现对滑动帧率的监控。根据实际场景选择适合的滑动检测方式即可。