Android客户端性能优化实践

字数 5965阅读 834

一、关于App性能优化

1. 性能优化分类

Google官方给出的性能优化教程,主要分为以下几类:
1)布局与UI渲染优化
2)运算与内存管理优化
3)电池电量优化
4)高效代码相关Tips
5)多线程操作

官方链接:https://developer.android.com/training/best-performance.html
中文博客:https://aeli.gitbooks.io/android-training-course/content/best-performance.html

2. 渲染性能与UI卡顿

大多数用户感知到的卡顿等性能问题的最主要根源都是因为渲染性能,Android系统很有可能无法及时完成那些复杂的界面渲染操作,Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着程序的大多数操作都必须在16ms内完成。

图1-1 Android界面渲染

如果你的某个操作花费时间是24ms,系统在得到VSYNC信号的时候就无法进行正常渲染,这样就发生了丢帧现象。那么用户在32ms内看到的会是同一帧画面。

图1-2 Android渲染丢帧

用户容易在UI执行动画或者滑动ListView的时候感知到卡顿不流畅,是因为这里的操作相对复杂,容易发生丢帧的现象,从而感觉卡顿。有很多原因可以导致丢帧,也许是因为你的layout太过复杂,无法在16ms内完成渲染,有可能是因为你的UI上有层叠太多的绘制单元,还有可能是因为动画执行的次数过多。这些都会导致CPU或者GPU负载过重。

二、工具篇

我们可以通过一些工具来定位问题,比如可以使用HierarchyViewer来查找Activity中的布局是否过于复杂,也可以使用手机设置里面的开发者选项,打开Show GPU Overdraw等选项进行观察。你还可以使用TraceView来观察CPU的执行情况,更加快捷的找到性能瓶颈。

1. HierarchyViewer

HierarchyViewer是我们优化程序的工具之一,它是Android自带的非常有用的工具,可以帮助我们更好地检测和设计用户界面,能够以可视化的角度直观地获得UI布局设计结构和各种属性的信息,帮助我们优化布局设计。但是独立的HierarchyViewer已废弃,Android Studio中建议使用Android Device Monitor。
需使用模拟器或开发版的真机,因为需要系统中的Hierarchy Viewer服务开启。
使用方式:
1)点击Tools->Android->Android Device Monitor,进入Android Device Monitor,切换到HierarchyViewer标签。

图2-1 Android HierarchyViewer标签

2)或者直接进入独立的HierarchyViewer:进入sdk/tools路径,找到HierarchyViewer,双击运行。

图2-2 运行HierarchyViewer

3)在左侧树状结构中选中要查看的activity,加载完毕后会显示当前界面的树状结构层次,从PhoneWindow.DecorView出发,右侧是xml内定义的布局结构,大小可以缩放,可以拖动,右侧是属性预览区域。

图2-3 界面树状层次

4)观察单个view,选择单个view后会出现如下所示图形,可以看到Measure、Layout、Draw的耗时,从左到右三个圆点,分别表示对Measure、Layout、Draw耗时性能的评级,按照性能从高到低,分别为绿色、黄色和红色,浮窗上是具体的数值

图2-4 界面控制渲染耗时

通过使用HierarchyViewer,我们可以通过减少页面层级,优化布局结构来实现UI性能的优化,针对每个单独控件的渲染性能数据,可以独立的针对某控件做特殊优化。

2. GPU过度绘制

过度绘制就是Overdraw,是指在一帧的时间内(16.67ms)像素被绘制了多次,理论上一个像素每次只绘制一次是最优的,但是由于重叠的布局导致一些像素会被多次绘制,而每次绘制都会对应到CPU的一组绘图命令和GPU的一些操作,产生额外开销,当这个操作耗时超过16.67ms时,就会出现掉帧现象,也就是我们所说的卡顿,所以应尽量减少Overdraw的发生。
Android提供了测量Overdraw的选项,在开发者选项-调试GPU过度绘制(Show GPU Overdraw),打开选项就可以看到当前页面Overdraw的状态,就可以观察屏幕的绘制状态。该工具会使用三种不同的颜色绘制屏幕,来指示overdraw发生在哪里以及程度如何,其中:

