Android应用性能剖析全攻略

性能是软件质量的一个重要方面,好的软件必须要在性能上达到一定的标准。对于Android应用程序来讲,更是如此,移动互联网的红海竞争,如果应用的性能差,肯定会缺少竞争力的,这里就来聊一聊应用开发中如何提升性能,以及在开发过程中如何处理性能问题。

##性能的定义
对于Android应用程序来讲分为三个方面,一方面是软件整体表现上的性能,也就是能多快给用户想要的结果,比如新闻阅读类应用,这个性能就是当用户点一条新闻时,多快能把新闻内容展示给用户,这个通常取决于业务逻辑,网络,以及后台服务器的性能。
另外一方面就是UI性能,也就是所谓的流畅度,这个在移动应用上面有着更严重的影响,因为触摸和手势的原因,如果应用程序不流畅,会严重影响体验,相比如PC桌面软件会更严重。这个是我们通常所谓的性能,大多数情况下,以及网络上绝大多数文章都是针对于此。对于安卓应用来说要想达到流畅,或者说做动画时,列表滑动时不卡顿,那么帧率(FPS Frame per Second)要达到60这个也是我们在做性能优化时的一个衡量的标杆。
还有一方面就是更少的资源占用,包括内存,CPU,电池,磁盘,网络流量,服务器资源等等。这个也很重要,特别是内存,CPU和电池,前二个对于所有软件来说都是衡量性能的一个重要指标,电池则是移动应用特有的,特别是智能手机上面。
总之,性能是一个很大很大的话题,也是一个无止境的任务,适可而止,见好就收。虽我们都有着一颗工程师的心,都想把东西做到极致,但试问天下,又有谁真的能把所有的东西都做到最优呢?具体把性能做到什么程度,要看需要强烈与否。比如一个应用在生命初期,可能没有人关注性能。但假如已到百万,千万级别的时候,才考虑性能也是作死的节奏。即使是超级App,性能优化也要适可而止,

##如何提升UI流畅度

###造成UI不流畅的原因
要想让UI流畅,首先要了解一下造成不流畅的原因都有哪些:

  • 主线程做了费时操作,或者本不该在主线程中做的轻微逻辑,这不但会严重影响帧率,甚至还会触发ANR(Application Not Responding)
  • 布局过于复杂或者View层次太多
    这个情况也是经常出现。无论是页面确实复杂,或者为了实现某些特殊的视觉效果(比如边框或者3D效果),结果就是一个非常复杂,层次深,View个数多的布局,最终结果就是渲染性能差。特别是对于列表的Cell,影响更加严重,都会造成滑动时的卡顿。

  • 局部更新造成了整体布局的重绘
    这里指的是,一个View层次中的某一个View需要刷新,但是却会触发整体页面的刷新,从而造成浪费。

  • 整体布局的重绘被触发了多次
    这通常出现在需要动画的场景,比如以改变View的布局(大小)的方式来实现动画,或者频繁的改变View的层次,比如频繁的addView和removeView。这都会不断的触发measure/onMeasure,layout/onLayout和View的重绘。

  • 敏感方法里面做了太多不相干的事情
    通常是View的一些关键的方法中onDraw, onMeasure, onLayout,特别是onDraw里面只应该做绘制相关的,连创建对象这种级别的事情都最好别做。当然,这个出现的情况比较少,毕竟需要直接自定义一个原始View的情况并不多见。

  • 频繁的GC发生
    无论是在主线程,还是worker线程,如果频繁的大量的创建对象,就会触发频繁的GC,GC会对所有的线程产生影响,对UI线程也是有影响。

90%的情况前四种情况是主因,把前四种情况解决了就无大碍了。而前四个中,前二个又是重灾区,通常情况处理了前二个就能解决不流畅的问题。
知道了原因,就可以对症下药了:

###设计和编码时要考虑性能
性能是设计和编码时必须要考虑的一个因素,跟程序的正确性,robustness和可维护性同样重要。而不是应用已经上线了很久后才开始考虑性能问题。但是我们活在现实生活中,实际的情况往往都是当应用已经上线了并且稳定了之后才开始做,而且情况往往都是代码都还不是你写的。设计和编码时不考虑性能的原因一般有:

  • 开发人员水平不足,意识不到性能问题,或者不知道如何写出高性能的代码
  • 需求太多,或者需求经常变动,没时间考虑别的

