Android 屏幕刷新机制与优化方案~

news2025/1/21 1:27:21

作者:阿健君

屏幕刷新机制

基本概念

  • 刷新率:屏幕每秒刷新的次数,单位是 Hz,例如 60Hz,刷新率取决于硬件的固定参数。
  • 帧率:GPU 在一秒内绘制操作的帧数,单位是 fps。Android 采用的是 60fps,即每秒 GPU 最多绘制 60 帧画面,帧率是动态变化的,例如当画面静止时,GPU 是没有绘制操作的,帧率就为0,屏幕刷新的还是 buffer 中的数据,即 GPU 最后操作的帧数据。

显示器不是一次性将画面显示到屏幕上,而是从左到右边,从上到下逐行扫描,顺序显示整屏的一个个像素点,不过这一过程快到人眼无法察觉到变化。以 60 Hz 刷新率的屏幕为例,这一过程的耗时: 1000 / 60 ≈ 16.6ms。

屏幕刷新的机制大概就是: CPU 执行应用层的测量,布局和绘制等操作,完成后将数据提交给 GPU,GPU 进一步处理数据,并将数据缓存起来,屏幕由一个个像素点组成,以固定的频率(16.6ms)从缓冲区中取出数据来填充像素点。

画面撕裂

如果一个屏幕内的数据来自两个不同的帧,画面会出现撕裂感。屏幕刷新率是固定的,比如每 16.6ms 从 buffer 取数据显示完一帧,理想情况下帧率和刷新率保持一致,即每绘制完成一帧,显示器显示一帧。但是 CPU 和 GPU 写数据是不可控的,所以会出现 buffer 里有些数据根本没显示出来就被重写了,即 buffer 里的数据可能是来自不同的帧,当屏幕刷新时,此时它并不知道 buffer 的状态,因此从 buffer 抓取的帧并不是完整的一帧画面,即出现画面撕裂。

那怎么解决这个问题呢?Android 系统采用的是 双缓冲 + VSync

双缓冲:让绘制和显示器拥有各自的 buffer,GPU 将完成的一帧图像数据写入到 BackBuffer,而显示器使用的是 FrameBuffer,当屏幕刷新时,FrameBuffer 并不会发生变化,当 BackBuffer 准备就绪后,它们才进行交换。那什么时候进行交换呢?那就得靠 VSync。

VSync:当设备屏幕刷新完毕后到下一帧刷新前,因为没有屏幕刷新,所以这段时间就是缓存交换的最佳时间。此时硬件屏幕会发出一个脉冲信号,告知 GPU 和 CPU 可以交换了,这个就是 Vsync 信号。

掉帧

有时,当布局比较复杂,或者设备性能较差的时候,CPU 并不能保证在 16.6ms 内就完成绘制,这里系统又做了一个处理,当正在往 BackBuffer 填充数据时,系统会将 BackBuffer 锁定。如果到了 GPU 交换两个 Buffer 的时间点,你的应用还在往 BackBuffer 中填充数据,会发现 BackBuffer 被锁定了,它会放弃这次交换。
这样做的后果就是手机屏幕仍然显示原先的图像,这就是所谓的掉帧。

优化方向

如果想要屏幕流畅运行,就必须保证 UI 全部的测量,布局和绘制的时间在 16.6ms 内,因为人眼与大脑之间的协作无法感知超过 60fps 的画面更新,也就是 1000 / 60Hz = 16.6ms,也就是说超过 16.6ms 用户就会感知到卡顿。

层级优化

层级越少,View 绘制得就越快,常用有两个方案。

  • 合理使用 RelativeLayout 和 LinearLayout:层级一样优先使用 LinearLayout,因为 RelativeLayout 需要考虑视图之间的相对位置关系,需要更多的计算和更高的系统开销,但是使用 LinearLayout 有时会使嵌套层级变多,这时就应该使用 RelativeLayout。

  • 使用 merge 标签:它会直接将其中的子元素添加到 merge 标签 Parent 中,这样就不会引入额外的层级。它只能用在布局文件的根元素,不能在 ViewStub 中使用 merge 标签,当需要 inflate 的布局本身是由 merge 作为根节点的话,需要将其置于 ViewGroup 中,设置 attachToRoot 为 true。

一个布局可以重复利用,当使用 include 引入布局时,可以考虑 merge 作为根节点,merge 根节点内的布局取决于include 这个布局的父布局。编写 XML 时,可以先用父布局作为根节点,然后完成后再用 merge 替换,方便我们预览效果。

merge_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="World" />

</merge>

父布局如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <include layout="@layout/merge_layout" />

</LinearLayout>

如果需要通过 inflate 引入 merge_layout 布局文件时,可以这样引入:

class MyLinearLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {

    init {
        LayoutInflater.from(context).inflate(R.layout.merge_layout, this, true)
    }
}

第一个参数为 merge 布局文件 id,第二个参数为要将子视图添加到的 ViewGroup,第三个参数为是否将加载好的视图添加到 ViewGroup 中。

需要注意的是,merge 标签的布局,是不能设置 padding 的,比如像这样:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="30dp">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="World" />

</merge>

上面的这个 padding 是不会生效的,如果需要设置 padding,可以在其父布局中设置。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="30dp"
    tools:context=".MainActivity">

    <include layout="@layout/merge_layout" />

</LinearLayout>

ViewStub

ViewStub 是一个轻量级的 View,一个看不见的,并且不占布局位置,占用资源非常小的视图对象。可以为 ViewStub 指定一个布局,加载布局时,只有 ViewStub 会被初始化,当 ViewStub 被设置为可见或 inflate 时,ViewStub 所指向的布局会被加载和实例化,可以使用 ViewStub 来设置是否显示某个布局。

ViewStub 只能用来加载一个布局文件,且只能加载一次,之后 ViewStub 对象会被置为空。适用于某个布局在加载后就不会有变化,想要控制显示和隐藏一个布局文件的场景,一个典型的场景就是我们网络请求返回数据为空时,往往要显示一个默认界面,表明暂无数据。

view_stub_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="no data" />

</LinearLayout>

通过 ViewStub 引入

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="click"
            type="com.example.testapp.MainActivity.ClickEvent" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{click::showView}"
            android:text="show" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{click::hideView}"
            android:text="hide" />

        <ViewStub
            android:id="@+id/default_page"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout="@layout/view_stub_layout" />

    </LinearLayout>
</layout>

然后在代码中 inflate,这里通过按钮点击来控制其显示和隐藏。

class MainActivity : AppCompatActivity() {

    private var viewStub: ViewStub? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding =
            DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        binding.click = ClickEvent()
        viewStub = binding.defaultPage.viewStub
        if (!binding.defaultPage.isInflated) {
            viewStub?.inflate()
        }
    }

    inner class ClickEvent {
        // 后面 ViewStub 已经回收了,所以只能用 GONE 和 VISIBLE
        fun showView(view: View) {
            viewStub?.visibility = View.VISIBLE
        }

        fun hideView(view: View) {
            viewStub?.visibility = View.GONE
        }
    }
}

过度绘制

过度绘制是指屏幕上的某个像素在同一帧的时间内被绘制了多次,在多层次重叠的 UI 结构中,如果不可见的 UI 也在做绘制操作,就会导致某些像素区域被绘制了多次,从而浪费了 CPU 和 GPU 资源。

我们可以打开手机的开发人员选项,打开调试 GPU 过度绘制的开关,就能通过不同的颜色区域查看过度绘制情况。我们要做的,就是尽量减少红色,看到更多的蓝色。

  • 无色:没有过度绘制,每个像素绘制了一次。
  • 蓝色:每个像素多绘制了一次,蓝色还是可以接受的。
  • 绿色:每个像素多绘制了两次。
  • 深红:每个像素多绘制了4次或更多,影响性能,需要优化,应避免出现深红色区域。

优化方法

  • 减少不必要的背景:比如 Activity 往往会有一个默认的背景,这个背景由 DecorView 持有,当自定义布局有一个全屏的背景时,这个 DecorView 的背景对我们来说是无用的,但它会产生一次 Overdraw,可以干掉。
window.setBackgroundDrawable(null)
  • 自定义 View 的优化:在自定义 View 的时候,某个区域可能会被绘制多次,造成过度绘制。可以通过 canvas.clipRect 方法指定绘制区域,可以节约 CPU 与 GPU 资源,在 clipRect 区域之外的绘制指令都不会被执行。

AsyncLayoutInflater

setContentView 函数是在 UI 线程执行的,其中有一系列的耗时动作:XML 的解析,View 的反射创建等过程都是在 UI 线程执行的,AsyncLayoutInflater 就是把这些过程以异步的方式执行,保持 UI 线程的高响应。

implementation 'androidx.asynclayoutinflater:asynclayoutinflater:1.0.0'

class TestActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        AsyncLayoutInflater(this).inflate(R.layout.activity_test, null) { view, _, _ ->
            setContentView(view)
        }
    }
}

这样,将 UI 的加载过程迁移到了子线程,保证了 UI 线程的高响应,使用时需要特别注意,调用 UI 一定要等它初始化完成之后,不然可能会产生崩溃。

Compose

