屏幕改写机制

基本概念

  • 改写率:屏幕每秒改写的次数,单位是 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 状况下的终究视图。