[Framework] Android FPS 计算

Android FPS 计算

简述绘制过程

简单描述一下 View 的绘制过程:SurfaceFlinger 在收到 Vsync 脉冲信号后会,通过 IPC 的通信方式通知位于应用层的 Choreographer, Choreographer 在收到信号后会通过 Handler 把 Vsync 同步信号发送至主线程,这时候 Choregrapher 会根据目前的状态,选择下发触摸事件,动画,绘制(包括 layout,measure,draw) 等等。这些事件会下发至 ViewRootImpl 依次在下发至 Activity, View 等我们应用程常见的控件,如果是绘制的事件,最终会完成整个 View 树的 layout,measure,draw 过程(这整个过程都是由 CPU 在主线程上完成),主线程完成后会把绘制的数据传递给应用层的RenderThread(开启硬件加速),RenderThread 会向 SurfaceFlinger 请求获取绘制数据的 Buffer,这个过程也是通过 Binder 完成的,获取到 Buffer 后将主线程绘制的数据通过 GPU 绘制(通常是通过 OpenGL ES)到 Buffer 上,绘制完成后 RenderThread 通过 Binder 将填充好数据的 Buffer 传递给 SurfaceFlingerSurfaceFlinger 最后合成这些数据再传递给 HardwareComposer (HAL 层),完成一帧画面的显示。

Vsync 信号中的重要信息

我们从应用层 Choregorapher 入手,它是通过他的私有内部类 FrameDisplayEventReceiver 获取 Vsync 同步信号,它是继承至 DisplayEventReceiver,回调方法是 onVsync() 方法:


/**
 * Called when a vertical sync pulse is received.
 * The recipient should render a frame and then call {@link #scheduleVsync}
 * to schedule the next vertical sync pulse.
 *
 * @param timestampNanos The timestamp of the pulse, in the {@link System#nanoTime()}
 * timebase.
 * @param physicalDisplayId Stable display ID that uniquely describes a (display, port) pair.
 * @param frame The frame number.  Increases by one for each vertical sync interval.
 * @param vsyncEventData The vsync event data.
 */
public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
        VsyncEventData vsyncEventData) {
}

其中比较重要的参数:

  • timestampNanos
    Vsync 发生的的时间戳
  • frame
    frame 的编号,是依次增长的
  • vsyncEventData
    见以下面的代码

static final class VsyncEventData {
    // The frame timeline vsync id, used to correlate a frame
    // produced by HWUI with the timeline data stored in Surface Flinger.
    public final long id;
    // The frame deadline timestamp in {@link System#nanoTime()} timebase that it is
    // allotted for the frame to be completed.
    public final long frameDeadline;
    /**
     * The current interval between frames in ns. This will be used to align
     * {@link FrameInfo#VSYNC} to the current vsync in case Choreographer callback wa
     * delayed by the app.
     */
    public final long frameInterval;
    VsyncEventData(long id, long frameDeadline, long frameInterval) {
        this.id = id;
        this.frameDeadline = frameDeadline;
        this.frameInterval = frameInterval;
    }
    VsyncEventData() {
        this.id = FrameInfo.INVALID_VSYNC_ID;
        this.frameDeadline = Long.MAX_VALUE;
        this.frameInterval = -1;
    }
}

VsyncEventData 中比较重要的参数:

  • frameDeadline
    表示当前帧要在该时间之前完成绘制,如果超过该时间就有可能导致丢帧(因为有三重缓存的存在,所以不是一定会掉帧)。
  • frameInterval
    帧之间的间隔,屏幕刷新率 60 Hz 为例,间隔就为 1 / 60 * 10 ^ 9 纳秒。

通过上面的 Vsync 同步参数我们就可以很容易地计算出 FPS,或者当前的帧绘制是否超过了间隔。

Android 源码中掉帧计算

