面试题 Android 如何实现自定义View 固定帧率绘制

公主: 别写代码了, 我们一起去看电影吧

曾经遇到的面试题, 如何实现自定义View 1s内固定帧率的绘制.

当时对Android理解不深, 考虑的不全面, 直接回答了在onDraw结束时通过postDelay发送一个(1000 / 帧数)ms的延时消息触发invalidate进行下一次绘制. 但实际上这样做存在明显的问题 实际上1s绘制的帧数是不符合期望帧数的. 个人觉得主要还是考察对Android渲染机制的理解以及熟悉程度

Android渲染机制

先简单介绍下Android的渲染机制

绘制入口

在Android中, 当系统Vsync信号到来之后Choreographer会执行doFrame函数将Choreographer内注册的各种类型的Callback一一执行. 这其中包含了Choreographer.CALLBACK_TRAVERSAL这一类型的Callback. 在Callback的实现中, 将会调用ViewRootImpl.doTraversal()然后开始Android绘制的三大流程即 measure, layout, draw. 不考虑高刷屏幕的话, Vsync信号会每间隔16.6ms到来一次. 基于此, 应用得以完成每秒60帧的绘制

创建绘制任务

当View需要重新绘制时, 会调用到View的requestLayoutinvalidate申请重新绘制. 实际上这两个函数最终都会调用到ViewRootImplscheduleTraversals这一函数向Choreographer注册绘制的Callback

代码解释

scss 复制代码
ViewRootImpl
void invalidate() {
    mDirty.set(0, 0, mWidth, mHeight);
    if (!mWillDrawSoon) {
        // 计划绘制
        scheduleTraversals();
    }
}

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //插入同步屏障
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // 向mChoreographer中注册Callback
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

//向mChoreographer注册的Callback类
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        //移除同步屏障
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }
        //绘制三大流程入口
        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

Choreographer
void doFrame(long frameTimeNanos, int frame,
        DisplayEventReceiver.VsyncEventData vsyncEventData) {
        
        // 省略大部分代码
        //执行Input类型Callback
        doCallbacks(Choreographer.CALLBACK_INPUT, frameData, frameIntervalNanos);

        //执行Input类型Callback
        doCallbacks(Choreographer.CALLBACK_ANIMATION, frameData, frameIntervalNanos);
        
        //执行动画类型Callback
        doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameData,
                frameIntervalNanos);

        //执行绘制类型Callback
        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameData, frameIntervalNanos);
        //执行Commit类型Callback
        doCallbacks(Choreographer.CALLBACK_COMMIT, frameData, frameIntervalNanos);

}

如何实现固定帧率的绘制(60帧为例)

为什么postDelay帧间隔存在问题

假设Vsync信号在第0ms时到达, 而我们的onDraw函数执行完时已经达到了第X ms(0 < X < 16 不考虑掉帧的情况). 此时如果按照上面所讲的方式发送一个16ms的延时Message. 那么invalidate被触发的时机是在第二次Vsync执行doFrame之后了, 也就是说下一次绘制实际上是在第三个Vsync信号到来执行doFrame的时候. 由于invalidate调用时机不正确实际上绘制的帧数与预期是完全不符的

从以下日志中可以看出绘制60帧实际上花了大约1800ms 远大于实际期望的1s时间

kotlin 复制代码
class CustomView1 : View {

    companion object {
        private const val TAG = "CustomView1"
        private const val DELAY = 16L
    }

    private var mSum = 0
    private val mRunnable = Runnable {
        invalidate()
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        Log.d(TAG, "onDraw")
        mSum++
        if (mSum < 60) {
            postDelayed(
                mRunnable,
                DELAY
            )
        }
    }

}
arduino 复制代码
//第一帧绘制
2023-11-13 23:47:27.185 29343-29343 CustomView1             com.example.fps.test                 D  onDraw
//第60帧绘制
2023-11-13 23:47:28.996 29343-29343 CustomView1             com.example.fps.test                 D  onDraw

如何实现1s内固定帧率的绘制