无颜色:没有overdraw。像素只画了一次。
蓝色:overdraw 1倍。像素绘制了两次。大片的蓝色还是可以接受的(若整个窗口是蓝色的,可以摆脱一层)。
绿色:overdraw 2倍。像素绘制了三次。中等大小的绿色区域是可以接受的但你应该尝试优化、减少它们。
浅红:overdraw 3倍。像素绘制了四次,小范围可以接受。
暗红:overdraw 4倍。像素绘制了五次或者更多。这是错误的,要修复它们。

图3-1 Android过度绘制

减少过度绘制Tips:

  • 不同控件容器特点不同,根据不同场景合理选择控件容器,例如RelativeLayout
    相对于LinearLayout,在某些场景下可减少嵌套层次,但LinearLayout使用简单,效率也更高
  • 去掉window的默认背景,DecorView会创建默认背景,而自定义布局会盖上一层背景图或背景色,导致默认背景无用,带来绘制性能损耗
  • 去掉其他不必要的背景,减少背景重叠或覆盖
  • 多使用轻量级的ViewStub(只有设置可见或inflate时才会实例化内部的布局),避免将所有View写上,并动态控制GONE、VISIBLE
  • 使用Merge,减少一层View层级
  • 慎用Alpha,对一个View做Alpha转化,需要先将View绘制出来,再做Alpha转化,最后将转换后效果绘制在界面上,也就是需要对当前View绘制两遍

3. Allocation Tracker

Android系统会依据内存中不同的内存数据类型分别执行不同的GC操作,常见的导致GC频繁执行的原因主要可能是因为短时间内有大量频繁的对象创建与释放操作,也就是俗称的内存抖动现象,或者短时间内已经存在大量内存占用介于阈值边缘,每当有新对象创建时都会导致超越阈值触发GC操作。
Allocation Tracker,内存分配跟踪器,可以记录代码运行过程中的内存分配情况,包括已分配对象的调用栈、大小、代码位置等,可以帮助我们发现在相同的调用栈中,短时间迅速分配与回收的大量相似对象,也可以帮助我们发现代码中可能对内存性能产生不良影响的地方。

1)打开Android Studio中的Android Monitor控制器,进入Memory内存监控窗口,点击如下所示的Start Allocation Tracking按钮,开始跟踪

图3-2 启动Allocation Tracking

2)操作手机相关待检测页面,执行相应路径代码,此时Allocation Tracker会在后台不断记录,注意操作时间不要过长,否则会产生过度的记录,不便于发现问题。然后再点击一次Start Allocation Tracking按钮,停止跟踪

图3-3 Allocation Tracking结果

3)最终会形成上图所示的阴影区域,表示内存跟踪的时间段,然后会生存以.alloc命名的文件,默认会以线程来分组

图3-4 Allocation Tracking日志

4)可选择Group by Method或Group by Allocator,每组内的数据默认按照对象的分配顺序排列,可点击每条展开,并可最终跟踪到最底层调用,Count表示分配的内存的次数,Size表示分配内存的大小

图3-5 结果排序与调用堆栈

5)若选择Group by Allocator,则按照包名来分组,组内排序和使用与上述相同

图3-6 按包名分组

6)标题栏有个统计按钮,点击后,可以生存炫酷的内存分配轮胎图或柱状图,默认是轮胎图,轮胎图是以圆心为起点,最外层是其内存实际分配的对象,每一个同心圆可能被分割成多个部分,代表了其不同的子孙,每一个同心圆代表他的一个后代,每个分割的部分代表了某一带人有多人,你双击某个同心圆中某个分割的部分,会变成以你点击的那一代为圆心再向外展开。如果想回到原始状态,双击圆心就可以了

图3-7 内存分配轮胎图

4. TraceView

