面试题 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

代码解释

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时间

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
            )
        }
    }

}

//第一帧绘制
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信号到来的时间. 该接口常被用于帧率/掉帧的检测

public interface FrameCallback {
    public void doFrame(long frameTimeNanos);
}

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

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()
    }
}

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左右完成了绘制

为了帮助大家能够能顺利的面试,我这边知识梳理了一些核心的知识点,也准备了不少的电子书和面试笔记等学习文档,这些笔记将各个知识点进行了完美的总结(包含了很多内容:Android 基础、Java 基础、Android 源码相关分析、常见的一些原理性问题等等)。

  • Android 知识点汇总:https://qr18.cn/CyxarU
  • 面试题笔记:https://qr18.cn/CgxrRy

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/157589.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

main函数的数组参数是干嘛用的

今天在看项目代码的时候&#xff0c;突然看到项目中用到了main函数的参数args&#xff0c;在这之前我还没怎么注意过这个参数&#xff0c;一时间居然不知道这个参数是干嘛的&#xff01; 虽然也写过一些java和scala&#xff0c;但是确实没遇到过会用这个参数的情况。 网上就查…

国鑫受邀出席2023松山湖软件和信息服务业高质量发展大会

为推动粤港澳大湾区的软件和先进制造产业的融合发展&#xff0c;“2023松山湖软件和信息服务业高质量发展大会”于今日在松山湖畔隆重举办&#xff0c;会议以“推动软件和制造业深度融合发展&#xff0c;打造软件和信息服务业集聚高地”为主题&#xff0c;聚焦工业软件应用、智…

springboot引入redisson分布式锁及原理