总之,无论如何,在设计和编码时不考虑性能是很令人烦恼的事情,但亡羊补牢,虽有些无奈但还是有益的。

###简单设计做更少的事情
这似乎是废话,少做事情,或者不做事情效率自然高,性能肯定能上去。页面布局尽可能简单,功能尽可能简单,能做一遍的事情不要做二遍,没必要的准备工作不做,等等。但是现实情况往往是应用越做越复杂,越做越功能越多,页面越来越复杂,这是多种元素决定的,或许是竞争的需要,或者是产品这么定义的,或者是老板就喜欢这样。
但无论怎么样,对于开发人员来讲,当实现功能时要本着简单的原则,这说来容易,但是当代码出来时却千差万别,明明很简单的逻辑,有人却能代码写的巨复杂,一坨一坨的。虽然可能说你看得懂他的设计图,看得懂他的流程图看得懂他的类图等等,但是你却不一定看得他的懂代码。
这里扯一点题外话,写代码绝对是衡量一个程序员的重要指标,虽然不能做为全部,但是至少应该占50%。所以如果面试时看不到应聘者近一二个月的代码,或者不让其当场写代码的话,面试可以认定是失败的。尽管他可能是BAT出身,尽管他可能做过(维护)过顶级App,但是很可能他写出的代码都跟翔一样,一坨一坨的,完全看不懂写的是啥玩意儿。孤认为,面试时最好花一天或者一个下午时间,让应聘者在近似真实的环境中写代码,或者是一个小功能,或者是一个小项目,或是修改一个bug,最好还是坐在他旁边,与其一起工作,就好像平日里你跟同事一起工作一样,这非常有效果,也很能看出一个人的水平,而且你聘他来后也是要这样子工作的。光在那里Bla bla的问答,连他说的是真是假都难以分辨,而且世上事永远都是说起来容易做起来难,我们都见过很多人Blabla就会说,就会吹,不会做事情,或者干起事情跟小孩子一样,也有很多人实干型的,会做事,能把事情做好,但就是说不出,或者非常不愿意在别人面前blabla。然并卵。。。。蛋扯远了

###远离主线程(UI线程)
这似乎才是正题。
对于应用程序来说主线程是很重要的,因为主线程通常的作用是用于刷新用户界面(UI),与用户进行交互,是与用户接触最近的,因此也通常被称作UI线程。Android和iOS都是如此。想像一下,应用要想达到60FPS,也就是说一帧的绘制要在16ms内完成,你的布局又那么的复杂,一层套一层,每个View都要一遍遍的measure, layout, draw,就知道主线程有多么忙碌了,还能忍心再做其他事情吗?
那么,让应用流畅就变得很简单,在主线程中做最少的事情,但不能更少,它只做二件事情:

  • UI(View)相关的事情
    这个是平台框架的限制,必须遵守。

  • 必须在主线程中做的事情
    比如启动其他线程,必要的初始化等等。比如像AsyncTask是一定要在主线程中初始化的,否则会有Crash,具体可以看这篇文章的分析。

其他,所有事情,都应该放到其他线程中去。如果在设计和编码的时候能考虑到这二点,那么你的应用流畅至少不会卡。使用其他线程异步操作时一定要注意生命周期和上下文,也即当执行任务时生命周期是否还是活动的,或者所依赖的上下文是否已经变化了,不在了。

###布局的优化

####减少View的层次和数目,减化复杂布局
View的层次越少,数目越少,肯定渲染越快,这个常见的技巧有:

  • 删除没有用的View
  • 除去无必要的嵌套,比如当内部仅有一个View时,外面就没有必要再加一个ViewGroup了
  • 多使用RelativeLayout。它能够随意的排版View,三维上的方位都可以搞定,所以对象像列表的Cell之类的,一个RelativeLayout基本上就可以搞定。
  • 用TextView的drawable属性来组合图片+文字
  • 用merge来减少层次
  • 对于某些情况才用到的View,就使用ViewStub,然后在需要显示的时候再inflate。也就是所谓的延时和按需渲染
  • 尽量不要用背景图片,特别整个Activity大小的背景,费内存,占资源
  • 尽可能用矢量图形,比如颜色,drawable,shape,icon font等等

