Android中布局UI的优化总结

Android应用UI性能分析

在使用App时会发现有些界面启动卡顿、动画不流畅、列表等滑动时也会卡顿出现这种情况,可以考虑对UI性能分析。

首先要清楚卡顿的原因,有以下几种情况:

1. 人为在UI线程中做轻微耗时操作,导致UI线程卡顿;

2. 布局Layout过于复杂,无法在16ms内完成渲染;( Android系统每隔16ms会发出VSYNC信号重绘我们的界面(Activity).为什么是16ms, 因为Android设定的刷新率是60FPS(Frame Per Second), 也就是每秒60帧的刷新率, 约合16ms刷新一次.这就意味着, 我们需要在16ms内完成下一次要刷新的界面的相关运算, 以便界面刷新更新. 然而, 如果我们无法在16ms内完成此次运算会怎样呢?例如, 假设我们更新屏幕的背景图片, 需要24ms来做这次运算. 当系统在第一个16ms时刷新界面, 然而我们的运算还没有结束, 无法绘出图片. 当系统隔16ms再发一次VSYNC信息重绘界面时, 用户才会看到更新后的图片. 也就是说用户是32ms后看到了这次刷新(注意, 并不是24ms). 这就是传说中的丢帧(dropped frame),丢帧给用户的感觉就是卡顿, 而且如果运算过于复杂, 丢帧会更多, 导致界面常常处于停滞状态, 卡到爆.)

3. 同一时间动画执行的次数过多,导致CPU或GPU负载过重;

4. View过度绘制,导致某些像素在同一帧时间内被绘制多次,从而使CPU或GPU负载过重;

5. View频繁的触发measure、layout,导致measure、layout累计耗时过多及整个View频繁的重新渲染;

6. 内存频繁触发GC过多(同一帧中频繁创建内存),导致暂时阻塞渲染操作;

7. 冗余资源及逻辑等导致加载和执行缓慢;

8. 臭名昭著的ANR;

如何分析?

分析UI卡顿我们一般都借助工具,通过工具一般都可以直观的分析出问题原因,从而反推寻求优化方案,具体如下细说各种强大的工具

1. 使用HierarchyViewer分析UI性能

我们可以通过SDK提供的工具HierarchyViewer来进行UI布局复杂程度及冗余等分析
通过命令启动HierarchyViewer

图片.png

接下来Hierarchy window窗口打开:

图片.png

一个Activity的View树,通过这个树可以分析出View嵌套的冗余层级,以及每个View在绘制的使用时长也有表示。

2. 使用Lint进行资源及冗余UI布局等优化

冗余资源及逻辑等也可能会导致加载和执行缓慢,这可以使用Link工具,来发现优化这些问题的

在AndroidStudio 1.4版本中使用Lint最简单的办法:就是将鼠标放在代码区点击右键->Analyze->Inspect Code–>界面选择你要检测的模块->点击确认开始检测,等待一下后会发现如下结果:

图片.png

如果存在冗余的UI层级嵌套,会进行高亮显示, 我们根据提示可以点击跳进去进行优化处理掉的。

3. 使用Memory监测及GC打印与Allocation Tracker进行UI卡顿分析

由于Android系统会依据内存中不同的内存数据类型分别执行不同的GC操作,常见应用开发中导致GC频繁执行的原因主要可能是因为短时间内有大量频繁的对象创建与释放操作,也就是俗称的内存抖动现象,或者短时间内已经存在大量内存暂用介于阈值边缘,接着每当有新对象创建时都会导致超越阈值触发GC操作

如何查看?

Android Studio 工具提供内存查看器:

图片.png

根据内存抖动现象,查看log日志进行分析:

图片.png

如何看到,这种不停的大面积打印GC导致所有线程暂停的操作必定会导致UI视觉的卡顿,所以我们要避免此类问题的出现,具体的常见优化方式如下:

  1. 检查代码,尽量避免有些频繁触发的逻辑方法中存在大量对象分配;
  2. 尽量避免在多次for循环中频繁分配对象;
  3. 避免在自定义View的onDraw()方法中执行复杂的操作及创建对象(譬如Paint的实例化操作不要写在onDraw()方法中等);
  4. 对于并发下载等类似逻辑的实现尽量避免多次创建线程对象,而是交给线程池处理。

有了上面说明GC导致的性能后我们就该定位分析问题了,我们可以通过运行DDMS->Allocation Tracker标签打开一个新窗口,然后点击Start Tracing按钮,接着运行你想分析的代码,运行完毕后点击GetAllocations按钮就能够看见一个已分配对象的列表,如下:

图片.png