1.引入依赖 <dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.13.6</version> </dependency>2.配置类创建bean /*** author qujingye* Classname RedissonConfig* Description TOD…

数据结构与集合源码

我是南城余&#xff01;阿里云开发者平台专家博士证书获得者&#xff01; 欢迎关注我的博客&#xff01;一同成长&#xff01; 一名从事运维开发的worker&#xff0c;记录分享学习。 专注于AI&#xff0c;运维开发&#xff0c;windows Linux 系统领域的分享&#xff01; 本…

java调用c函数

一、关于JNI JNI是Java Native Interface的缩写&#xff0c;JNI是JAVA平台专门用于和本地C代码进行相互操作的API&#xff0c;称为JAVA本地接口。 二、JNI开发流程 1.在JAVA中先声明一个native方法。2.通过javac -h或javah -jni命令导出JNI使用的C头头文件。3.使用C实现本地方…

跨国企业扎根中国市场,应该选择什么样的云服务?

众所周知&#xff0c;伴随着中国经济的高速发展&#xff0c;越来越多的跨国企业都将目光瞄向了中国市场。 然而&#xff0c;要想扎根中国市场&#xff0c;开展本地业务创新&#xff0c;什么样的云服务商才是这些跨国企业的最佳选择&#xff1f; 跨国企业转型创新的三大趋势 面对…

鸿蒙应用开发初尝试《创建项目》,之前那篇hello world作废

经过几年的迅速发展&#xff0c;鸿蒙抛弃了JAVA写应用的方式&#xff0c;几年前了解的鸿蒙显然就gg了。 这几年鸿蒙发布了方舟&#xff08;ArkUI Arkts&#xff09;&#xff0c;将TypeScript作为了推荐开发语言&#xff0c;你依然可以用FAJS,但华为推荐用StageArkTs!!!那么你还…

如何在工作外发展副业?主业和副业该如何权衡

有一句话说得好&#xff0c;不要把所有的鸡蛋放在一个篮子里。在面对繁忙的工作生活之外&#xff0c;想要拥有额外的收入来源那就是做一份不影响主业的副业。而副业的发展&#xff0c;不仅能够增加收入&#xff0c;更可以拓展个人的技能和兴趣。 主业跟副业该如何权衡呢&#x…

UI原型图

最近没啥项目&#xff0c;闲来无事&#xff0c;研究了一下原型图&#xff0c;万一以后年龄大了&#xff0c;代码敲不动还可以画画原型图&#xff0c;嘿嘿嘿 今天研究了两款画原型图的工具&#xff0c;即时设计-即时设计 - 可实时协作的专业 UI 设计工具 MODAO-墨刀 两款工具…

WordPress网站迁移实战经验

前几日,网站服务器到期,换了服务商,就把我的WordPress的网站迁移到本地电脑了。方便以后文章迁移。 本次迁移网站主要经历以下几个步骤。 1.域名转出。 2.备份数据库及网站文件下载。 3.重新搭建WordPress网站。 4.网站文件及数据库导入。 下面详细介绍下每个步骤的操作…

Gooxi亮相2023世界互联网大会 展现AI创新实力

■■ 11月7日&#xff0c;2023年世界互联网大会在中国乌镇正式拉开序幕&#xff0c;作为世界互联网一年一度的盛大集会&#xff0c;此次大会以“建设包容、普惠、有韧性的数字世界——携手构建网络空间命运共同体”为主题&#xff0c;涵盖5G与6G、IPv6、人工智能、大数据、网络…

【Linux】C文件系统详解(一)——C文件操作

文章目录 文件操作总结预备知识结论: C文件操作回顾语言方案w写入方式a写入方式r只读方式 系统方案但是这个**没有设置权限**,需要这样改: 文件操作总结 1.文件描述符,重定向,缓冲区,语言和系统关于文件的不同的视角的理解 – 都是要让我们深刻理解文件 2.文件系统 3.动静态库 …

【腾讯云云上实验室-向量数据库】TAI时代的数据枢纽-向量数据库 VectorDB

一、向量数据库的发展历程和时代机遇 回顾向量数据库的发展历程&#xff1a; 2012年开始&#xff0c;深度神经网络的发展催生了向量数据库的发展&#xff1b;2015年至2016年&#xff0c;Google和微软发布了标志性的论文&#xff1b;2017年&#xff0c;Facebook开源了Faiss框架…

牛客——OR36 链表的回文结构(C语言,配图,快慢指针)

本题是没有对C的支持的&#xff0c;但因为Cpp支持C&#xff0c;所以这里就用C写了&#xff0c;可以面向更多用户 链表的回文结构_牛客题霸_牛客网 (nowcoder.com) 思路一&#xff1a;链表翻转 简单的想想整形我们怎么比较&#xff0c;就是将整形A 依次取尾&#xff0c;放到整形…

html-网站菜单-点击显示导航栏

一、效果图 1.点击显示菜单栏&#xff0c;点击x号关闭&#xff1b; 2.点击一级菜单&#xff0c;展开显示二级&#xff0c;并且加号变为减号&#xff1b; 3.点击其他一级导航&#xff0c;自动收起展开的导航。 二、代码实现 <!DOCTYPE html> <html><head>&…

AE(2)_tuning时AE的一些策略

1、设置帧率&#xff1a; 修改帧率可以通过修改V_Blank 或者frame length。配置在寄存器中生效。 一帧图像的曝光时间 帧长 * 一行时间。提高帧长&#xff0c;1帧图像的曝光时间就变大了&#xff0c;单位时间内可曝光的帧数就少了&#xff0c;也就是帧率就下降了。这就是项目…

贪吃蛇游戏

package com.snake.controller;import javax.swing.JFrame; import javax.swing.JOptionPane;import com.snake.view.SnakeJPanel;public class SnakeStart {public static void main(String[] args) {int speed 0;String showInputDialog null;//初始化时间//得到速度while(…

2023年11月11日~11月17日周报(基于matlab生成模拟数据、批量修改文件名、重写dataset)

目录 一、前言 二、基于matlab生成模拟数据 二、批量修改文件名 三、代码调试 四、重写dataset 一、前言 上周完成了FCNVMB的训练与测试&#xff0c;但是由于数据量较少&#xff0c;训练效果不明显。工作站运行forward.py代码生成模拟数据的时候出现错误&#xff0c;未解决…

二次元商业计划书PPT模版

二次元商业计划书PPT模版 共&#xff1a;9页 PPT模版&#xff1a; 百度网盘 请输入提取码&#xff1a;ax48