【从入门到放弃】android布局优化深入解析

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结点前,会调用mFactory2onCreateView方法

这样如果我们重写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” />

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方案

AnkoJetBrains开发的一个强大的库,支持使用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与反射过程,性能更好,以下是AnkoXML的性能对比

不过由于AnKo已经停止维护了,这里不建议大家使用,了解原理即可

AnKo建议大家使用Jetpack Compose来替代使用

Compose方案

ComposeJetpack 中的一个新成员,是 Android 团队在2019年I/O大会上公布的新的UI库,目前处于Beta阶段

Compose使用纯kotlin开发,使用简洁方便,但它并不是像Anko一样对ViewGroup的封装

Compose 并不是对 ViewViewGroup 这套系统做了个上层包装来让写法更简单,而是完全抛弃了这套系统,自己把整个的渲染机制从里到外做了个全新的。

可以确定的是,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前后时间相减,得到布局加载时间

onWindowFocusChangedActivity真正可见时间,将其与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延迟加载

最后

分享一份NDK基础开发资料

详解:Linux网络虚拟化技术

分享内容包括不限于高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
一些改动比较大的方案,其实我们在实际开发中也有些常规的方法可以优化布局加载

比如优化布局层级,避免过度绘制等,这些简单的手段可能正是可以应用到项目中的

优化布局层级及复杂度

1.使用ConstraintLayout,可以实现完全扁平化的布局,减少层级

2.RelativeLayout本身尽量不要嵌套使用

3.嵌套的LinearLayout中,尽量不要使用weight,因为weight会重新测量两次

4.推荐使用merge标签,可以减少一个层级

5.使用ViewStub延迟加载

最后

分享一份NDK基础开发资料

[外链图片转存中…(img-sREQrwjh-1714977727257)]

分享内容包括不限于高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 16
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值