UI布局优化措施有哪些?

  • 尽可能的减少布局的嵌套层级(Google建议View树的高度不宜超过10层。)
    以前我们用Eclipse写代码时,自动生成的模板是以LinearLayout为根节点的,但是后面变成了RelativeLayout为根节点。
    RelativeLayout可以让视图树的层级少,但是LinearLayout的测量效率要高。
    如果使用RelativeLayout,需要尽量避免嵌套;如果使用LinearLayout,保证层级不能太深。

  • 使用HierarchyViewer工具分析视图树,剔除没有用到的布局元素

  • 不用设置不必要的背景,避免过度绘制,比如父控件设置了背景色,子控件完全将父控件给覆盖的情况下,那么父控件就没必要设置背景了

  • <include>标签
    编写页面布局是可以将通用的UI提取出来,使用的时候使用<include>标签将其引入。但是<include>标签并不能减少布局层级,只能增加代码可读性。

  • <merge>标签
    问:使用merge标签有哪些优点?
    答:<merge/>标签可以用于减少View树的层次来优化Android的布局,使用<include>包含布局的时候,系统会自动忽略merge层级。

    问:merge标签有什么缺点么?
    答:1. <merge/>只能作为XML布局的根标签使用。
    2.当Inflate以<merge/>开头的布局文件时,必须指定一个父ViewGroup,并且必须设定attachToRoot为true。

    问:什么情况考虑使用Merge标签?
    答:1.子视图不需要指定任何针对父视图的布局属性,例子中TextView仅仅需要直接添加到父视图上用于显示就行。
    2.假如需要在LinearLayout里面嵌入一个布局(或者视图),而恰恰这个布局(或者视图)的根节点也是LinearLayout,这样就多了一层没有用的嵌套,无疑这样只会拖慢程序速度。而这个时候如果我们使用merge根标签就可以避免那样的问题。

  • <ViewStub>标签
    问:使用ViewStub标签有哪些优点?
    答:<ViewStub>标签可以实现对一个view进行延迟加载,是一个轻量级的布局,它声明在xml布局中,在Activity加载布局时,被它包裹的View看不见也不占布局位置,最重要的是:它不会实例化里边的View,也不会产生绘制的消耗,直到它被主动inflate.

    问:使用ViewStub有啥需要注意的吗?
    答:1. 在要渲染的布局中并不支持<merge/>标签。
    2.ViewStub.infalte方法不能调用两次,否则会出现异常。(因为一旦ViewStub visible/inflated,则ViewStub将从视图框架中移除,其id也会失效)

    问:ViewStub标签和View.GONE的区别?
    答:ViewStub标签只有在显示时才去渲染整个布局,但是View.GONE在初始化布局时就已经添加在布局树上了。

<ViewStub>用法:

((ViewStub)findViewById(R.id.viewstub)).setVisibility(View.VISIBLE);
 // or
View importPanel = ((ViewStub) findViewById(R.id.viewstub)).inflate();

Android性能优化之App应用启动分析与优化

App启动方式
通常来说, 一个App启动也会分如下两种不同的状态:

  • 1)冷启动
    当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用,这个启动方式就是冷启动。冷启动因为系统会重新创建一个新的进程分配给它,所以会先创建和初始化Application类,再创建和初始化MainActivity类(包括一系列的测量、布局、绘制),最后显示在界面上。

  • 2.)热启动
    当启动应用时,后台已有该应用的进程(例:按back键、home键,应用虽然会退出,但是该应用的进程是依然会保留在后台,可进入任务列表查看),所以在已有进程的情况下,这种启动会从已有的进程中来启动应用,这个方式叫热启动。热启动因为会从已有的进程中来启动,所以热启动就不会走Application这步了,而是直接走MainActivity(包括一系列的测量、布局、绘制),所以热启动的过程只需要创建和初始化一个MainActivity就行了,而不必创建和初始化Application,因为一个应用从新进程的创建到进程的销毁,Application只会初始化一次。

启动时间的测量
关于Activity启动时间的定义
对于Activity来说,启动时,首先执行的是onCreate()、onStart()、onResume()这些生命周期函数,但即使这些生命周期方法回调结束了,应用也不算已经完全启动,还需要等View树全部构建完毕,一般认为,setContentView中的View全部显示结束了,算作是应用完全启动了。

那么这个时间,实际上是Activity启动,到Layout全部显示的过程,但是要注意,这里并不包括数据的加载,因为很多App在加载时会使用懒加载模式,即数据拉取后,再刷新默认的UI。

基于上面的启动流程我们尽量做到如下几点

    1. Application的创建过程中尽量少的进行耗时操作
    1. 首屏Activity的渲染

关于Application

Application是程序的主入口,特别是很多第三方SDK都会需要在Application的onCreate里面做很多初始化操作,一般来说我们可以将这些初始化放在一个单独的线程中处理, 为了方便今后管理,
优化的方法,无非是通过以下几个方面:

  • 1) 异步初始化
    这个很简单,就是让App在onCreate里面尽可能的少做事情,而利用手机的多核特性,尽可能的利用多线程,例如一些第三方框架的初始化,如果能放线程,就尽量的放入线程中,最简单的,你可以直接new Thread(),当然,你也可以通过公共的线程池来进行异步的初始化工作,这个是最能够压缩启动时间的方式

  • 2) 后台任务
    使用IntentService不同于Service, 它是工作在后台线程的.

  • 3) 界面预加载
    当系统加载一个Activity的时候,onCreate()是一个耗时过程,那么在这个过程中,系统为了让用户能有一个比较好的体验,实际上会先绘制一些初始界面,类似于PlaceHolder。
    系统首先会读取当前Activity的Theme,然后根据Theme中的配置来绘制,当Activity加载完毕后,才会替换为真正的界面。所以,Google官方提供的解决方案,就是通过android:windowBackground属性,来进行加载前的配置,同时,这里不仅可以配置颜色,还能配置图片.

自定义View如何优化内存?

  • 不建议在draw或者layout的过程中去实例化对象

推荐阅读更多精彩内容