Android 卡顿优化之 Skipped * frames 掉帧的计算

本文详细解析了Android系统中Skipped frames(掉帧)的计算过程,通过分析Choreographer源码,揭示了系统如何通过VSYNC信号来检测主线程耗时操作导致的卡顿。当主线程任务执行延迟超过一帧的绘制时间(约16.7ms),就会记录为掉帧。优化卡顿不仅关注耗时操作,还需注意任务调度的分散。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Android 卡顿优化之 Skipped * frames 掉帧的计算

有时候看日志的时候,可能会在日志中看到类似下文的打印:

Skipped 30 frames! The application may be doing too much work on its main thread.

这句话的意思就是:该行日志打印之前的主线程存在耗时操作,导致掉了30帧。之前知道这句日志的意思,但是不知道系统是怎么计算的,于是专门花时间看了下 Choreographer 的源码。

Choreographer

职责:用于配合系统的 VSYNC 信号,用于接收系统发出的 VSYNC 信号,统一管理应用程序的输入、动画和绘制等任务的执行时机

初始化时机:

    public ViewRootImpl(Context context, Display display) {
        ...
        // 主线程初始化
        mChoreographer = Choreographer.getInstance();
        ...
    }
    private static final ThreadLocal<Choreographer> sThreadInstance =
            new ThreadLocal<Choreographer>() {
        @Override
        protected Choreographer initialValue() {
            Looper looper = Looper.myLooper();
            if (looper == null) {
                throw new IllegalStateException("The current thread must have a looper!");
            }
            return new Choreographer(looper, VSYNC_SOURCE_APP);
        }
    };
FrameHandler

Choreographer 的成员变量,在 Choreographer 的构造器中被初始化,为主线程 Handler

应用程序的 Input(主要是 Input 事件)、Animation(动画相关)、Traversal(包括 measure、layout、draw 等操作)任务,都会进入 FrameHandler 的消息队列等待执行

FrameDisplayEventReceiver

Choreographer 的成员变量,在 Choreographoer 的构造器中被初始化,用于接收 VSYNC 信号并进行相应处理

下面就介绍下 Choreographer 中计算掉帧的方式

首先看下 FrameDisplayEventReceiver 类:

    // 继承 DisplayEventReceiver,可以接收 VSYNC 信号
    // 实现了 Runnable 接口,可以作为一个 Runnable 传入消息队列
    private final class FrameDisplayEventReceiver extends DisplayEventReceiver 
            implements Runnable { 
        private boolean mHavePendingVsync;
        private long mTimestampNanos;
        private int mFrame;

        public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
            super(looper, vsyncSource);
        }

        @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {  
            // 接收到 VSYNC 信号,开始当前帧的绘制
            ...
            
            long now = System.nanoTime();
            if (timestampNanos > now) {
                Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
                        + " ms in the future!  Check that graphics HAL is generating vsync "
                        + "timestamps using the correct timebase.");
                timestampNanos = now;
            }

            if (mHavePendingVsync) {
                Log.w(TAG, "Already have a pending vsync event.  There should only be "
                        + "one at a time.");
            } else {
                mHavePendingVsync = true;
            }
            
            // 当前帧计划绘制时间
            mTimestampNanos = timestampNanos; 
            mFrame = frame;
            // 该类实现了 Runnable 接口,因此 this 代表该类,Handler 执行该 Message 时,就会执行下方 run() 方法
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            // mHandler 是 FrameHandler 对象,为主线程 Handler,即将本次 Runable 在指定时间 timestampNanos 在主线程执行
            // 执行该任务前,会先将该任务之前的所有任务执行完
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run() {
            mHavePendingVsync = false;
            // 实际执行到该 Runnable 时,会调用 doFrame 方法,并将该方法计划执行时间 mTimestampNanos 作为参数传入
            doFrame(mTimestampNanos, mFrame);
        }
    }