如果想要在1s内均匀的绘制完固定的帧率, 我们需要控制好invalidate的调用时机. 那么我们就需要了解下一次需要绘制的Vsync到来的时间, 在Vsync信号到来之前就调用invalidate 实际上对于非高刷屏幕, 我们可以直接在onDraw结束时就调用invalidate这样1s内60帧View的onDraw都将被执行. 但是对于高刷屏幕或者60以外的帧数的话, 就需要做一些额外处理了.

Andorid在Choreographer中提供了接口可以用来监听Vsync信号到来的时间. 该接口常被用于帧率/掉帧的检测

csharp 复制代码
public interface FrameCallback {
    public void doFrame(long frameTimeNanos);
}

在自定义View中, 我们可以通过监听Vsync信号到来的时间以及当前绘制的时间还有屏幕刷新率推算出我们期望下一次绘制所对应的Vsync信号时间的间隔, 然后发送延时消息触发View绘制

kotlin 复制代码
private val mRunnable = Runnable {
    Log.d(TAG, "run invalidate")
    invalidate() // 触发绘制
    Choreographer.getInstance().postFrameCallback(this) //继续监听
}

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    val expectDrawTime = mLastVsyncTime + DRAW_INTERVAL //期望绘制的时间
    var targetVsyncTime = mLastVsyncTime + mDoFrameInterval
    while (targetVsyncTime + mDoFrameInterval <= expectDrawTime) { //得出对应的Vsync时间
        targetVsyncTime += mDoFrameInterval
    }
    val curTime = SystemClock.uptimeMillis()
    var delayTime = targetVsyncTime - curTime
    if (delayTime > mDoFrameInterval) {
        delayTime -= mDoFrameInterval / 2 // 不能将delay时间设置为刚好Vsync时间 不然会错过
        Log.d(TAG, "postDelayed targetVsyncTime:$targetVsyncTime curTime:$curTime delayTime:$delayTime")
        postDelayed(
            mRunnable,
            delayTime
        )
    } else { // 下一次Vsync时间马上到来直接触发
        Log.d(TAG, "direct invalidate")
        mRunnable.run()
    }
}
yaml 复制代码
30帧(第一帧与最后一帧时间)
2023-11-16 21:42:59.323 17976-17976 CustomView2             com.example.fps.test                 D  onDraw
2023-11-16 21:43:00.290 17976-17976 CustomView2             com.example.fps.test                 D  onDraw

60帧(第一帧与最后一帧时间)
2023-11-16 21:40:54.886 17390-17390 CustomView2             com.example.fps.test                 D  onDraw
2023-11-16 21:40:55.878 17390-17390 CustomView2             com.example.fps.test                 D  onDraw

120帧(第一帧与最后一帧时间)
2023-11-16 21:41:41.243 17650-17650 CustomView2             com.example.fps.test                 D  onDraw
2023-11-16 21:41:42.225 17650-17650 CustomView2             com.example.fps.test                 D  onDraw

从以上日志可以看出, 基本在1s左右完成了绘制

代码仓库

CustomFpsDraw

相关推荐
sszmvb12349 分钟前
测试开发 | 电商业务性能测试: Jmeter 参数化功能实现注册登录的数据驱动
jmeter·面试·职场和发展
测试杂货铺15 分钟前
外包干了2年,快要废了。。
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
王佑辉15 分钟前
【redis】redis缓存和数据库保证一致性的方案
redis·面试
HerayChen18 分钟前
HbuildderX运行到手机或模拟器的Android App基座识别不到设备 mac
android·macos·智能手机
顾北川_野19 分钟前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
hairenjing112321 分钟前
在 Android 手机上从SD 卡恢复数据的 6 个有效应用程序
android·人工智能·windows·macos·智能手机
真忒修斯之船22 分钟前
大模型分布式训练并行技术(三)流水线并行
面试·llm·aigc
ZL不懂前端43 分钟前
Content Security Policy (CSP)
前端·javascript·面试
小黄人软件1 小时前
android浏览器源码 可输入地址或关键词搜索 android studio 2024 可开发可改地址
android·ide·android studio
dj15402252031 小时前
group_concat配置影响程序出bug
android·bug