Jetpack Compose 相对于传统的 XML 布局方式,具有更强的可组合性,更高的效率和更佳的开发体验,相信未来会成为 Android UI 开发的主流方式。

传统的 XML 布局方式是基于声明式的 XML 代码编写的,使用大量的 XML 标签来描述 UI 结构,XML 文件通过解析和构建生成 View 对象,并将它们添加到 View 树中。在 Compose 中,UI 代码被组织成可组合的函数,每个函数都负责构建某个具体的 UI 元素,UI 元素的渲染是由 Compose 运行时直接管理的,Composable 函数会被调用,以计算并生成当前 UI 状态下的最终视图。


其实除了布局优化与UI优化以外,Android 性能优化中还有 内存优化、网络优化、卡顿优化、存储优化……等,为了让大家一次都可以了解全,所以将其整合成名为《Android 性能优化核心知识点手册》,大家可以参考下:

《APP 性能调优进阶手册》:https://qr18.cn/FVlo89

启动优化

内存优化

UI优化

网络优化

Bitmap优化与图片压缩优化

多线程并发优化与数据传输效率优化

体积包优化

《Android 性能调优核心笔记汇总》:https://qr18.cn/FVlo89

《Android 性能监控框架》:https://qr18.cn/FVlo89

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

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

相关文章

小程序组件的生命周期

组件生命周期 组件的生命周期&#xff0c;指的是组件自身的一些函数&#xff0c;这些函数在特殊的时间点或遇到一些特殊的框架事件时被自动触发。 其中&#xff0c;最重要的生命周期是 created attached detached &#xff0c;包含一个组件实例生命流程的最主要时间点。 …

(链表专题) 83. 删除排序链表中的重复元素 ——【Leetcode每日一题】

83. 删除排序链表中的重复元素 给定一个已排序的链表的头 head &#xff0c; 删除所有重复的元素&#xff0c;使每个元素只出现一次 。返回 已排序的链表 。 示例 1&#xff1a; 输入&#xff1a;head [1,1,2] 输出&#xff1a;[1,2] 示例 2&#xff1a; 输入&#xff1a;he…

在线Plist文件格式转Json文件格式

Plist文件是一种用于存储应用程序配置信息的文件格式&#xff0c;其中包含应用程序的各种设置和数据。在过去&#xff0c;Plist文件通常是以 .plist 格式存储的。然而&#xff0c;随着时间的推移&#xff0c;人们开始使用 JSON 格式来存储更复杂的数据结构和数据。如果您需要将…

浏览器便携化操作方法

直接进入主题 如果我们不想把 Chrome 安装进 C 盘&#xff0c;又或者想测试多配置&#xff0c;那么浏览器的便携化就非常重要了。 浏览器便携化的方法有很多&#xff0c;国内常用的有两种。 1、MyChrome MyChrome 最早由网友“甲壳虫”开发&#xff0c;除了浏览器便携化&a…

Linux0.11 进程切换(十)

系列文章目录 Linux 0.11启动过程分析&#xff08;一&#xff09; Linux 0.11 fork 函数&#xff08;二&#xff09; Linux0.11 缺页处理&#xff08;三&#xff09; Linux0.11 根文件系统挂载&#xff08;四&#xff09; Linux0.11 文件打开open函数&#xff08;五&#xff09…

AI 写文献回顾,好使吗?

&#xff08;注&#xff1a;本文为小报童精选文章&#xff0c;已订阅小报童或加入知识星球「玉树芝兰」用户请勿重复付费&#xff09;作为一个大学老师&#xff0c;我平时少不了指导学生写论文。选题阶段很重要的工作&#xff0c;就是文献回顾。文献回顾的目的&#xff0c;是要…

jsp企业职工考勤管理系统myeclipse定制开发sqlserver数据库网页模式java编程jdbc

一、源码特点 jsp企业职工考勤管理系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 jsp企业职工考勤管理系统myeclipse定制开发sqls二、功能介绍 此次系统主要在JS…

Java集合框架概述

java中的集合分为单列集合(Collection)和双列集合(Map)&#xff0c;都在java.util包下&#xff0c;所有的集合框架都分为三大块内容&#xff1a;对外的接口、接口的实现、集合中的算法&#xff0c; 接口&#xff1a;代表集合的抽象数据类型&#xff0c;Collection(List、Set)、…

使用向量机(SVM)算法的推荐系统部署实现

包括3个模块&#xff1a;数据预处理、模型训练及保存、模型测试&#xff0c;下面分别给出各模块的功能介绍及相关代码。 数据集下载链接为https://www.aitechclub.com/data-detail? data_id29&#xff0c;停用词典下载链接为http://www.datasoldier.net/archives/636。 1.数…