TraceView 是 Android 平台特有的数据采集和分析工具,它主要用于分析 Android 中应用程序的 hotspot。TraceView 本身只是一个数据分析工具,而数据的采集则需要使用 Android SDK 中的 Debug 类或者利用Android Device Monitor工具:

  • 在一些关键代码段开始前调用 Android SDK 中 Debug 类的 startMethodTracing 函数,并在关键代码段结束前调用 stopMethodTracing 函数。这两个函数运行过程中将采集运行时间内该应用所有线程(注意,只能是 Java 线程)的函数执行情况,并将采集数据保存到 /mnt/sdcard/ 下的一个文件中,然后利用 SDK 中的TraceView工具来分析这些数据
  • 借助Android Device Monitor工具,采集系统中某个正在运行的进程的函数调用信息。对开发者而言,此方法适用于没有目标应用源代码的情况

1)打开Android Studio,进入Tools->Android->Android Device Monitor,选择相关进程,并点击Start Method Tracing按钮,开始追踪

图3-8 Start Method Tracing

2)操作应用待检测部分,不要时间太久,然后再次点击Start Method Tracing按钮,停止追踪,便可自动生成.trace文件

图3-9 Trace文件

TraceViewUI 划分为上下两个面板,Timeline Panel(时间线面板)和 Profile Panel(分析面板),Timeline Panel 又可细分为左右两个 Panel:

左边 Panel 显示的是测试数据中所采集的线程信息
右边 Pane 所示为时间线,时间线上是每个线程测试时间段内所涉及的函数调用信息。这些信息包括函数名、函数执行时间等

下半部分的Profile Panel 是 TraceView 的核心界面,主要展示了某个线程(先在 Timeline Panel 中选择线程)中各个函数调用的情况,包括 CPU 使用时间、调用次数等信息。而这些信息正是查找 hotspot 的关键依据。下表是Profile Panel 中比较重要的列名及其描述:

图3-10 Profile Panel列名与描述

可点击上述任一排序项进行排序

3)双击任一条记录,可展开详细信息,包括调用者(Parents)和子函数(Children),可详细的对调用次数过多的函数和每次执行时间过长的函数做排查,便于发现HotSpot

二、布局优化

1. 抽象布局标签

1)<include>标签
include标签常用于将布局中的公共部分提取出来供其他layout共用,以实现布局模块化,这在布局编写方便提供了大大的便利。

2)<viewstub>标签
viewstub标签同include标签一样可以用来引入一个外部布局,但是,viewstub引入的布局默认不会扩张,即既不会占用显示也不会占用位置,从而在解析layout时节省cpu和内存。viewstub常用来引入那些默认不会显示,只在特殊情况下显示的布局,如进度布局、网络失败显示刷新布局、信息出错出现提示布局等。

3)<merge>标签
在使用了include后可能导致布局嵌套过多,多余不必要的layout节点,从而导致解析变慢,merge标签可用于两种典型情况:

  • 布局顶结点是FrameLayout且不需要设置background或padding等属性,可以用merge代替,因为Activity内容试图的parent view就是个FrameLayout,所以可以用merge消除只剩一个
  • 某布局作为子布局被其他布局include时,使用merge当作该布局的顶节点,这样在被引入时顶结点会自动被忽略,而将其子节点全部合并到主布局中

2. 去除不必要的嵌套和View节点

1)首次不需要使用的节点设置为GONE或使用viewstub
2)使用RelativeLayout代替LinearLayout

3. 减少不必要的infalte

1)对于inflate的布局可以直接缓存,用全部变量代替局部变量,避免下次需再次inflate

三、代码优化

1. 降低执行时间

包括:缓存、数据存储优化、算法优化、JNI、逻辑优化等几种优化方式 。

1)缓存
缓存主要包括对象缓存、IO缓存、网络缓存、DB缓存,对象缓存能减少内存的分配,IO缓存减少磁盘的读写次数,网络缓存减少网络传输,DB缓存较少Database的访问次数。
在内存、文件、数据库、网络的读写速度中,内存都是最优的,且速度数量级差别,所以尽量将需要频繁访问或访问一次消耗较大的数据存储在缓存中。
Android中常使用缓存:

  • 线程池,对线程的缓存
  • Android图片缓存
  • 消息缓存,通过handler.obtainMessage复用之前的message,如下:handler.sendMessage(handler.obtainMessage(0, object));
  • ListView缓存
  • 网络缓存,数据库缓存http response,根据http头信息中的Cache-Control域确定缓存过期时间
  • 文件IO缓存,使用具有缓存策略的输入流,BufferedInputStream替代InputStream,BufferedReader替代Reader,BufferedReader替代BufferedInputStream.对文件、网络IO皆适用
  • layout缓存
  • 其他需要频繁访问或访问一次消耗较大的数据缓存

