共计 8837 个字符,预计需要花费 23 分钟才能阅读完成。
前言
Android 的绘制优化其实能够分为两个局部,即布局 (UI) 优化和卡顿优化,而布局优化的外围问题就是要解决因布局渲染性能不佳而导致利用卡顿的问题,所以它能够认为是卡顿优化的一个子集。
本文次要包含以下内容:
1. 为什么要进行布局优化及 android 绘制,布局加载原理。
2. 获取布局文件加载耗时的办法。
3. 介绍一些布局优化的伎俩与办法。
4. 为什么放弃应用这些优化办法?
1为什么要进行布局优化?
为什么要进行布局优化?
答案是不言而喻的,如果布局嵌套过深,或者其余起因导致布局渲染性能不佳,可能会导致利用卡顿。
那么布局到底是如何导致渲染性能不佳的呢?首先咱们应该理解下 android 绘制原理与布局加载原理。
android 绘制原理
Android 的屏幕刷新中波及到最重要的三个概念(为便于了解,这里先做简略介绍)。
1、CPU:执行应用层的 measure、layout、draw 等操作,绘制实现后将数据提交给 GPU。
2、GPU:进一步解决数据,并将数据缓存起来。
3、屏幕:由一个个像素点组成,以固定的频率(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 来实现,咱们就不在这里贴源码了,一起来看看它的时序图。
咱们能够看到,在 setContentView 中次要有两个耗时操作:
1. 解析 xml, 获取 XmlResourceParser, 这是 IO 过程。
2. 通过 createViewFromTag, 创立 View 对象,用到了反射。
以上两点就是布局加载可能导致卡顿的起因,也是布局的性能瓶颈。
2获取布局文件加载耗时的办法
咱们如果须要优化布局卡顿问题,首先最重要的就是:确定定量规范。
所以咱们首先介绍几种获取布局文件加载耗时的办法。
惯例获取
首先介绍一下惯例办法:
val start = System.currentTimeMillis()
setContentView(R.layout.activity_layout_optimize)
val inflateTime = System.currentTimeMillis() - start
这种办法很简略,因为 setContentView 是同步办法,如果想要计算耗时,间接将前后工夫计算相减即可失去后果了。
AOP(Aspectj,ASM)
下面的形式尽管简略,然而却不够优雅,同时代码有侵入性,如果要对所有 Activity 测量时,就须要在基类中复写相干办法了,比拟麻烦了。
上面介绍一种 AOP 的形式计算耗时。
@Around("execution(* android.app.Activity.setContentView(..))")
public void getSetContentViewTime(ProceedingJoinPoint joinPoint) {Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try {joinPoint.proceed();
} catch (Throwable throwable) {throwable.printStackTrace();
}
Log.i("aop inflate",name + "cost" + (System.currentTimeMillis() - time));
}
下面用的 Aspectj,比较简单,下面的注解的意思是在 setContentView 办法执行外部去调用咱们写好的 getSetContentViewTime 办法。
这样就能够获取相应的耗时。
咱们能够看下打印的日志:
I/aop inflate: AppCompatActivity.setContentView(..) cost 69
I/aop inflate: AppCompatActivity.setContentView(..) cost 25
这样就能够实现无侵入的监控每个页面布局加载的耗时。
具体源码可见文末。
获取任一控件耗时
有时为了更准确的晓得到底是哪个控件加载耗时,比方咱们新增加了自定义 View, 须要监控它的性能。
咱们能够利用 setFactory2 来监听每个控件的加载耗时。
首先咱们来回顾下 setContentView 办法:
public final View tryCreateView(@Nullable View parent, @NonNull String name,
...
View view;
if (mFactory2 != null) {view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {view = mFactory.onCreateView(name, context, attrs);
} else {view = null;}
...
return view;
}
在真正进行反射实例化 xml 结点前,会调用 mFactory2 的 onCreateView 办法。
这样如果咱们重写 onCreateView 办法,在其前后加上耗时统计,即可获取每个控件的加载耗时。
private fun initItemInflateListener(){
LayoutInflaterCompat.setFactory2(layoutInflater, object : Factory2 {
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet
): View? {val time = System.currentTimeMillis()
val view = delegate.createView(parent, name, context, attrs)
Log.i("inflate Item",name + "cost" + (System.currentTimeMillis() - time))
return view
}
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {return null}
})
}
如上所示:真正的创立 View 的办法,依然是调用 delegate.createView, 咱们只是其之前与之后做了埋点。
留神,initItemInflateListener 须要在 onCreate 之前调用。
这样就能够比拟不便地实现监听每个控件的加载耗时。
3布局加载优化的一些办法介绍
布局加载慢的次要起因有两个, 一个是 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 插件,就义了局部易用性。
Anko 计划
Anko 是 JetBrains 开发的一个弱小的库, 反对应用 kotlin DSL 的形式来写 UI, 如下所示:
class MyActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {super.onCreate(savedInstanceState, persistentState)
MyActivityUI().setContentView(this)
}
}
class MyActivityUI : AnkoComponent<MyActivity> {override fun createView(ui: AnkoContext<MyActivity>) = with(ui) {
verticalLayout {val name = editText()
button("Say Hello") {onClick { ctx.toast("Hello, ${name.text}!") }
}
}
}
}
如上所示,Anko 应用 kotlin DSL 实现布局,它比咱们应用 Java 动态创建布局不便很多,次要是更简洁,它和领有 xml 创立布局的层级关系,能让咱们更容易浏览。
同时,它去除了 IO 与反射过程, 性能更好,以下是 Anko 与 XML 的性能比照。
以上数据来源于:https://medium.com/android-ne…
不过因为 AnKo 曾经进行保护了,这里不倡议大家应用,理解原理即可。
AnKo 倡议大家应用 Jetpack Compose 来代替应用。
Compose 计划
Compose 是 Jetpack 中的一个新成员,是 Android 团队在 2019 年 I / O 大会上颁布的新的 UI 库, 目前处于 Beta 阶段。
Compose 应用纯 kotlin 开发,应用简洁不便,但它并不是像 Anko 一样对 ViewGroup 的封装。
Compose 并不是对 View 和 ViewGroup 这套零碎做了个下层包装来让写法更简略,而是齐全摈弃了这套零碎,本人把整个的渲染机制从里到外做了个全新的。
能够确定的是,Compose 是取代 XML 的官网计划。
Compose 的次要长处就在于它的简略好用,具体来说就是两点:
1. 它的申明式 UI。
2. 去掉了 xml,只应用 Kotlin 一种语言。
因为本文并不是介绍 Compose 的,所以就不持续介绍 Compose 了,总得来说,Compose 是将来 android UI 开发的方向, 读者能够自行查阅相干材料。
4为什么放弃应用这些优化办法?
下面介绍了不少布局加载优化办法,而我最初在我的项目中最初都没有应用,这就是从真从入门到放弃。
总得来说有以下几个起因:
1. 有些形式 (如 AsyncLayoutInflater,X2C) 就义了易用性,尽管性能晋升了,然而开发变得麻烦了。
2.Anko 应用上比拟不便同时性能较高,然而比起 XML 形式改变很大,同时 Anko 曾经放弃保护了,在团队中推动难度大。
3.Compose 是将来 android UI 开发的方向,但目前仍处于 Beta 阶段,置信在 Release 后,会成为咱们替换 XML 的无效伎俩。
4. 还有最次要的一点是,针对咱们的我的项目,布局加载耗时并不是次要耗时的中央,所以优化收益不大,能够将精力投入到其余中央。
如下所示,咱们将 setConteView 前后工夫相减,失去布局加载工夫。
而 onWindowFocusChanged 是 Activity 真正可见工夫,将其与 onCreate 工夫相减,可得页面显示工夫。
在咱们的我的项目中测试成果如下:
android 5.0
I/Log: inflateTime:33
I/Log: activityShowTime:252
I/Log: inflateTime:11
I/Log: activityShowTime:642
I/Log: inflateTime:83
I/Log: activityShowTime:637
android 10.0
I/Log: inflateTime:11
I/Log: activityShowTime:88
I/Log: inflateTime:5
I/Log: activityShowTime:217
I/Log: inflateTime:27
I/Log: activityShowTime:221
我在 android5.0 手机与 10.0 手机上别离做了测试,在咱们的我的项目中布局加载耗时并不很长,同时它们在整个页面可见过程中,占得比例也并不高。
所以得出结论:针对咱们我的项目,布局加载耗时并不是次要耗时的中央,优化收益不大。
这就是从入门到放弃的起因。
一些惯例优化伎俩
下面介绍了一些改变比拟大的计划,其实咱们在理论开发中也有些惯例的办法能够优化布局加载。
比方优化布局层级,防止适度绘制等,这些简略的伎俩可能正是能够利用到我的项目中的。
优化布局层级及复杂度
1. 应用 ConstraintLayout, 能够实现齐全扁平化的布局,缩小层级。
2.RelativeLayout 自身尽量不要嵌套应用。
3. 嵌套的 LinearLayout 中,尽量不要应用 weight,因为 weight 会从新测量两次。
4. 举荐应用 merge 标签,能够缩小一个层级。
5. 应用 ViewStub 提早加载。
防止适度绘制
1. 去掉多余背景色, 缩小简单 shape 的应用。
2. 防止层级叠加。
3. 自定义 View 应用 clipRect 屏蔽被遮蔽 View 绘制。
总结
本文次要介绍了以下内容:
1.andrid 绘制原理与布局加载原理。
2. 如何定量的获取 android 布局加载耗时。
3. 介绍了一些布局加载优化的办法与伎俩(AsyncLayoutInflater,X2C,Anko,Compose 等)。
4. 介绍了因为在咱们在我的项目中布局加载耗时优化收益不大,所以没有引入上述优化伎俩。
Android 高级开发零碎进阶笔记、最新面试温习笔记 PDF,我的 GitHub
文末
您的点赞珍藏就是对我最大的激励!
欢送关注我,分享 Android 干货,交换 Android 技术。
对文章有何见解,或者有何技术问题,欢送在评论区一起留言探讨!