减少View的层次和数目能显著提高帧率。曾经有一个列表,列表不复杂,左边一个TextView,右边有三个也是TextView,但是在添加的时候在外面又包了一层TextView,布局就变成了:
1
2
3

<LinearLayout....><TextView/></LinearLayout>

虽然可能这不起眼的多加了一个LinearLayout,但是别忘记了,这是在List中,一屏会显示10多行,每一行多3个View,加起来就是30多个View啊!一次多绘制30多个View是什么概念?
对于布局的优化可以多看看lint的输出Warning,它对于无用的View,没必要的嵌套,以及优化建议都能准确的给出提示。

####当局部更新时不要触发整体重绘
比如一个坨复杂布局中,仅需要更新一个图标时,就直接更新它所属的ImageView就好;再如,有CheckBox选中状态的列表,点击时,就只更新具体的列表的具体的CheckBox就可以了,而不是改变数据,然后notifyDataSetChanged。
这里需要,首先,不要故意的去触发整体刷新(除非非常的有必要,比如多个View都需要刷新数据时);另外,就是要小心防止触发整体刷新的坑,因为某些原因,即使小心的更新局部也会造成整体的刷新。

####避免频繁的触发整体的重绘
千万不要直接改变View的大小的方式来做动画,或者在做动画的同时改变View的布局,更不要添加或者移除View,这都会直接触发整体的重绘。

####避免在onDraw的时候做额外的事情
如果是自定义的View就要注意这个事情,在onDraw的时候不要去new对象或者做其他不相干的事情,即使这些操作在UI线程中作也毫不费时的。

####列表类的优化
对于列表(List和Grid)优化除了上面提到的,还要注意使用组件传回来的convertView以及ViewHolder。convertView可以复用View对象,避免inflate过多的View。ViewHolder模式主要是减少findViewById的调用。

####把界面设计的尽可能简单
大道至简,简约是最优秀的用户体验,没有之一,所以产品汪们,不要把页面搞的太复杂,会导致不好用:用户不会用,和渲染性能差。

####写布局时要考虑到渲染性能
这是非常重要的,再牛B的方法和技巧,如果你不鸟,或者不用都木有卵用,如果你心系性能,必然会有所思,有所为,然后渲染性能就所升。

###及时反馈给用户
这实际上不是真正的流畅,而是给用户感觉流畅,避免用户认为应用假死。比如当做一些费时操作的时候,是放在了工作线程中,但是主线程也却没事情做,应用流畅不卡顿,但在用户看来却是无意义的,这时可以用一些动画,进度等等及时反馈给用户程序当前的状态。
另外,当做费时操作的时候也要及时终止并反馈,程序可能会有异常情况或者错误情况,都是需要处理的,比如从网络加载数据,可能会有无网络,或者网络异常,或者服务器返回异常,那么要尽早失败。比如是不是可以在任务启动前先判断网络状态,而不是照常发请求,网络返回异常了,那么正常情况时的结果处理就不要做了,等等。
说到这里,不得不讲一下代码的编写原则:先检查异常情况,尽早退出,而不是层层if,举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

DatafetchNewsDetail(Stringurl){if(urlisinvalid){returnempty;}if(nonetworks){returnempty;}if(someotherbadconditions){returnempty;}sendrequset;if(responsecodenot200){return;}if(noresponse){return;}if(parseresponsefailed){return;}returnparsedata;}

而不是这样:
1
2
3
4
5
6
7
8
9
10

// Ugly code, DO NOT do thisDatafetchNewsDetail(Stringurl){if(urlvalid){if(hasnetworks){if(responsecode200){if....}}}}

##流畅度剖析工具

###流畅度定性体验
那么如何测试或者衡量我们应用是否流畅呢? 首先就是自己体验,快速滑动,看看是否能感觉到卡顿,或者页面闪烁。

