每个做 UI 的 Android 开发,上辈子都是折翼的天使。

本文主要记载

  • 1 UI 优化究竟指的是什么呢?
  • 2 屏幕适配???
  • 3 如何测量、监控 UI 需要优化?
    • 3.1 过度绘制
    • 3.2 Profile GPU Rendering
    • 3.3 测量 setContentView 具体耗时
      • 3.3.1 AOP
      • 3.3.2 Factory2
    • 3.4 问题定位 systrace
  • 4 UI 优化常用手段
    • 4.1 尽量使用硬件加速
    • 4.2 Create View 优化
      • 4.2.1 使用代码创建
      • 4.2.2 异步创建
    • 4.3. measure/layout 优化
    • 4.4 其他
      • 4.4.1 Litho:异步布局
      • 4.4.2 Flutter:自己的布局 + 渲染引擎
      • 4.4.3 RenderThread 与 RenderScript
  • 5 总结

1 UI 优化究竟指的是什么呢?

我认为所谓的 UI 优化,应该包含两个方面

  1. 一个是效率的提升,我们可以非常高效地把 UI 的设计图转化成应用界面,并且保证 UI 界面在不同尺寸和分辨率的手机上都是一致的
  2. 另一个是性能的提升,在正确实现复杂、炫酷的 UI 设计的同时,需要保证用户有流畅的体验。

2 屏幕适配???

Android 的碎片化问题令人痛心疾首,屏幕的差异正是碎片化问题的 “中心” ,多年来,有那么一群苦逼的 Android 开发,他们饱受碎片化之苦,面对着各式各样的手机屏幕尺寸和分辨率,还要与“凶残”的产品和 UI 设计师过招,日复一日、年复一年的做着 UI 适配和优化工作,蹉跎着青春的岁月。更加不幸的是,最近两年这个趋势似乎还愈演愈烈:刘海屏、全面屏,还有即将推出的柔性折叠屏,UI 适配将变得越来越复杂。

对于屏幕碎片化的问题,Android 推荐使用 dp 作为尺寸单位来适配 UI,因此每个 Android 开发都应该很清楚 pxdpdpippidensity 这些概念。

通过 dp 加上自适应布局可以基本解决屏幕碎片化的问题。

  • 不一致性。因为 dpi 与实际 ppi 的差异性,导致在相同分辨率的手机上,控件的实际大小会有所不同。
  • 效率。设计师的设计稿都是以 px 为单位的,开发人员为了 UI 适配,需要手动通过百分比估算出 dp 值。

除了直接 dp 适配之外,目前业界比较常用的 UI 适配方法主要有下面几种:

限制符适配方案。主要有宽高限定符与 smallestWidth 限定符适配方案,具体可以参考《Android 目前稳定高效的 UI 适配方案》《smallestWidth 限定符适配方案》。今日头条适配方案。通过反射修正系统的 density 值,具体可以参考《一种极低成本的 Android 屏幕适配方式》《今日头条适配方案》


3 如何测量、监控 UI 需要优化?

3.1 过度绘制

理论上一个像素每次只绘制一次是最优的,但是由于重叠的布局导致一些像素会被多次绘制,Overdraw由此产生。

我们可以通过调试工具来检测 Overdraw设置——>开发者选项——>调试 GPU 过度绘制——>显示过度绘制区域

原色 – 没有过度绘制 – 这部分的像素点只在屏幕上绘制了一次。
蓝色 – 1次过度绘制 – 这部分的像素点只在屏幕上绘制了两次。
绿色 – 2次过度绘制 – 这部分的像素点只在屏幕上绘制了三次。
粉色 – 3次过度绘制 – 这部分的像素点只在屏幕上绘制了四次。
红色 – 4次过度绘制 – 这部分的像素点只在屏幕上绘制了五次。

在实际项目中,一般认为蓝色即是可以接受的颜色。

具体优化点:

  1. 各种控件背景颜色
  2. ActivityTheme 中的背景颜色

备注:有些过度绘制都是不可避免的,需要结合具体的布局场景具体分析。

3.2 Profile GPU Rendering

设置——>开发者选项——>GPU 呈现模式分析——>条形图

