Android的绘制优化其实可以分为两个部分,即布局(UI)优化和卡顿优化,而布局优化的核心问题就是要解决因布局渲染性能不佳而导致应用卡顿的问题,所以它可以认为是卡顿优化的一个子集。
本文主要包括以下内容
1.为什么要进行布局优化及android绘制,布局加载原理
2.获取布局文件加载耗时的方法
3.介绍一些布局优化的手段与方法
4.为什么放弃使用这些优化方法?
为什么要进行布局优化?
答案是显而易见的,如果布局嵌套过深,或者其他原因导致布局渲染性能不佳,可能会导致应用卡顿
那么布局到底是如何导致渲染性能不佳的呢?首先我们应该了解下android
绘制原理与布局加载原理
android
绘制原理Android
的屏幕刷新中涉及到最重要的三个概念(为便于理解,这里先做简单介绍)
CPU
:执行应用层的measure
、layout
、draw
等操作,绘制完成后将数据提交给GPU
GPU
:进一步处理数据,并将数据缓存起来总结一句话就是: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
中主要有两个耗时操作
xml
,获取XmlResourceParser
,这是IO过程createViewFromTag
,创建View
对象,用到了反射以上两点就是布局加载可能导致卡顿的原因,也是布局的性能瓶颈
我们如果需要优化布局卡顿问题,首先最重要的就是:确定定量标准
所以我们首先介绍几种获取布局文件加载耗时的方法
首先介绍一下常规方法
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
之前调用
这样就可以比较方便地实现监听每个控件的加载耗时
布局加载慢的主要原因有两个,一个是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文件:
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 {
override fun createView(ui: AnkoContext) = with(ui) {
verticalLayout {
val name = editText()
button("Say Hello") {
onClick { ctx.toast("Hello, ${name.text}!") }
}
}
}
}
如上所示,Anko
使用kotlin DSL
实现布局,它比我们使用Java
动态创建布局方便很多,主要是更简洁,它和拥有xml
创建布局的层级关系,能让我们更容易阅读
同时,它去除了IO
与反射过程,性能更好,以下是Anko
与XML
的性能对比
不过由于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
开发的方向,读者可以自行查阅相关资料
上面介绍了不少布局加载优化方法,而我最后在项目中最后都没有使用,这就是从真从入门到放弃
总得来说有以下几个原因
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
我在android
5.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.介绍了因为在我们在项目中布局加载耗时优化收益不大,所以没有引入上述优化手段
最后分享给大家一份阿里P8大佬熬夜30天整理的《Android 360°全方面性能调优》,涵盖设计思想与代码质量优化、程序性能优化、开发效率优化、其中详细讲解了启动优化、布局优化、内存优化、屏幕适配、OOM问题等方面,并且他还将自己工作中的一些项目案例在PDF中做了实践分享,整合成了一套系统的性能优化知识笔记PDF,长达721页,几乎都可以出本书了。
1.六大原则.
2.设计模式
3.数据结构
4.算法
1.启动速度与执行效率优化
2.布局检测与优化
3.内存优化
4.耗电优化
5.网络传输与数据储存优化
6.APK大小优化
7.屏幕适配
8.OOM问题原理解析
9.ANR问题解析
10.Crash监控方案
1.分布式版本控制系统Git
2.自动化构建系统Gradle
1.启动速度
2.流畅度
3.抖音在APK包大小资源优化的实践
4.优酷响应式布局技术全解析
5.网络优化
6.手机淘宝双十一性能优化项目揭秘
7.高德APP全链路源码依赖分析
8.彻底干掉OOM的实战经验分享
9.微信Android终端内存优化实践