###借助开发者工具来感受
开发者工具有很多选项可以帮助开发者来测量,比如调试过度绘制,显示GPU更新等。通过这些可以看出不必要的UI刷新。
比如开发者选项里有一个”硬件加速渲染“,里面有一个“调试GPU过度绘制”,这个会在屏幕上以颜色来区分overdraw(过度绘制,也就是进行了不必要的绘制)的严重重度:

  • 蓝色 1 倍overdraw
  • 绿色 2 倍overdraw
  • 红色 3 倍overdraw
  • 紫色 4 倍overdraw

总之,颜色越深,证明做了过多的不必要的绘制(overdraw).什么又叫过度绘制呢(overdraw)比如一个列表,如果每个Item都有背景色,那么List本身实际上是不需要背景色的,比如子View占满了父View,那么父View不用画背景,等等。对于不可见的元素,就不要运行绘制,这是减少overdraw的方法。

在开发者选项面有一个是“监控”,里面有几个:

  • 启用严格模式
  • 显示CPU使用情况
  • GPU呈现模式分析
  • 启用OpenGL跟踪

特别是第3个“GPU使用情况”,它是系统在GPU渲染时加入一些分析,以呈现UI渲染的性能,它有三个选项:

  • 关闭
  • 在屏幕上显示为条形
  • 在adb shell dumpsys gfxinfo中

其实,它的数据是一样的,只不过一个是在命令行把raw data输出,一个是在手机屏幕以图表方式展示。后面会详细介绍这个。

###adb shell dumpsys gfxinfo
这个能收集GPU渲染时的一些数据,从而反映应用UI渲染的性能信息。
从这个命令的输出能看出二个信息一个帧的数量,另一个就是每一帧绘制的情况。 应用比较卡,表现出来就是丢帧,也就是有些帧太慢了,赶不上火车了,不得不丢掉,从而页面会卡顿。正常来讲,即使是简单的布局,用这个命令抓也至少能抓到20+帧的数据,如果少了,或者很少,只有几帧,就就证明你在主线程中干了太多其他的事情,也就是说主线程被block了。这时就要好好看看源码,主线程中都干了啥,哪里可能会耗时,把非UI操作都放到工作线程中去。

对于每一帧的数据,体现着绘制这一帧所花的时间:

  • Draw是创建列表所需要的时间,表示运行绘图方法用了多长时间,比如View.onDraw()所花的时间;
  • Prepare在5.0版本加入了这一列数据的显示
  • Process是Android 2D引擎渲染显示列表(DisplayList)所需要的时间。页面上的View越多,层次越深,就会有越多的绘制命令需要执行,这个值会越大。
  • Execute是把一帧数据送到屏幕上排版显示的时间,这个值通常会比较小,且在应用层无法直接控制,换句话说,这个时间是无法优化的。

为了流畅,每一帧的绘制时间应该少于16ms,因为应用要想流畅要达到60FPS,算下来就是一帧不能超过16ms,但这个并不是死规定,不是说某一帧超过,应用就会卡,就会慢,而是说几十帧的平均值或者90%的帧应该在16ms以内。
这个方法是针对每个ViewRootImpl的统计数据。ViewRootImpl对象就是一个View的根元素,通常情况下一个Activity仅有一个ViewRootImpl对象。需要注意的是Dialog也会有一个ViewRootImpl,所以当有Dialog时,你会看到二个ViewRootImpl的统计数据。
还有需要注意的是,如果使用了SurfaceView(比如GLSurfaceView),因为它不是使用常规View的渲染方法来渲染的,它有自己的线程和渲染方式,所以这个方法是抓不到SurfaceView的渲染性能的。

在屏幕显示,则会在屏幕上面以柱状图的方式实时显示UI每一帧渲染的性能,可以看到一条绿色的线,这个就是16ms。柱状图中几种颜色所代码的意义分别是

###traceview
这是一个十分强大的功能,能得到某一时间段内,进程内的时序执行情况,具体到能体现出所有线程的所有方法执行所花的CPU时间和实际时间,并且还能看出包含子调用和不包含的情况。

####启用方法
在Android Studio中点击Android Device Monitor或者直接运行monitor (位于SDK/tools/),选择某一进程,然后点击,开始录制,再点击结束,就会出现。