如果我们把上面的步骤转化线程模型,可以得到下面的流水线模型。CPU 将数据同步(sync)给 GPU 之后,一般不会阻塞等待GPU 渲染完毕,而是通知结束后就返回。而 RenderThread 承担了比较多的绘制工作,分担了主线程很多压力,提高了 UI 线程的响应速度。

3.3 测量 setContentView 具体耗时

3.3.1 AOP

我们可以通过 AOP 将代码切入到 android.app.Activity.setContentView(..) 中。

1
2
3
4
5
6
7
8
9
10
11
12
@Around("execution(* android.app.Activity.setContentView(..))") //方法切入点
public void methodAnnotated(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
LogUtils.i(name + " cost " + (System.currentTimeMillis() - time));
}

3.3.2 Factory2

View 的绘制源码中,在生成我们 xml 所对应的 View 的时候有一个 mFactory2 的判断,我们可以在创建的时候做一些事情。

作用:

  1. 定制生成控件的过程,全局替换,比如将 TextView 替换为 CustomTextView
  2. 监控控件的生成时间,自己调用系统创建方法并且在方法前后记录时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (name.equals("TextView")) {
//创建 CustomTextView
}
long startTime = System.currentTimeMillis();
View view = getDelegate().createView(parent,name,context,attrs);
LogUtils.i(name + " cost " + (System.currentTimeMillis() - startTime));
return view;//返回生成后的 View
}

@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});

3.4 问题定位 systrace

Android 4.1,新增了 Systrace 性能数据采样和分析工具。在启动优化中,已经用过 Systrace 了,这里也可以用它来检测每一帧的渲染情况。

结合 Android 内核的数据,生成 HTML 报告,API 18 以上,推荐 TraceCompat

  • 首先清空后台💦

杀掉所有应用,防止出现莫名莫名方法。

  • 在需要检测的地方写入代码(可以不写,但是会少一些 tag 提示)💦
1
2
3
TraceCompat.beginSection("AppOnCreate");
...
TraceCompat.endSection();
  • 执行检测💦

首先进入 SDK 目录,Sdk\platform-tools\systrace\ 目录下有一个 systrace.py 文件,打开 cmd 输入

1
python systrace.py -b 32768 -t 5 -a <packagename> -o test.log.html sched gfx view wm am

这种方式是 5 秒后自动输出

1
python systrace.py gfx view wm am pm ss dalvik sched -b 32768 -a <packagename> -o test.log.html

这种方式可以在自己收集完后,点击 Enter 键停止收集

两种内容是不太一样的, -t 表示时间,-a 表示包名,-o 输出文件名,最终在当前目录打开文件即可看到,文件只能使用 Chrome 来打开,如果打开 HTML 出现

Unable to select a master clock domain because no path can be found from "SYSTRACE" to "LINUX_FTRACE_GLOBAL".

那就是命令出错了,命令我也是收集了好久,最终找到可用的,想了解更多的请查看 Gityuan官方文档

  • 分析文件

在每个 app 进程,都有一个 Frames 行,正常情况以绿色的圆点表示。当圆点颜色为黄色或者红色时,意味着这一帧超过 16.6ms (即发现丢帧),这时需要通过放大那一帧进一步分析问题。对于 Android 5.0(API level 21) 或者更高的设备,该问题主要聚焦在 UI ThreadRender Thread 这两个线程当中。对于更早的版本,则所有工作在 UI Thread

想要了解更多,全力推荐 Android Systrace

  • Alerts

Systrace 能自动分析 trace 中的事件,并能自动高亮性能问题作为一个 Alerts ,建议调试人员下一步该怎么做。

比如对于丢帧是,点击黄色或红色的 Frames 圆点便会在下方有相关的提示信息;另外,在 systrace 的最右上方,有一个 Alerts tab 可以展开,这里记录着所有的的警告提示信息。

总的来说,UI 渲染的优化必然会朝着两个方向。一个是进一步压榨硬件的性能,让 UI 可以更加流畅。一个是改进或者增加更多的分析工具,帮助我们更容易地发现以及定位问题。


4 UI 优化常用手段

4.1 尽量使用硬件加速

硬件加速绘制的性能是远远高于软件绘制的。所以说 UI 优化的第一个手段就是保证渲染尽量使用硬件加速。