2)数据存储优化

包括数据类型、数据结构的选择:

  • 数据类型选择
    字符串拼接用StringBuilder代替String,在非并发情况下用StringBuilder代替StringBuffer。
    64位类型如long double的处理比32位如int慢。
    使用SoftReference、WeakReference相对正常的强应用来说更有利于系统垃圾回收。
    final类型存储在常量区中读取效率更高。
    枚举值相比常量会占用更大的内存空间和运行开销,适用于强类型安全的场景,对于普通常量的汇总,就不一定需要。
    尽量使用原始数据类型,避免频繁的自动装箱。
    避免使用非静态内部类,当创建并实例化了一个非静态内部类,内部就包含一个指向外部类型的隐含引用,如果这个内部类实例比外部类型存活的时间还长,那即使不需要这个外部类型,它还是会被保存在内存中。
    LocalBroadcastManager代替普通BroadcastReceiver,效率和安全性都更高。
    如果对象中某个方法的调用不依赖于该对象的实例化,则建议将该方法定义成static,可提高访问与调用效率。
  • 数据结构选择
    常见的数据结构选择如:
    ArrayList和LinkedList的选择,ArrayList随机访问更快,LinkedList更占内存、随机插入删除更快速、扩容效率更高。
    HashMap、LinkedHashMap、HashSet的选择,HashMap为键值对数据结构,LinkedHashMap可以记住加入次序的hashMap,HashSet不允许重复元素。
    HashMap、WeakHashMap选择,WeakHashMap中元素可在适当时候被系统垃圾回收器自动回收,所以适合在内存紧张型中使用。
    Collections.synchronizedMap和ConcurrentHashMap的选择,ConcurrentHashMap为细分锁,锁粒度更小,并发性能更优。Collections.synchronizedMap为对象锁,自己添加函数进行锁控制更方便。
    Android也提供了一些性能更优的数据类型,如SparseArray、SparseBooleanArray、SparseIntArray、Pair。
    Sparse系列的数据结构是为key为int情况的特殊处理,采用二分查找及简单的数组存储,加上不需要泛型转换的开销,相对Map来说性能更优。
    当需要存储一对元数据数组,例如(Foo,Bar),采用两个平行的二维数组Foo[ ]和Bar[ ],要比直接存储对象(Foo,Bar)数组的效率高。
    使用增强的for-each循环对实现了iterable接口的collections以及数组做遍历,for (Foo a : mArray)和for (int i = 0; i < len; ++i)效率最高,避免使用for (int i = 0; i < mArray.length; ++i)

3)算法优化
这个主题比较大,需要具体问题具体分析,尽量不用O(n*n)时间复杂度以上的算法,必要时候可用空间换时间。查询考虑hash和二分,尽量不用递归。

4)JNI
Android应用程序大都通过Java开发,需要编译器将Java字节码转换成本地代码运行,而本地代码可以直接由设备管理器直接执行,节省了中间步骤,所以执行速度更快。不过需要注意从Java空间切换到本地空间需要开销,同时编译器也能生成优化的本地代码,所以糟糕的本地代码不一定性能更优。

5)逻辑优化
主要是理清程序业务逻辑,减少不必要的操作和分支判断等。

2. 异步,利用多线程提高TPS

充分利用多核CPU优势,利用线程解决密集型计算、IO、网络等操作。

3. 提前或延迟操作,错开时间段提高TPS

1)延迟操作
不在Activity、Service、BroadcastReceiver的生命周期等对响应时间敏感函数中执行耗时操作,可适当delay,Java中延迟操作可使用ScheduledExecutorService, Android中除了支持ScheduledExecutorService之外,还有一些delay操作,如handler.postDelayed,handler.postAtTime,handler.sendMessageDelayed,View.postDelayed,AlarmManager定时等。

2)提前操作
对于第一次调用较耗时操作,可统一放到初始化中,将耗时提前。如得到壁纸wallpaperManager.getDrawable();