####如何分析
颜色越深代码花的时间越多。

主要指标有:

  • CPU time 某个方法占用的CPU时间
  • Real time 某个方法运行的真实时间
  • CPU time/call - 某方法CPU时间与调用次数比

还有二个前缀:

  • Incl - 这是Inclusive简写,意思就是包含方法里面的子调用
  • Excl - 这个是Exclusive的简写,意思方法本身,不包含子调用

通过这个可以分析出哪些方法比较耗时。

###systrace
systrace可以查看出进程的执行情况,不单单是你的应用进程,也能看到系统进程的执行情况,能够以时间线的形式来展示进程中各线程的执行情况。

####如何使用
根据系统版本的不同使用方法略有不同:

  • Android 4.3及以上系统
  1. 确保打开了ADB调试模式
  2. 执行以下命令

    $ cd android-sdk/platform-tools/systrace
    $ python systrace.py –time=10 -o mynewtrace.html sched gfx view wm

输出的mynewtrace.html文件就是带有trace的结果,用浏览器打开查看即可。

  • Android 4.2及以下系统
  1. 打开ADB调试模式
  2. 开发者选项中->监控->启用跟踪中选择想要查看的类型
  3. 执行命令

    $ python systrace.py –cpu-freq –cpu-load –time=10 -o mytracefile.html

更多的systrace命令的使用方法可以参考官方文档

####如何分析结果
systrace命令得到的结果是一个HTML文件,用浏览器打开即可.
基本操作:w 放大;s 缩小; a 向左移动;s 向右移动
从中可以看出帧绘制的信息,通常每一帧应该小于16.6ms,为绿色。对于有问题的,比如delay或者绘制时间长的,会以黄色和红色标注出来,并且在顶部会有Alert。点击帧F和Alert可以看到具体的详细信息,以及系统自动分析出来的可能的原因。

###hierarchyview
这个工具很明显就是用来调试布局的,它能以可视化的方式展示View的层次结构,顺带显示每一层View的渲染速度。运行方法是找到SDK/tools/运行hierarchyviewer.
注意:默认情况下只有调试的ROM(build with eng)才能抓到View的层次信息(否则,应用的页面就很容易被破解了),对于可控制源码的可以用开源库来解决这个问题。

###代码层次剖析打点
这个要对代码熟悉后可以进行,对于怀疑执行较慢的代码加上时间打点(System.currentTimeMillis())来确定其执行所花的时间。也就是说在编码的时候要有意识,对于持有怀疑态度的方法,要时不时的打时间点,以看其是否能放在主线程中。

###打开StrictMode
这是一个开发者工具,能够帮助开发者检测到不经意间做的一些违反平台开发原则的事情,比如在主线程中做了IO操作或者主线程中操作网络等等。时至今日它能检测的远不止这些,还能检测主线程中的比较慢的方法调用,还有检测Dialog的泄露(Dialog未关闭,Activity就退出了),Activity的泄露以及未正确关闭的对象(Cursor, Binder)等。总之,它能帮助你减少因为代码写法不规范而造成的问题。详细的如何使用可以参考文档

##如何提升程序性能
这个比较难,比如读取大文件必然耗时,从服务器上取数据肯定慢(比从本地读),但是聪明的人类还是有方法做的更好的:

####把业务逻辑弄简单点
这个就不废话了,代码搬运工们没有太多的话语权。但是对于能控制的部分要做好,比如尽早失败,不重复等等。

####多用缓存
缓存绝对是计算机技术一个非常重要的东西,发明这东西的人肯定是个天才。缓存无处不在,缓存的目的就是提高性能,加快访问速度,衡量缓存好坏就看命中率。CPU有三层缓存来提升运算性能。软件中缓存也是提升性能的一个非常重要的手段。
比如对于不太常变化的数据,从网络成功获取后就要缓存在本地;再如,对于经常访问的本地数据也要在内存中有缓存;用到的图片比较多的应用,要做内存和本地二级缓存,以减少图片的加载时间(比如UIL的做法);
常见的缓存工具有内存级的LruCache以及磁盘级的DiskLruCache,教程可以参考这里

