最近越来越无法忍受我的应用KeepassA的转场动画卡顿问题,因此查看了官方的指南,看到有系统跟踪概览这东西,一番研究,总算是搞明白如何使用这东西分析UI卡顿。

image-20210318154813725

一、抓取报告

进入cpu检测模块,录制一段时间的cpu事件,便可以抓取报告

image-20210310160446627

二、处理卡顿

点击主线程

image-20210310160340540

可以看到在main这个线程中每一个方法的调用时间。

image-20210318154813725

在这里可以看到,inflate 布局和 inflate menu 会消耗大量的时间。

2.1 加载布局优化

一般大家在写页面时都是通过xml写布局,通过setContentView、或LayoutInflater.from(context).inflate方法将xml布局加载到内存中。

但是会有一些缺点:

  • 读取xml很耗时
  • 递归解析xml较耗时
  • 反射生成对象的耗时是new的3倍以上

因此可以从以下几个方面进行优化

  • asynclayoutinflater来进行异步加载

    runtimeOnly group: 'androidx.asynclayoutinflater', name: 'asynclayoutinflater', version: '1.0.0'

    使用方式

    new AsyncLayoutInflate(this).inflate(setLayoutId(), null,
    (view, resid, parent) -> {
    // 做一些操作
    }
    });

    缺点:使用这种方式,会导致转场动画无效

  • 使用[X2C](iReaderAndroid/X2C: Increase layout loading speed 200% (github.com))框架进行优化

  • 减少布局层次

  • 将非必要的复杂布局放在IdelHandler中

  • 使用ViewStub延迟加载一些非必须的布局

2.2 合理使用IdelHandler

继续查看报告

image-20210311084738653

发现onResume中,看到Activity和Fragment中都有一个耗时14ms的方法,检查该方法,发现其是一个启动定时器的保存一个字符串到SharedPreferences的功能。

override fun onResume() {
super.onResume()
// 启动定时器
if (KeepassAUtil.instance.isStartQuickLockActivity(this)) {
if (BaseApp.isLocked) {
AutoLockDbUtil.get()
.startLockWorkerNow()
return
}
if (KeepassAUtil.instance.isRunningForeground(this)) {
AutoLockDbUtil.get()
.resetTimer()
return
}
}
}

对于这个,有两个方案可以参考:

1、将该方法放在线程中执行

2、如果必须要在主线程中执行,可以将该方法放在IdelHandler中执行,顾名思义IdelHandler是一个消息队列中消息执行完成才会执行的Handler,在Android中,Activity是在ActivityThread中的Handler创建和启动的,当主线程中的所有消息都执行完成后,也就意味着Activity已经完全启动。因此可以考虑将一些必要的操作放在这里面执行,以便加快界面进入的速度。

img

由于我的Activity中有两个Fragment,这样一来启动的时候,将能减少15 * 3的时间消耗,相对于3帧时间。

2.3 滚动列表优化

有些场景下,我们可能不能避免的需要大量inflateLayout布局,比如Recyclerview中,由于需要大量inflate 子布局,因此会消耗很多主线程的时间。

image-20210318090257734

为了能保证进入页面的流畅性,可以考虑先加载少部分必要的View,然后同时启动等待动画和Recyclerview的加载流程,以达到视觉上流畅的效果

在infalte布局的过程中,解析xml 很耗时,因此itemView需要尽可能的减少布局层次。

官方Recyclerview优化建议

优化后的效果

感觉效果比之前好了不少

android_optimization_end

参考文档

渲染速度缓慢

系统跟踪概览