打卡学习Android开发 ——布局优化

屏幕刷新机制

基本概念

  • 刷新率:屏幕每秒刷新的次数,单位是 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 状态下的最终视图。

如有需要更多性能优化学习资料 请点击免费领取

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 好的,Android Studio 是一个用于开发 Android 应用程序的集成开发环境。打卡学习 app 可以帮助用户记录每天的学习进度和打卡情况,提高学习效率和自律性。在 Android Studio 中,可以使用 Java 或 Kotlin 编写应用程序,并使用 Android SDK 提供的各种组件和 API 来实现各种功能。同时,还可以使用 Android Studio 提供的布局编辑器和调试工具来简化开发过程。 ### 回答2: Android Studio 是一款专门用于开发 Android 应用程序的 IDE(集成开发环境),因此可以使用该工具开发一个学习打卡的应用程序。在开发此类应用程序时,您可以使用 Android Studio 提供的各种工具和功能来创建一个美观、易于使用且功能强大的应用程序。 首先,应用程序的主要功能应该是让用户能够记录他们的学习进度和打卡情况。可以通过一个登陆页面注册用户信息,并且提供一个主界面用于展示用户已经完成了哪些任务和目标以及还有哪些任务和目标需要完成。为了方便用户对自己的数据进行管理和查询,最好提供类似于日历式的界面,让用户能够直观地了解自己的学习计划是否完成。 其次,应用程序还需要提供一些辅助功能,以帮助用户更好地完成自己的学习目标。例如,可以集成一些学习资源,例如在线课程、学习资料和学习笔记,让用户可以更加便捷地获取知识和资料。此外,还可以利用一些算法和技术,提供智能化的内容推荐和学习建议,帮助用户更加高效地学习。 最后,为了提升应用程序的用户体验,还可以加入一些交互式元素,例如音效、动画和个性化主题。此外,还可以添加社交化功能,例如让用户可以在应用程序中创建学习小组,并邀请好友加入,共同学习。这些元素可以增加用户使用应用程序时的趣味性和互动性,加强用户黏度,提高用户满意度。 总之,使用 Android Studio 开发一个学习打卡的应用程序需要综合考虑用户需求和使用体验,结合技术和设计进行开发。只有在做好以上方方面面的工作的基础上,才能打造出一款优秀的学习打卡应用程序。 ### 回答3: Android Studio是一款针对安卓平台开发应用程序的集成开发环境,是开发安卓应用程序的主流开发工具之一。而打卡学习app则是一款管理个人学习进度、记录学习时间、提升学习效率的应用,拥有广泛的用户群体和市场需求。 在使用Android Studio开发打卡学习app时,首先需要搭建开发环境,包括安装JDK、Android Studio软件和安卓模拟器等。其次,需要进行UI设计,在xml文件中编写界面布局,这是打卡学习app的重要组成部分。可以上网寻找一些UI设计的素材,把这些素材的样式、颜色、布局等元素融入到应用程序中。然后需要编写Java代码,实现app的逻辑功能,包括数据存储、定时提醒、用户登录和注册、学习计划管理等等。 在应用程序的开发过程中,需要结合安卓开发的一些基础知识,例如Activity组件、Intent、Adapter等,以及常用的三方库和框架,例如Retrofit、Gson、ButterKnife等。同时还需要高效地利用Android Studio提供的各类工具,例如调试工具、性能分析工具、布局测试工具等。 在应用程序开发完成后,还需要进行应用程序的测试和发布,通过模拟器和真实设备测试应用程序的性能、稳定性、兼容性等。同时还需要在应用商店中发布应用程序,让更多的用户下载并使用。 总的来说,在开发打卡学习app时,需要掌握安卓开发的基础知识,结合Android Studio提供的各种工具和平台,高效地开发出一个稳定、高效、易用的应用程序。同时也需要注重用户体验,不断优化应用程序,增强用户对应用的使用体验和黏性,提高应用程序竞争力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值