面试题 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中注册CallbackmChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);notifyRendererOfFramePending();pokeDrawLockIfNeeded();}
}//向mChoreographer注册的Callback类
final class TraversalRunnable implements Runnable {@Overridepublic 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类型CallbackdoCallbacks(Choreographer.CALLBACK_INPUT, frameData, frameIntervalNanos);//执行Input类型CallbackdoCallbacks(Choreographer.CALLBACK_ANIMATION, frameData, frameIntervalNanos);//执行动画类型CallbackdoCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameData,frameIntervalNanos);//执行绘制类型CallbackdoCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameData, frameIntervalNanos);//执行Commit类型CallbackdoCallbacks(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 = 0private 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 + mDoFrameIntervalwhile (targetVsyncTime + mDoFrameInterval <= expectDrawTime) { //得出对应的Vsync时间targetVsyncTime += mDoFrameInterval}val curTime = SystemClock.uptimeMillis()var delayTime = targetVsyncTime - curTimeif (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  onDraw60帧(第一帧与最后一帧时间)
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  onDraw120帧(第一帧与最后一帧时间)
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.hqwc.cn/news/188909.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于java web的中小型人力资源管理系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

[工业自动化-23]:西门子S7-15xxx编程 - 软件编程 - 西门子PLC人机界面交互HMI功能概述、硬件环境准备、软件环境准备

目录 一、什么是人机界面 二、什么是PLC人机交互界面HMI 三、人机界面设计的功能列表 四、开发主机与PLC的连接方式 五、开发主机与HMI的连接方式 六、HMI组态 一、什么是人机界面 人机界面是指人与机器或系统之间的交互界面。它是人类与计算机或其他设备之间进行信息交换…

基于STM32的蓝牙低功耗(BLE)通信方案设计与实现

蓝牙低功耗&#xff08;Bluetooth Low Energy&#xff0c;简称BLE&#xff09;是一种能够在低功耗环境下实现无线通信的技术。在物联网应用中&#xff0c;BLE被广泛应用于传感器数据采集、健康监测设备、智能家居等领域。本文将基于STM32微控制器&#xff0c;设计并实现一个简单…

鸿蒙4.0开发笔记之DevEco Studio启动时不直接打开原项目

1、想要在DevEco Studio启动时不直接打开关闭前的那个项目&#xff0c;可以在设置中进行。 有两个位置可以进入“设置”&#xff0c;一个是左上角的File>Settings&#xff0c;二是右上方的设置图标。 2、进入Settings界面以后&#xff0c;选择Appearance&Behavior下面…

使用Docker部署Python Flask应用的完整教程

一、引言 Docker是一种开源的容器化平台&#xff0c;可以将应用程序及其依赖项打包成一个独立的容器&#xff0c;实现快速部署和跨平台运行。本文将详细介绍如何使用Docker来部署Python Flask应用程序&#xff0c;帮助开发者更高效地构建和部署应用。 二、准备工作 在开始之前…

Vellum —— Constraint 约束

目录 Stretch Bend Pin Drag 解算器对DOP外节点的约束属性&#xff0c;只会读取起始帧的值&#xff1b; Stretch 保持点间的初始距离&#xff1b; Stiffness 越高的stiffness&#xff0c;就需要越多的迭代来收敛&#xff0c;如constraint iterations或substeps(子步会更好)…

jenkins清理缓存命令

def jobName "yi-cloud-operation" //删除的项目名称 def maxNumber 300 // 保留的最小编号&#xff0c;意味着小于该编号的构建都将被删除 Jenkins.instance.getItemByFullName(jobName).builds.findAll { it.number < maxNumber }.each { it.delet…

.Net6 部署到IIS示例

基于FastEndpoints.Net6 框架部署到IIS 环境下载与安装IIS启用与配置访问网站 环境下载与安装 首先下载环境安装程序&#xff0c;如下图所示,根据系统位数选择x86或者x64进行下载安装,网址&#xff1a;Download .NET 6.0。 IIS启用与配置 启用IIS服务 打开控制面板&#xff…

初学Redis(Redis的启动以及字符串String)

首先使用在Windows PowerShell中输入指令来启动Redis&#xff1a; redis-server.exe 然后通过指令连接Redis&#xff1a; redis-cli 上图的127.0.0.1是计算机的回送地址 &#xff0c;6379是默认端口 上述代码中创建了两个键&#xff0c;注意Redis中严格区分大小写&#xff0…

JUC工具类_CyclicBarrier与CountDownLatch

最近被问到CyclicBarrier和CountDownLatch相关的面试题&#xff0c;CountDownLatch平时工作中经常用到&#xff0c;但是CyclicBarrier没有用过&#xff0c;一时答不上来&#xff0c;因此简单总结记录一下 1.什么是CyclicBarrier&#xff1f; 1.1 概念 CyclicBarrier&#xff…

2024年山东省职业院校技能大赛中职组 “网络安全”赛项竞赛试题-A卷

2024年山东省职业院校技能大赛中职组 “网络安全”赛项竞赛试题-A卷 2024年山东省职业院校技能大赛中职组 “网络安全”赛项竞赛试题-A卷A模块基础设施设置/安全加固&#xff08;200分&#xff09;A-1&#xff1a;登录安全加固&#xff08;Windows, Linux&#xff09;A-2&#…

航天联志Aisino-AISINO26081R服务器通过调BIOS用U盘重新做系统(windows系统通用)

产品名称:航天联志Aisino系列服务器 产品型号:AISINO26081R CPU架构&#xff1a;Intel 的CPU&#xff0c;所以支持Windows Server all 和Linux系统&#xff08;重装完系统可以用某60驱动管家更新所有硬件驱动&#xff09; 操作系统&#xff1a;本次我安装的服务器系统为Serv…