####延迟加载和按需加载
这个就容易理解一些,比如三层页面才用到的数据,你没必要一启动在第一级页面就加载它(当然,也可能有这样的情况,比如数据有依赖时)。
按需要加载就是,第一个页应该只加它需要的数据,而不是一个请求,把应用所有数据都拉下来。

####尽早发出异步请求
对于像异步从网络获取数据,或者异步IO加载数据的,或者做一些费时的异步初始化等,可以尽早的把请求发送出去,在等待结果的同时再做其他事情,这样能保证结果最快的呈现出来。

####使用工具(开源库)
这个就是,世上总有人比你聪明,他们的方法更巧妙,更高效,为什么不用呢?比如图片加载,比如网络库,比如JSON解析等等,那么多优秀的人做的优秀的东西不用太浪费了。要感谢那些优秀的开发者,总能找到合适的库,不但好用,而且开源,既然完成任务,又能学习,还有比这更好的事情么?
可以到这里这里来找需要的开源库。

##如何占用更少资源
对于资源的使用首页的原则就是,尽量少用或者不用,听上去是废话,其实不然,有一些具体的可实践的准则可供参考。其实这里面的话题每一个都可以扩展成一整篇文章来探讨,这里仅列出一些要点,不作细致讨论。

###内存

####尽可能的少创建对象
主要的原则就是尽可能的复用,比如像对话框,或者Toast之类的都是可以复用的。再如尽可能的把创建对象放在循环外面等等。

####尽量缩短对象的生命周期
比如能在一个调用链中传递的对象就没有必要非声明为成员变量。在方法尾部使用的对象就别在一进入方法时就创建。用户事件触发的逻辑就没有必要一进入页面时就创建。当onResume后才会使用到的对象就没有必要在onCreate里创建等等。

####避免内存泄露
所谓内存泄露就是内存在不再使用之后仍没有得到释放,一般情况下它是无害的,无非也就多用点内存,现在设备内存越来越大,空着不用也浪费,但是内存总有用尽的时候。对于Android,更是如此,每个应用(进程)有固定的内存配额(HeapSize),它是由系统ROM决定的,所以一旦有泄露,程序必定会因OOM(Out Of Memory Error)而崩溃(其实崩溃了也是好事,一是你会重视,二是进程退出了,重新启动后内存泄露会得到一定的缓解),特别是现在应用中的图片和视频等多媒体元素越来越多,这些东西本来就吃内存,再来点泄露,那么发生OOM的机率大大增加。
Android中最容易泄露的对象就是Activity,Activity对象由系统创建,生命周都是由系统来控制,我们只能发送请求, 不能强行干预。正常情况下的Activity对象在onDestroy()之后是要被回收的,所以如果在onDestroy以后仍有其他生命周期更长的对象持有对Activity对象的引用的话,就会导致Activity的泄露。
而Android中很多系统API都是需要Context(少量的是需要Activity,比如Dialog),而Activity又是Context的一个实现,因此啊,很多人在很多时候都简单的把Activity对象直接传了过去,很多系统API的生命周期要比应用程序长的得多,这就是导致Activity对象泄露的原因。避免这种泄露很简单,就是尽可能传ApplicationContext,也就是说不要直接传Activity对象,而是传activity.getApplicationContext()。因为ApplicationContext一个应用只有一个,也就是说一个手机里只有一个,而且系统本身就会缓存它,所以长一点持有它也没关系。当然要视情况而定,比如像Dialog虽然是Context,但必须传Activity。

####缓存对象,以避免复创建
比如像Dialog对象,可以缓存起来以避免每次都创建新的。
对于大量的缓存对象可以使用LruCache来管理。

####对于缓存,尽量用WeakReference
特别是像Activity和Fragment以及Service等有固定生命周期,且生命周期又是由系统来控制的对象,最好加持有WeakReference。

####监听onTrimMemory和onLowMemory,以采取措施
当系统内存吃紧的时候会向Activity发送通知,此时可以做一些措施,比如释放不用的资源,释放不用的对象,清空缓存等以缓解压力。

