------《一篇文章搞定Android布局优化》
前言
在使用ViewPager时 , 如果我们的适配器使用的是 Fragment。
Android的绘制优化其实可以分为两个部分,即布局(UI)优化和卡顿优化,而布局优化的核心问题就是要解决因布局渲染性能不佳而导致应用卡顿的问题,所以它可以认为是卡顿优化的一个子集。本文主要包括以下内容:
- 为什么要进行布局优化及android绘制,布局加载原理
- 获取布局文件加载耗时的方法
- 介绍一些布局优化的手段与方法
为什么要进行布局优化?
为什么要进行布局优化?答案是显而易见的,如果布局嵌套过深,或者其他原因导致布局渲染性能不佳,可能会导致应用卡顿 那么布局到底是如何导致渲染性能不佳的呢?首先我们应该了解下android绘制原理与布局加载原理
Android绘制原理
Android的屏幕刷新中涉及到最重要的三个概念(为便于理解,这里先做简单介绍)
CPU:执行应用层的measure、layout、draw等操作,绘制完成后将数据提交给GPU
GPU:进一步处理数据,并将数据缓存起来
屏幕:由一个个像素点组成,以固定的频率(16.6ms,即1秒60帧)从缓冲区中取出数据来填充像素点
总结一句话就是:CPU 绘制后提交数据、GPU 进一步处理和缓存数据、最后屏幕从缓冲区中读取数据并显示

双缓冲机制
看完上面的流程图,我们很容易想到一个问题,屏幕是以16.6ms的固定频率进行刷新的,但是我们应用层触发绘制的时机是完全随机的(比如我们随时都可以触摸屏幕触发绘制)
如果在GPU向缓冲区写入数据的同时,屏幕也在向缓冲区读取数据,会发生什么情况呢?有可能屏幕上就会出现一部分是前一帧的画面,一部分是另一帧的画面,这显然是无法接受的,那怎么解决这个问题呢?
所以,在屏幕刷新中,Android系统引入了双缓冲机制
GPU只向Back Buffer中写入绘制数据,且GPU会定期交换Back Buffer和Frame Buffer,交换的频率也是60次/秒,这就与屏幕的刷新频率保持了同步。

虽然我们引入了双缓冲机制,但是我们知道,当布局比较复杂,或设备性能较差的时候,CPU并不能保证在16.6ms内就完成绘制数据的计算,所以这里系统又做了一个处理。
当你的应用正在往Back Buffer中填充数据时,系统会将Back Buffer锁定。
如果到了GPU交换两个Buffer的时间点,你的应用还在往Back Buffer中填充数据,GPU会发现Back Buffer被锁定了,它会放弃这次交换。
这样做的后果就是手机屏幕仍然显示原先的图像,这就是我们常常说的掉帧
布局加载原理
由上面可知,导致掉帧的原因是CPU无法在16.6ms内完成绘制数据的计算。
而之所以布局加载可能会导致掉帧,正是因为它在主线程上进行了耗时操作,可能导致CPU无法按时完成数据计算
布局加载主要通过setContentView来实现,我们就不在这里贴源码了,一起来看看它的时序图