有哪些情况我们不能使用硬件加速呢?之所以不能使用硬件加速,是因为硬件加速不能支持所有的 Canvas API。如果使用了不支持的 API,系统就需要通过 CPU 软件模拟绘制,这也是渐变、磨砂、圆角等效果渲染性能比较低的原因。

SVG 也是一个非常典型的例子,SVG 有很多指令硬件加速都不支持。但我们可以用一个取巧的方法,提前将这些 SVG 转换成 Bitmap 缓存起来,这样系统就可以更好地使用硬件加速绘制。

4.2 Create View 优化

View 的创建是在 UI 线程里,对于一些非常复杂的界面,这部分的耗时不容忽视。

4.2.1 使用代码创建

使用 XML 进行 UI 编写可以说是十分方便,可以在 Android Studio 中实时预览到界面。如果我们要对一个界面进行极致优化,就可以使用代码进行编写界面。

但是这种方式对开发效率来说简直是灾难,因此我们可以使用一些开源的 XML 转换为 Java 代码的工具,例如 X2C 。但坦白说,还是有不少情况是不支持直接转换的,有时候需要修改源码。

所以我们需要兼容性能与开发效率,我建议只在对性能要求非常高,但修改又不非常频繁的场景才使用这个方式。

4.2.2 异步创建

之前在 启动优化 中提到的参考文章 Android - 一种新奇的冷启动速度优化思路(Fragment极度懒加载 + Layout子线程预加载)

遇到个点,如果 xml 中是 com.google.android.material.appbar.AppBarLayoutgoogle 包里的,需要加上一条属性

1
android:theme="@style/Theme.AppCompat.Light.DarkActionBar"

这个属性与自己 Activitytheme 相对应。

即刻时间中还提到一个错误 :java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare() at android.os.Handler.(Handler.java:121)

这个问题在异步初始化 WebView 是有出现过。

解决办法:通过又一个非常取巧的方式来实现。在使用线程创建 UI 的时候,先把线程的 Looper 的 MessageQueue 替换成 UI 线程 Looper 的 Queue。

不过需要注意的是,在创建完 View 后我们需要把线程的 Looper 恢复成原来的。

x5WebView 中异步调用会报错,还在研究中。。。

4.3. measure/layout 优化

渲染流程中 measurelayout 也是需要 CPU 在主线程执行的,对于这块内容网上有很多优化的文章,一般的常规方法有:

  • 减少 UI 布局层次。例如尽量扁平化,使用 等优化。
  • 优化 layout 的开销。尽量不使用 RelativeLayout 或者基于 weighted LinearLayout,它们 layout 的开销非常巨大。这里我推荐使用 ConstraintLayout 替代 RelativeLayout 或者 weighted LinearLayout
  • 背景优化。尽量不要重复去设置背景,这里需要注意的是主题背景(theme), theme 默认会是一个纯色背景,如果我们自定义了界面的背景,那么主题的背景我们来说是无用的。但是由于主题背景是设置在 DecorView 中,所以这里会带来重复绘制,也会带来绘制性能损耗。

对于 measurelayout,我们可以像 Create View 一样实现异步创建,这样可以大大地提升首次显示的性能。

Textview 是系统控件中非常强大也非常重要的一个控件,强大的背后就代表着需要做很多计算。在 2018 年的 Google I/O 大会,发布了 PrecomputedText 并已经集成在 Jetpack 中,它给我们提供了接口,可以异步进行 measurelayout,不必在主线程中执行。

PrecomputedText

xml 布局中使用 AppCompatTextView

1
<androidx.appcompat.widget.AppCompatTextView

赋值的时候

1
2
3
4
Future<PrecomputedTextCompat> future = PrecomputedTextCompat.getTextFuture(
"content xxx", mTextView.getTextMetricsParamsCompat(), null);

mTextView.setTextFuture(future);

如果使用 kotlin ,那么利用拓展方法会更加酸爽。

1
2
3
4
5
6
7
8
9
10
fun AppCompatTextView.setTextFuture(charSequence: CharSequence){
this.setTextFuture(PrecomputedTextCompat.getTextFuture(
charSequence,
TextViewCompat.getTextMetricsParams(this),
null
))
}

// 调用
textView.setTextFuture(“text”)

