前言

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 69I/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线程的高响应。

应用如下:

@Overrideprotected 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.0I/Log: inflateTime:33I/Log: activityShowTime:252I/Log: inflateTime:11I/Log: activityShowTime:642I/Log: inflateTime:83I/Log: activityShowTime:637android 10.0I/Log: inflateTime:11I/Log: activityShowTime:88I/Log: inflateTime:5I/Log: activityShowTime:217I/Log: inflateTime:27I/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技术。
对文章有何见解,或者有何技术问题,欢送在评论区一起留言探讨!