接下来看 doFrame 方法:

    void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        synchronized (mLock) {
            if (!mFrameScheduled) {
                return; // no work to do
            }

            if (DEBUG_JANK && mDebugPrintNextFrameTimeDelta) {
                mDebugPrintNextFrameTimeDelta = false;
                Log.d(TAG, "Frame time delta: "
                        + ((frameTimeNanos - mLastFrameTimeNanos) * 0.000001f) + " ms");
            }
            // 计划执行时间
            long intendedFrameTimeNanos = frameTimeNanos;
            // 实际执行时间
            startNanos = System.nanoTime();
            // 该帧实际执行时间和计划执行时间的差值,即延迟时间
            final long jitterNanos = startNanos - frameTimeNanos;
            // mFrameIntervalNanos = (long)(1000000000 / getRefreshRate()) 一般为 16.7ms,即每一帧的绘制时间
            if (jitterNanos >= mFrameIntervalNanos) { 
                // 由此可以得出共计延迟了多少帧,就是掉了多少帧
                final long skippedFrames = jitterNanos / mFrameIntervalNanos;
                // SKIPPED_FRAME_WARNING_LIMIT 默认为30,可以代码里面通过反射更改
                if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
                    Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                            + "The application may be doing too much work on its main thread.");
                }
                final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
                if (DEBUG_JANK) {
                    Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
                            + "which is more than the frame interval of "
                            + (mFrameIntervalNanos * 0.000001f) + " ms!  "
                            + "Skipping " + skippedFrames + " frames and setting frame "
                            + "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
                }
                frameTimeNanos = startNanos - lastFrameOffset;
            }

            if (frameTimeNanos < mLastFrameTimeNanos) {
                if (DEBUG_JANK) {
                    Log.d(TAG, "Frame time appears to be going backwards.  May be due to a "
                            + "previously skipped frame.  Waiting for next vsync.");
                }
                scheduleVsyncLocked();
                return;
            }

            mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
            mFrameScheduled = false;
            mLastFrameTimeNanos = frameTimeNanos;
        }

        try {
            // 执行具体的 Input、Animation、Traversal Task
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
            AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

            mFrameInfo.markInputHandlingStart();
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

            mFrameInfo.markAnimationsStart();
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

            mFrameInfo.markPerformTraversalsStart();
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } finally {
            AnimationUtils.unlockAnimationClock();
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

        if (DEBUG_FRAMES) {
            final long endNanos = System.nanoTime();
            Log.d(TAG, "Frame " + frame + ": Finished, took "
                    + (endNanos - startNanos) * 0.000001f + " ms, latency "
                    + (startNanos - frameTimeNanos) * 0.000001f + " ms.");
        }
    }
通过查看源码可以得出

系统计算掉帧的流程是:收到 VSYNC 信号之后,会向 FrameHandler 发送一条 message,message 中包含本帧计划执行的时间。等到 message 真正执行的时候,计算实际执行时间和计划执行时间的差值,除以16.7ms 之后就得到了丢帧数

思考

从系统计算丢帧数的方式可以看出,丢帧的原因是由于本帧之前的一系列 task ,总计耗时导致的。因此处理卡顿问题的时候,不仅要关注耗时操作的优化,还要考虑任务的离散,同一时刻执行的任务太多也有可能造成卡顿。