4.4 其他

4.4.1 Litho:异步布局

LithoFacebook 开源的声明式 Android UI 渲染框架,它是基于另外一个 Facebook 开源的布局引擎 Yoga 开发的。

Litho 如何优化的

  1. 异步布局

    一般来说的 Android 所有的控件绘制都要遵守 measure -> layout -> draw 的流水线,并且这些都发生在主线程中。

Litho 如我前面提到的 PrecomputedText 一样,把 measurelayout 都放到了后台线程,只留下了必须要在主线程完成的 draw,这大大降低了 UI 线程的负载。它的渲染流水线如下:

  1. 界面扁平化

    前面也提到过,降低 UI 的层级是一个非常通用的优化方法。Litho 就给了我们一种方案,由于 Litho 使用了自有的布局引擎(Yoga),在布局阶段就可以检测不必要的层级、减少 ViewGroups,来实现 UI 扁平化。

  2. 优化 RecyclerView

    Litho 还优化了 RecyclerViewUI组件的缓存和回收方法。原生的 RecyclerView 或者 ListView 是按照 viewType 来进行缓存和回收,但如果一个RecyclerView/ListView 中出现 viewType 过多,会使缓存形同虚设。但 Litho 是按照 textimagevideo 独立回收的,这可以提高缓存命中率、降低内存使用率、提高滚动帧率。

    Litho 虽然强大,但也有自己的缺点。它为了实现 measure/layout 异步化,使用了类似 react 单向数据流设计,这一定程度上加大了 UI 开发的复杂性。并且 LithoUI代码是使用 Java/Kotlin 来进行编写,无法做到在 AS 中预览。如果你没有计划完全迁移到 Litho,我建议可以优先使用 Litho 中的 RecyclerCollectionComponentSections 来优化自己的 RecyelerView 的性能。

4.4.2 Flutter:自己的布局 + 渲染引擎

如下图所示,Litho 虽然通过使用自己的布局引擎 Yoga,一定程度上突破了系统的一些限制,但是在 draw 之后依然走的系统的渲染机制。

FlutterSkia 引擎直接集成进了 App 中,这使得 Flutter App 就像一个游戏 App。并且直接使用了 Dart 虚拟机,可以说是一套跳脱出 Android 的方案,所以 Flutter 也可以很容易实现跨平台。

《Flutter 原理与实践》

4.4.3 RenderThread 与 RenderScript

Android 5.0,系统增加了 RenderThread,对于 ViewPropertyAnimatorCircularReveal 动画,我们可以使用 RenderThead 实现动画的异步渲染。当主线程阻塞的时候,普通动画会出现明显的丢帧卡顿,而使用 RenderThread 渲染的动画即使阻塞了主线程仍不受影响。

我们可以通过 RenderScript,它是 Android 操作系统上的一套 API。它基于异构计算思想,专门用于密集型计算。RenderScript 提供了三个基本工具:一个硬件无关的通用计算 API;一个类似于 CUDAOpenCLGLSL 的计算 API;一个类 C99 的脚本语言。允许开发者以较少的代码实现功能复杂且性能优越的应用程序。


5 总结

  1. 在系统的框架下优化。布局优化、使用代码创建、View 缓存等都是这个思路,我们希望减少甚至省下渲染流水线里某个阶段的耗时。
  2. 利用系统新的特性。使用硬件加速、RenderThreadRenderScript 都是这个思路,通过系统一些新的特性,最大限度压榨出性能。
  3. 突破系统的限制。由于 Android 系统碎片化非常严重,很多好的特性可能低版本系统并不支持。而且系统需要支持所有的场景,在一些特定场景下它无法实现最优解。这个时候,我们希望可以突破系统的条条框框,例如 Litho 突破了布局,Flutter 则更进一步,把渲染也接管过来了。

对于 UI 优化的另一个思考是效率,目前 Android Studio 对设计并不友好,例如不支持 Sketch 插件和 AE 插件。Lottie 是一个非常好的案例,它很大提升了开发人员写动画的效率。

“设计师和产品,你们长大了,要学会自己写 UI 了”。在未来,我们希望 UI 界面与适配可以实现自动化,或者干脆把它交还给设计师和产品。

感谢

极客时间 Android开发高手课