####内存使用监测工具和分析方法
可以时不时的用监测工具来监测一下应用所消耗的内存,有这些方式:

  • adb shell dumpsys meminfo
  • Android Device Monitor - (其实就是早期的DDMS的进化版本)监测用的GUI工具,选择进程,然后update heap,就能实时看到heap使用情况
  • AndroidStudio 已经集成了内存监测工具,可以实时看到内存的使用情况。
  • MAT - Memory Analysis Tool它是Java的标准内存分析工具,安卓的dex不直接支持,但无妨,可先用monitor dump出prof文件,再用SDK中的工具hprof-conv进行转换后MAT就认识了。详细的可以参考这篇文章
  • 更多的Java内存使用建议可以参考这篇文章.
  • 学会查看GC输出的信息
    Logcat日志中的GC信息也能非常直观看出内存的使用情况,而且看出性能上的原因,特别是UI卡顿,或者动画丢帧等情况。因为GC或者说频繁的GC发生,是会影响到应用性能,特别是会影响UI线程。GC的日志通常能看出触发GC的原因,释放掉了多少内存以及花了多少时间,具体的还跟虚拟机的版本不一样而不同,下面分别来详细的讲述:
  • Dalvik
    Dalvik虚拟机GC的日志格式如下:

    dalvikvm: , ,

  • reason – 触发GC的原因
  • freed – 此次GC释放了多少内存
  • free memory – 还有多少空闲的内存空间
  • time – 此次GC花费多少时间

其中reason又有几个:

  • GC_CONCURRENT
  • GC_MALLOC
  • GC_EXPLICT
  • GC_BEFORE_OOM
  • ART
    ART虚拟机的GC格式比Dalvik要详细一些:

    I/art: ,,,,

更多内容可以参考这篇文章

准确的来讲MAT是分析工具而非监测工具,也就是当发现有内存泄露的时候抓一段heap的使用情况用MAT来分析。其他几个都可以用来监测,也就是说看一下内存是否有问题,表现都是当操作时内存使用会有所增加,但当操作停止后内存应该迅速回落到操作前的水平。重复操作,内存使用不应该一直增加。如果长时间内存没回落或者内存一直增长,那么就很可能存在内存没有释放掉,就要抓heap然后用MAT分析,看是哪里出了问题。

###CPU

####减少忙等待
也就是说使用注册Listener(通俗的就是callback)方式来处理异步事件,而不是忙等待:
1
2
3
4

// DO NOT do thiswhile(somethingNotReady){sleep(100);}

####合理使用线程
理性的仅在有必要的费时操作启动worker线程来完成。不要盲目的创建线程。线程多了,不一定性能就上去了,反尔会带来同步的无尽烦恼和不可捉摸的诡异偶现Bug,而且频繁的Context Switch也会带额外的损耗。
对于频繁执行的异步任务,最好使用线程池,一方面可以复用资源,另一方面也方便控制。
对于长时间执行的任务,或者有Server用途的长时间工作线程,要使用Looper和消息队列Handler,详细的可以参考这篇文章
仅当需要与UI有交互的情况下才考虑使用AsyncTask,具体看这篇文章

####严格控制Service的生命周期,做到按需启动,及时停止
安卓的Service绝对要为手机的卡顿负一部分责任,系统放任Service,Service的控制权都在开发者手中,所以Service被滥用的特别严重。打开手机的设置,看看正在运行的应用程序,可以发现几乎所有的应用都有至少一个到二个左右的Service进程在运行。所以说安卓能不耗电么,能不卡么,能不耗流量么,跟水果手机咋比啊。
为了体现专业性,使用Service就要小心,当有需求的时候再启动(startService or bindService),当不用了就stopSelf or stopService。

####监测工具
在Android Studio中有工具可以监测CPU的使用情况

###磁盘

####没必要存的东西就不要存
比如直接作用到UI层面的一些信息,显示完就不再使用了,这种数据是没有必要缓存到磁盘上的,至多在内存中缓存就可以了。

####不是长期使用的就用临时文件,且是用标准API创建的临时文件
在同一个启动Session中,不同阶段都要使用的数据,可以用临时文件来存取,比如启动时,或者加载完时创建一个临时文件来存储,后面再使用。创建临时文件要用标准的File#createTempFile方法,而不是创建一个普通文件当作临时用。因为常常会忘记删除掉,即使有删除动作,但假如有异常出现,也会走不到删除。久而久之磁盘上的垃圾文件会越来越多。