<think>好的,我现在要帮助用户分析如何通过log文件解决Android应用卡顿的问题。首先,我需要回顾用户提供的引用内容,看看有哪些工具和方法已经被提到过。根据引用[1]、[3]、[4]、[5],提到了Traceview、BlockCanary、ANR日志、Systrace和adb命令。这些工具都是分析卡顿的关键。 用户的主要需求是通过日志文件定位卡顿原因。首先,我应该分步骤说明不同的日志类型和工具的使用方法。比如,ANR日志可以直接从设备中获取,分析主线程是否被阻塞。然后,Traceview和Systrace可以帮助分析方法的执行时间和系统层面的性能问题。此外,BlockCanary虽然需要植入代码,但可以监控主线程的耗时操作。 接下来,我需要确保每一步都结合引用内容,并正确标注引用标记。例如,当提到Traceview时,引用[3]详细介绍了它的功能,应该在这里引用。同样,BlockCanary的缺点在引用[2]中提到,需要指出可能的性能影响。 另外,用户可能需要知道如何获取这些日志文件。例如,使用adb命令导出ANR日志(引用[5]),或者通过Systrace生成报告(引用[4])。需要详细说明每个步骤的操作方法,比如运行什么命令,如何分析结果。 还要注意用户提到的性能开销问题,比如BlockCanary可能会影响应用性能,这在引用[2]中已经说明,应该在建议使用时提到这一点,让用户权衡利弊。 最后,要确保回答结构清晰,分步骤说明,每个部分都有对应的工具和日志分析方法,并结合实际例子,如主线程的堆栈跟踪、GC频繁等常见问题。这样用户能够逐步排查卡顿原因,并找到解决方案。</think>### 通过日志文件分析Android应用卡顿问题的步骤 #### 1. **定位ANR日志** 当应用主线程阻塞超过5秒会触发ANR(Application Not Responding),系统会自动生成`/data/anr/traces.txt`文件,记录主线程堆栈信息: ```bash adb pull /data/anr/traces.txt # 导出ANR日志[^5] ``` 分析重点: - 查找主线程中耗时操作(如同步I/O、复杂计算) - 检查锁竞争或死锁现象 - 识别第三方库的阻塞调用链[^1] #### 2. **使用Traceview进行方法级分析** 通过插桩方式记录方法执行耗时: ```java Debug.startMethodTracing("app_trace"); // 需要监控的代码段 Debug.stopMethodTracing(); ``` 生成`.trace`文件后,使用Android Studio的Profiler加载分析: - 查看主线程调用树中耗时超过16ms的方法(1的阈值) - 关注`Wall Clock Time`与`Thread Time`的差异(I/O等待时间) - 识别重复调用的冗余方法[^3] #### 3. **Systrace系统级跟踪** 通过adb采集系统级性能数据: ```bash python systrace.py -a com.example.app -b 16384 -o trace.html gfx view res ``` 分析要点: - 检查`RenderThread`的生成间隔 - 观察`Choreographer#doFrame`耗时 - 识别VSYNC信号丢失情况 - 查看GC事件频率(频繁GC会导致卡顿)[^4] ![Systrace渲染分析示意图][^4] #### 4. **BlockCanary自动化检测** 在build.gradle中添加依赖后实现拦截器: ```java public class AppBlockCanary extends BlockCanaryContext { // 配置检测阈值和日志路径 } ``` 监控结果包含: - 卡顿时主线程堆栈快照 - 耗时操作的代码位置 - 关联的IPC调用信息[^2] #### 5. **Logcat日志过滤** 通过特定标签过滤关键信息: ```bash adb logcat -v time -s Choreographer:W ActivityManager:I *:S ``` 重点关注: ``` W/Choreographer: Skipped 3 frames! The application may be doing too much work on its main thread I/ActivityManager: ANR in com.example.app ``` #### 6. **GC日志分析** 在应用启动参数中添加: ```bash adb shell setprop log.tag.Gc DEBUG ``` 检查日志中的GC原因: ``` D/art: Explicit concurrent mark sweep GC freed... D/art: Alloc concurrent mark sweep GC freed... ``` 频繁GC(每秒超过1次)可能说明存在内存抖动[^1] ### 典型卡顿场景解决方案 1. **布局过度绘制** - 使用`Layout Inspector`检查视图层级 - 替换`RelativeLayout`为`ConstraintLayout` - 启用`GPU过度绘制调试`(设置-开发者选项) 2. **主线程IO操作** ```java new Thread(() -> { // 文件操作移至子线程 }).start(); ``` 3. **内存泄漏检测** ```bash adb shell dumpsys meminfo com.example.app -d ``` $$ T_{frame} = T_{input} + T_{measure} + T_{layout} + T_{draw} \leq 16ms $$
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值