布局加载优化的一些方法介绍
布局加载慢的主要原因有两个,一个是IO,一个是反射。所以我们的优化思路一般有两个
1.侧面缓解(异步加载)
2.根本解决(不需要IO,反射过程,如X2C,Anko,Compose等)
AsyncLayoutInflater方案
AsyncLayoutInflater 是来帮助做异步加载 layout 的,inflate(int, ViewGroup, OnInflateFinishedListener) 方法运行结束之后 OnInflateFinishedListener 会在主线程回调返回 View;这样做旨在 UI 的懒加载或者对用户操作的高响应。
简单的说我们知道默认情况下 setContentView 函数是在 UI 线程执行的,其中有一系列的耗时动作:Xml的解析、View的反射创建等过程同样是在UI线程执行的,AsyncLayoutInflater 就是来帮我们把这些过程以异步的方式执行,保持UI线程的高响应。
使用如下:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new AsyncLayoutInflater(AsyncLayoutActivity.this)
.inflate(R.layout.async_layout, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(View view, int resid, ViewGroup parent) {
setContentView(view);
}
});
// 别的操作
}
这样做的优点在于将UI加载过程迁移到了子线程,保证了UI线程的高响应 缺点在于牺牲了易用性,同时如果在初始化过程中调用了UI可能会导致崩溃
X2C方案
X2C是掌阅开源的一套布局加载框架
它的主要是思路是在编译期,将需要翻译的layout翻译生成对应的java文件,这样对于开发人员来说写布局还是写原来的xml,但对于程序来说,运行时加载的是对应的java文件。
这就将运行时的开销转移到了编译时。如下所示,原始xml文件:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:paddingLeft="10dp">
<include
android:id="@+id/head"
layout="@layout/head"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true" />
<ImageView
android:id="@+id/ccc"
style="@style/bb"
android:layout_below="@id/head" />
</RelativeLayout>
X2C 生成的 Java 文件
public class X2C_2131296281_Activity_Main implements IViewCreator {
@Override
public View createView(Context ctx, int layoutId) {
Resources res = ctx.getResources();
RelativeLayout relativeLayout0 = new RelativeLayout(ctx);
relativeLayout0.setPadding((int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,10,res.getDisplayMetrics())),0,0,0);
View view1 =(View) new X2C_2131296283_Head().createView(ctx,0);
RelativeLayout.LayoutParams layoutParam1 = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
view1.setLayoutParams(layoutParam1);
relativeLayout0.addView(view1);
view1.setId(R.id.head);
layoutParam1.addRule(RelativeLayout.CENTER_HORIZONTAL,RelativeLayout.TRUE);
ImageView imageView2 = new ImageView(ctx);
RelativeLayout.LayoutParams layoutParam2 = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,(int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,1,res.getDisplayMetrics())));
imageView2.setLayoutParams(layoutParam2);
relativeLayout0.addView(imageView2);
imageView2.setId(R.id.ccc);
layoutParam2.addRule(RelativeLayout.BELOW,R.id.head);
return relativeLayout0;
}
}
使用时如下所示,使用X2C.setContentView替代原始的setContentView即可
// this.setContentView(R.layout.activity_main);
X2C.setContentView(this, R.layout.activity_main);
X2C优点
1.在保留xml的同时,又解决了它带来的性能问题
2.据X2C统计,加载耗时可以缩小到原来的1/3
X2C问题
1.部分属性不能通过代码设置,Java不兼容
2.将加载时间转移到了编译期,增加了编译期耗时
3.不支持kotlin-android-extensions插件(已被废弃),牺牲了部分易用性
Compose方案
Compose 是 Jetpack 中的一个新成员,Compose使用纯kotlin开发,使用简洁方便,Compose是未来android UI开发的方向。可以跳转下面链接自行学习。后续会更新Compose的文章哦
其实我们可以通过时间的计算发现。对于我们的项目,布局加载耗时并不是主要耗时的地方,优化收益不大。而一些常规的优化更适合我们的项目。
一些常规优化手段
上面介绍了一些改动比较大的方案,其实我们在实际开发中也有些常规的方法可以优化布局加载
比如优化布局层级,避免过度绘制等,这些简单的手段可能正是可以应用到项目中的
优化布局层级及复杂度
1、使用ConstraintLayout,可以实现完全扁平化的布局,减少层级
2、RelativeLayout本身尽量不要嵌套使用,能用LinearLayout就用LinearLayout,因为RelativeLayout需要测量上下、左右两个方向,而LinearLayout只测量设置的方向
3、嵌套的LinearLayout中,尽量不要使用weight,因为weight会重新测量两次
4、推荐使用merge标签,可以减少一个层级
5、使用ViewStub延迟加载
6、多使用include提高复用率减少测量绘制时间
merge
使用merge标签来优化布局可以提高布局文件的可重用性和简洁性。merge标签在布局文件中可以用来替代根视图,可以使布局文件更加简洁明了,同时也可以减少内存使用。
以下是使用merge标签来优化布局的步骤:
1、在布局文件中使用merge标记作为根视图,例如:
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 布局内容 -->
<Button android:text="Button" />
<TextView android:text="Hello World!" />
</merge>
2、在需要使用该布局时,通过include标记将该布局文件包含到其它布局文件中,例如:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/your_layout"/>
</LinearLayout>
这样,使用merge标签来优化布局可以使布局文件更加简洁,易于重用,同时也可以减少内存使用。
ViewStub
使用ViewStub来优化布局可以提高应用程序性能,节省布局文件的内存和CPU开销。
以下是使用ViewStub来优化布局的步骤:
在布局文件中添加ViewStub标记,并将它们设置为invisible或gone。比如:
<ViewStub
android:id="@+id/view_stub"
android:layout="@layout/your_layout"
android:visibility="gone" />
在需要加载布局的时候,通过ViewStub的inflate()方法将其展开为实际布局视图:
ViewStub viewStub = findViewById(R.id.view_stub);
View inflatedView = viewStub.inflate();
这样,布局文件就不会在应用程序启动时被加载,只有在需要使用时才会被加载,从而提高应用程序性能。
避免过度绘制
1.去掉多余背景色,减少复杂shape的使用
2.避免层级叠加
3.自定义View使用clipRect屏蔽被遮盖View绘制