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

news2024/10/7 18:23:18

曾经遇到的面试题, 如何实现自定义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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FindMy技术用于保温杯

在即将到来的冬季&#xff0c;每个人都开始给自己准备一个保温杯&#xff0c;保温杯是一种盛水的容器&#xff0c;主要由陶瓷或不锈钢制成&#xff0c;并加入真空层&#xff0c;以实现保温效果。这种杯子顶部有盖&#xff0c;密封严实&#xff0c;能够延缓内部液体散热&#xf…

基于黏菌算法优化概率神经网络PNN的分类预测 - 附代码

基于黏菌算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于黏菌算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于黏菌优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神经网络的光滑…

echarts 横向柱状图示例

该示例有如下几个特点&#xff1a; ①实现tooltip自定义样式&#xff08;echarts 实现tooltip提示框样式自定义-CSDN博客&#xff09; ②实现数据过多时滚动展示&#xff08;echarts 数据过多时展示滚动条-CSDN博客&#xff09; ③柱状图首尾展示文字&#xff0c;文字内容嵌入图…

【MMC/SD/SDIO】读写操作

SD 总线是基于命令和数据流&#xff0c;它们由一个开始 Bit 发起&#xff0c;由一个停止 Bit 结束。 Command&#xff1a;命令开始一个操作。命令由 Host 驱动&#xff0c;或者给单卡&#xff08;寻址命令&#xff09;&#xff0c;或者给所有连接的卡&#xff08;广播命令&…

【EI会议征稿】第七届大数据与应用统计国际学术研讨会(ISBDAS 2024)

第七届大数据与应用统计国际学术研讨会&#xff08;ISBDAS 2024&#xff09; 2024 7th International Symposium on Big Data and Applied Statistics 第七届大数据与应用统计国际学术研讨会&#xff08;ISBDAS 2024&#xff09;定于2024年3月8-10日在中国上海举行。会议旨在…

红黑树的插入与验证

红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每个结点上增加一个存储位表示结点的颜色&#xff0c;可以是Red或 Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制&#xff0c;红黑树确保没有一条路 径会比其他路径长出俩倍&#xff0c;因而是接近平衡的…

微信小程序获取手机号报错getPhoneNumber:fail no permission

目录 一、问题描述二、解决方法 一、问题描述 微信小程序调用 API 获取手机号报错&#xff1a; getPhoneNumber:fail no permission二、解决方法 小程序没有认证&#xff0c;需要对小程序进行微信认证。如果是复用公众号资质认证&#xff0c;在公众号关联小程序后&#xff0…

EtherCAT 伺服控制功能块实现

EtherCAT 是运动控制领域主要的通信协议&#xff0c;开源EtherCAT 主站协议栈 IgH 和SOEM 两个项目&#xff0c;IgH 相对更普及一些&#xff0c;但是它是基于Linux 内核的方式&#xff0c;比SOEM更复杂一些。使用IgH 协议栈编写一个应用程序&#xff0c;控制EtherCAT 伺服电机驱…

MIKE水动力笔记19_统计平均潮差

本文目录 前言Step 1 ArcGIS中创建渔网点Step 2 将dfsu数据提取到渔网点Step 3 Python统计平均潮差 前言 日平均潮差&#xff08;average daily tidal range&#xff09;&#xff1a;日高潮潮高合计之和除以实有高潮个数为日平均高潮潮高&#xff0c;日低潮潮高合计之和除以实…

【漏洞复现】NUUO摄像头存在远程命令执行漏洞

漏洞描述 NUUO摄像头是中国台湾NUUO公司旗下的一款网络视频记录器&#xff0c;该设备存在远程命令执行漏洞&#xff0c;攻击者可利用该漏洞执行任意命令&#xff0c;进而获取服务器的权限。 免责声明 技术文章仅供参考&#xff0c;任何个人和组织使用网络应当遵守宪法法律&…

【C语法学习】25 - strncpy()函数

文章目录 1 函数原型2 参数3 返回值4 使用说明5 示例5.1 示例15.2 示例2 1 函数原型 strncpy()&#xff1a;将str指向的字符串的前n个字符拷贝至dest&#xff0c;函数原型如下&#xff1a; char *strncpy(char *dest, const char *src, size_t n);2 参数 strncpy()函数有三个…

linux进程间通信之共享内存(mmap,shm_open)

共享内存&#xff0c;顾名思义就是允许两个不相关的进程访问同一个逻辑内存&#xff0c;共享内存是两个正在运行的进 程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常为同一段物理内存。进程可以将同一段物理内存连接到他们自己的地址空间中&#xff0c…

信号的机制——信号处理函数的注册

在 Linux 操作系统中&#xff0c;为了响应各种各样的事件&#xff0c;也是定义了非常多的信号。我们可以通过 kill -l 命令&#xff0c;查看所有的信号。 # kill -l1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP6) SIGABRT 7) SIGBUS …

【Spring】加载properties文件

文章目录 在Spring Context中加载properties文件测试总结 在Spring Context中加载properties文件 分为三步&#xff0c;如下图所示&#xff1a; 完整代码&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <beans xmlns"http://www.…

【Linux】U盘安装的cfg引导文件配置

isolinux.cfg文件 default vesamenu.c32 timeout 600display boot.msg# Clear the screen when exiting the menu, instead of leaving the menu displayed. # For vesamenu, this means the graphical background is still displayed without # the menu itself for as long …

计算机是如何工作的(简单介绍)

目录 一、冯诺依曼体系 二、CPU基本流程工作 逻辑⻔ 电⼦开关——机械继电器(Mechanical Relay) ⻔电路(Gate Circuit) 算术逻辑单元 ALU&#xff08;Arithmetic & Logic Unit&#xff09; 算术单元(ArithmeticUnit) 逻辑单元(Logic Unit) ALU 符号 寄存器(Regis…

java:IDEA中的Scratches and Consoles

背景 IntelliJ IDEA中的Scratches and Consoles是一种临时的文件编辑环境&#xff0c;用于写一些文本内容或者代码片段。 其中&#xff0c;Scratch files拥有完整的运行和debug功能&#xff0c;这些文件需要指定编程语言类型并且指定后缀。 举例&#xff1a;调接口 可以看到…

Unity——URP相机详解

2021版本URP项目下的相机&#xff0c;一般新建一个相机有如下组件 1:Render Type(渲染类型) 有Base和Overlay两种选项&#xff0c;默认是Base选项 Base:主相机使用该种渲染方式&#xff0c;负责渲染场景中的主要图形元素 Overlay&#xff08;叠加&#xff09;:使用了Oveylay的…

Python大数据之linux学习总结——day09_hive函数

hive函数 函数分类标准[重点] 知识点: 原生分类标准: 内置函数 和 用户定义函数(UDF,UDAF,UDTF)分类标准扩大化: 本来&#xff0c;UDF 、UDAF、UDTF这3个标准是针对用户自定义函数分类的&#xff1b; 但是&#xff0c;现在可以将这个分类标准扩大到hive中所有的函数&#…

vue项目如何防范XSS攻击?

场景&#xff1a; 前后端交互的过程中&#xff0c;前端使用v-html或者{{}}渲染时&#xff0c;网页自动执行其恶意代码&#xff0c;如页面弹窗、跳转到钓鱼网站等 解决方案&#xff1a; 先说解决方式&#xff0c;其原理下文解释. 由于我是vue项目所以用的是vue-dompurify-html这…