####如果不再需要就及时的删除文件
这个可以讲其实国内的甚至国外的绝大多数软件做的都不好,特别是机身存储和SD扩展卡上面的内容,因为这些区域是开放给所有App的,而且容量一般都很大,所以大家都很高兴的写,没有人去删除。这也是为什么市场上面的清理软件如此的受欢迎。作为良心开发者,还是自己擦自己的屁股吧!

####定期整理数据库,删除旧数据
数据库也跟磁盘一样,长期使用后会有过期的数据,也是需要清理的。
另外,由于数据库不断的增删改,会导致数据库文件产生断层(文件大小不必要的大于实际内容),或者碎片,这时就需要execute(“vacuum”)来重新生成数据库文件。当然这个比较有风险,而且耗时比较长,所以,只有当达到一定时间时才有必要这样做。

####给APK瘦身
虽然,安卓应用程序发布较PC软件非常之容易,各大应用市场傻瓜式的一键式搞定,但是,用户仍然需要下载和安装,这期间APK的大小直接影响应用的成功安装率,小的APK文件,下载快,耗流量少,安装快,占用ROM也少,低端机型的ROM没那么大。所以APK的瘦身也是势在必行的一个优化指标。
一般来说有这么几个方面,可以去下功夫:

  • 删除无用资源
    不再使用的图片,布局,库不但增加目标文件大小,而且会延长编译和打包的时间。不用了就删除,后面用的时候再还原。如果代码太多,或者不够熟悉搞不清该不该删除,可以参考lint的warning信息。

  • 删除无用代码
    这个比资源还严重,其实不用的代码对包增大没太大的作用,但是没有代码会严重影响项目的清析度和可维护性。比如新人来了,看一坨代码,最后发现半坨都是没用的代码,心中必有万个马在奔腾。不用了就删除,以后用到时可再还原,版本控制就是专门干这事的。

  • 集中使用xhdpi(或者xxhdpi),对于确实适配有问题的资源再添加其他支持(hdpi),一般情况下足够了

  • 对于PNG图片,可以使用pngshrink或者pngquant来进行一下无损压缩,之后再放入工程。视觉给的图都能达到50%~70%的压缩率。
  • 使用混淆器
    一方面防小白反编译你的项目,虽然可能也没啥有技术含量的代码,但让人家那么容易就获得了你的全部源码,也还是挺闹心的(虽然,可能你的代码也都是Github+Google来的,哈哈哈);另外一方面就是混淆,特别是Android中最流行的ProGuard,能显著的减少目标dex的大小。

###网络流量
对于这点,其实优先级没那么高,现在Wifi覆盖越来越广,移动流量资费也越来越便宜,套餐越来越实惠,所以这些问题不必太纠结。

####对于更新时间比较长的要缓存到本地存储,以避免重复请求
这个其实也是提升响应速度的一个方式,对于更新周期比较长,且时效性要求不高的数据可以缓存在本地。客户端每隔一定时间更新一次。

####服务端主动推送更新通知
就是对于数据,客户端拿到后就缓存着,当数据有更新时服务端推送通知给客户端,然后客户端再来获取。这样即可以保证数据的更新到达,又可以减少不必要的网络请求。

####差分获取更新数据
当已经拿到了数据后,想要更新时,可以让服务端返回数据的差异,而不是返回整个数据,客户端拿到数据后再做融合。

####无论是请求还是服务器返回,没有用的参数不要带上

####使用压缩技术请求加上”Accept-Encoding”=gzip, deflate
无论是上传文件还是下载文件尽可能压缩一下,即使不为了省流量,也能提升些响应速度。当然这个需要服务端配合,如果无法控制服务端就没有办法了。

####对于要下载,事先判断网络类型,并给予提示,让用户来选择
相对于上面几点,这点倒是要注意,比如更新,或者下载插件,要判断网络类型,如果是移动网络,给出提示,让用户自己来判断。

##参考资料

原文 http://toughcoder.net/blog/2015/09/11/android-performance-profiling-made-easy/

Contents
,