没错,在 Choregorapher 源码中有掉帧的计算,我们先从它收到 Vsync 信号开始看:


    private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
        private boolean mHavePendingVsync;
        private long mTimestampNanos;
        private int mFrame;
        private VsyncEventData mLastVsyncEventData = new VsyncEventData();

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

        // TODO(b/116025192): physicalDisplayId is ignored because SF only emits VSYNC events for
        // the internal display and DisplayEventReceiver#scheduleVsync only allows requesting VSYNC
        // for the internal display implicitly.
        @Override
        public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
                VsyncEventData vsyncEventData) {
            try {
                if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
                    Trace.traceBegin(Trace.TRACE_TAG_VIEW,
                            "Choreographer#onVsync " + vsyncEventData.id);
                }
                // Post the vsync event to the Handler.
                // The idea is to prevent incoming vsync events from completely starving
                // the message queue.  If there are no messages in the queue with timestamps
                // earlier than the frame time, then the vsync event will be processed immediately.
                // Otherwise, messages that predate the vsync event will be handled first.
                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;
                mLastVsyncEventData = vsyncEventData;
                Message msg = Message.obtain(mHandler, this);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
        }

        @Override
        public void run() {
            mHavePendingVsync = false;
            doFrame(mTimestampNanos, mFrame, mLastVsyncEventData);
        }
    }

上面源码很简单,在 onVsync 方法回调中(工作在 Binder 线程),通过 handler 加入消息队列 (注意这里通过 setAsynchronous() 方法将该消息设置为异步消息,会优先被执行),这个 handler 的 Looper 是主线程的,也就是会在主线程执行,最后会执行 run() 方法,后进入 doFrame() 方法(这里还通过 mHavePendingVsync 变量来判断是否有消息还在队列里面没有执行,如果没有执行,就跳过,也就是出现了掉帧)。

doFrame() 方法中有一段代码来计算掉帧,这段代码是在绘制等事件下发前,可以理解为,前面的帧绘制时间过长导致的掉帧:


// ..
startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= frameIntervalNanos) {
    final long skippedFrames = jitterNanos / frameIntervalNanos;
    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 % frameIntervalNanos;
    if (DEBUG_JANK) {
        Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
                + "which is more than the frame interval of "
                + (frameIntervalNanos * 0.000001f) + " ms!  "
                + "Skipping " + skippedFrames + " frames and setting frame "
                + "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
    }
    frameTimeNanos = startNanos - lastFrameOffset;
}
// ..

通过当前时间减去 Vsync 脉冲产生的时间(startNanos - frameTimeNanos)来表示之间间隔(jitterNanos),因为这个时间还没有做耗时的类似于绘制的操作,如果正常的情况下,肯定是远远小于正常的帧间隔的(frameIntervalNanos),如果是大于这个值了(也可以是当前时间肯定大于了 frameDeadline),肯定是前面的帧绘制耗时时间过长导致的(因为这是一个消息队列,前面的帧绘制时间过长,就会导致后续的任务延迟执行),所以就可以通过 jitterNanos / frameIntervalNanos 来计算出掉了几帧。

简单的 FPS 计算

在应用层想要计算肯定涉及到反射,在 Android 中如果绕过高版本的反射限制,可以在网上找找方法。这里给一个简单的计算方式,在每次的 Vsync 绘制完成是来计算,通过当前的时间减去 timestampNanos 表示当前帧的绘制耗时,这个耗时如果小于 frameInterval 表示没有出现掉帧,当前的帧率可以近似认为等于当前刷新率,计算方法为 1 * 10 ^ 9 / frameInterval; 如果绘制耗时大于 frameInterval 就表示掉帧了(并不一定掉帧,因为有三重缓存的存在,只是简单暴力认为掉帧了),当前的 FPS 计算方法为 1 * 10 ^ 9 / (System.nanoTime() - timestampNanos)
这个 FPS 的计算不是绝对的准确,因为没有考虑到三重缓存、RenderThreadSurfaceFlinger 的耗时;如果像 Android 的实现一样,以收到 Vsync 后为基准时间,来计算上一次绘制的 FPS,要准一些,就有一帧的延迟。具体怎么计算那就取决于你自己了,实践是最好的验证方法,我这里只是简单贴出计算方法,我觉得你应该有更好的方法。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题

图片

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值