Python进阶内容--迭代器和生成器

什么是迭代器 在 Python 中&#xff0c;迭代器&#xff08;Iterator&#xff09;是一个访问集合元素的对象&#xff0c;它能够实现遍历集合的所有元素&#xff0c;而无需了解集合底层结构和细节。Python 中所有可迭代的对象&#xff08;如 列表、元组、字符串、字典、集合等&a…

NumPy 基础知识 :1~5

原文&#xff1a;Numpy Essentials 协议&#xff1a;CC BY-NC-SA 4.0 译者&#xff1a;飞龙 一、NumPy 简介 “我宁愿使用通用语言进行数学运算&#xff0c;也不愿尝试使用数学语言进行通用编程。” – John D Cook 在过去的十年中&#xff0c;Python 已成为科学计算中最受欢迎…

(C语言版)力扣(LeetCode)面试题 17.04. 消失的数字5种解法

消失的数字题目介绍第一种解法&#xff1a;按位异或第二种解法&#xff1a;公式运算第三种解法&#xff1a;临时数组第四种解法&#xff1a;相加再相减第五种解法&#xff1a;快排加二分查找结语题目介绍 该题目取自力扣&#xff08;LeetCode&#xff09;面试题 17.04. 消失的…

Direct3D 12——灯光——光照模型的概述

将之前所述的所有光照内容都结合起来&#xff0c;即表面反射的光量相当于环境反射光、漫反射光以及 镜面反射光的光量总和。 1.环境光Ca&#xff1a;模拟经表面反射的间接光量。 2.漫反射光Cd&#xff1a;对进入介质内部&#xff0c;又经过表面下吸收而最终散射岀表面的光进行…

盐边县高堰沟灌区综合信息化管理系统及平台建设-案例分享

项目背景 盐边县南部降雨逐年减少&#xff0c;可用水源有限&#xff0c;部分村庄的水源取自仅20亩的山坪塘&#xff0c;常常因降雨量小而蓄不满水&#xff0c;出现“人争水、地缺水”的现象&#xff0c;且支渠及以下渠系基本为土渠&#xff0c;渗漏损失严重。为解决不同程度的工…

解决魔兽世界wow中鼠标莫名其妙消失或自动跳回屏幕中间等类似问题整理

魔兽世界已经离开中国大陆~~&#xff0c;最近在玩xxx&#xff0c;咳咳&#xff0c;突然发现一个远古问题&#xff0c;一直以为是自己的鼠标问题&#xff0c;但这么多年换了这么多台电脑&#xff0c;这个问题也一直存在&#xff0c;今天突发奇想查了一下&#xff0c;结果竟然有答…

能聊天、会学习,远不是GPT的终局

自然语言处理&#xff08;NLP&#xff09;技术的发展和运用&#xff0c;使得计算机性能增长速度一举跃过摩尔定律瓶颈&#xff0c;将AI拱入属于它的高光时代。而象征技术融合的ChatGPT一夜爆红&#xff0c;仿佛给整个商业社会带来了一次“技术革命”。 微软、谷歌、百度、华为…

C++ 23 实用工具(二)绑定工具

C 23 实用工具&#xff08;二&#xff09;绑定工具 Adaptors for Functions std::bind、std::bind_front、std::bind_back和std::function这四个函数非常适合一起使用。 其中&#xff0c;std::bind、std::bind_front和std::bind_back可以让您即时创建新的函数对象&#xff0c…

一文! 解决镜像法,电轴法在电磁场中的应用

目录 镜像法原理 例题 模型一&#xff1a;无限大导体平面 一些理解 模型二&#xff1a;球面镜像 情况一&#xff1a;球壳接地 同样的几点思考&#xff1a; 情况二&#xff1a;球壳不接地 球壳不接地&#xff0c;但是点电荷放置在内部 镜像法在双层介质中的作用 电轴法…

C++017-C++指针及其应用

文章目录C017-C指针及其应用C指针及其应用CSP-J目标1. 指针1.指针变量的定义、赋值2.指针的引用与运算2. 基于指针的数组访问3. 指针与字符串4. 结构体与指针在练习&#xff1a;总结C017-C指针及其应用 在线练习&#xff1a; http://noi.openjudge.cn/ https://www.luogu.com.c…

设计师都在用的6个免费设计素材网站~

本期给大家推荐几个设计师都在用的素材网站&#xff0c;免费下载&#xff0c;赶紧收藏起来吧~ 1、菜鸟图库 https://www.sucai999.com/?vNTYwNDUx 菜鸟图库是我推荐过很多次的网站&#xff0c;主要是站内素材多&#xff0c;像平面、UI、电商等设计素材都能找到&#xff0c;还…