四、关于内存泄漏

1. 使用Android Studio Java Heap Dump检测

1)打开Android Studio,进入Android Device Monitor,切换到Memory监控窗口
2)选择待调试进程com.baidu.ls.waimai,并操作App中可能发生内存泄漏的地方,点击Initiate GC按钮进行强制GC,然后点击Dump Java Heap按钮,导出.hprof格式文件

图4-1 Dump Java Heap

3)打开.hprof文件,可选择Package Tree View,选择应用包内想要查看的对象类型,可在右侧Instance窗口查看该类型已存在的实例化对象数量,在下面Reference Tree窗口查看该对象引用路径

图4-2 hprof文件分析

4)上面窗口有个Total Count筛选项,可以看到该对象目前在内存中存在的数量,若某个对象数量存在异常,比如此时App所有的点菜页已关闭,但ShopMenuFragment数量不为0,则该对象很可能发生内存泄漏,需要根据reference tree做相关排查,并找到最终的GC Root

2. 使用LeakCanary检测

LeakCanary是一个用于检测内存泄露的开源类库,提供了一种便捷、自动的检测方式,并产出可视化报告,以很直白的方式将内存泄露链条展示给我们。

1)接入,在build.gradle中根据不同的编译方式,选择加入不同的引用方式,

dependencies { 
        debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3' 
        releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3' 
     }

2)LeakCanary.install(context) 会返回一个预定义的 RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy() 之后泄露的 activity。
在Application中进行配置:

public class ExampleApplication extends Application { 
  public static RefWatcher getRefWatcher(Context context) { 
    ExampleApplication application = (ExampleApplication) context.getApplicationContext(); 
    return application.refWatcher; 
  } 

  private RefWatcher refWatcher; 
  @Override public void onCreate() { 
    super.onCreate(); 
    refWatcher = LeakCanary.install(this); 
  } 
} 

3)使用RefWatcher监控Fragment

public abstract class BaseFragment extends Fragment { 
  @Override public void onDestroy() { 
    super.onDestroy(); 
    RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity()); 
    refWatcher.watch(this); 
  } 
} 

4)产出可视化报告

图4-3 LeakCanary可视化报告

3. 常见避免内存泄露方式

1)对Activity等组件的引用应该控制在Activity的生命周期之内,如果不能就考虑使用getApplicationContext或者getApplication,以避免Activity被外部长生命周期的对象引用而泄露
2)尽量不要在静态变量或者静态内部类中使用非静态外部成员变量(包括
context ),即使要使用,也要考虑适时把外部成员变量置空;也可以在内部类中使用弱引用来引用外部类的变量
3)对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:将内部类改为静态内部类、静态内部类中使用弱引用来引用外部类的成员变量
4)Handler持有的引用对象最好使用弱引用,资源释放时也可以清空 Handler 里面的消息。比如在 Activity onStop 或者 onDestroy 的时候,取消掉该 Handler 对象的 Message和 Runnable
5)在 Java 的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋值为 null,比如使用完Bitmap 后先调用 recycle(),再赋为null,清空对图片等资源有直接引用或者间接引用的数组(使用 array.clear() ; array = null)等,最好遵循谁创建谁释放的原则
6)正确关闭资源,对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销
7)保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期

六、参考链接

  1. Android官方性能优化:
    https://developer.android.com/training/best-performance.html
  2. Android官方性能典范教程:
    http://hukai.me/android-performance-patterns/
  3. 性能优化博客合辑:
    https://github.com/Juude/awesome-android-performance
  4. Trinea性能优化合辑:
    http://www.trinea.cn/android/performance/
  5. Android性能优化多途径实现:
    http://blog.csdn.net/yanbober/article/details/48394201
  6. Android Studio官方profile工具使用:
    https://developer.android.com/studio/profile/index.html
  7. Android内存泄露总结:
    https://yq.aliyun.com/articles/3009
  8. Android TraceView使用:
    http://www.cnblogs.com/sunzn/p/3192231.html
  9. LeakCanary使用:
    http://www.jianshu.com/p/7db231163168

推荐阅读更多精彩内容