Android应用性能剖析全攻略

原文链接 Android应用性能剖析全攻略

全文超万字,阅读时间约40分钟,TL;DR。

性能是软件质量的一个重要方面,好的软件必须要在性能上达到一定的标准。对于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,布局就变成了:

xml 复制代码
<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,举个例子:

java 复制代码
Data fetchNewsDetail(String url) {
    if (url is invalid) {
        return empty;
    }
    if (no networks) {
        return empty;
    }
    if (some other bad conditions) {
        return empty;
    }
    send requset;
    if (response code not 200) {
        return;
    }
    if (no response) {
        return;
    }
    if (parse response failed) {
        return;
    }
    return parse data;
}

而不是这样:

java 复制代码
// Ugly code, DO NOT do this
Data fetchNewsDetail(String url) {
    if (url valid) {
        if (has networks) {
            if (response code 200) {
                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. 执行以下命令
    shell 复制代码
    $ 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. 执行命令
    shell 复制代码
    $ 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> <freed>, <free memory>, <time>

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

      其中reason又有几个:

      • GC_CONCURRENT
      • GC_MALLOC
      • GC_EXPLICT
      • GC_BEFORE_OOM
    • ART

      ART虚拟机的GC格式比Dalvik要详细一些:

      I/art: <GC_Reason><Amount_freed>,<LOS_Space_Status>,<Heap_stats>,<Pause_time>,<Total_time>

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

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

CPU

减少忙等待

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

java 复制代码
// DO NOT do this
while (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

无论是上传文件还是下载文件尽可能压缩一下,即使不为了省流量,也能提升些响应速度。当然这个需要服务端配合,如果无法控制服务端就没有办法了。

对于要下载,事先判断网络类型,并给予提示,让用户来选择

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

参考资料

原创不易,打赏点赞在看收藏分享 总要有一个吧

相关推荐
言兴几秒前
从输入 URL 到页面显示:深入理解浏览器缓存机制
前端·javascript·面试
言兴2 分钟前
面试题之解析“类组件”与“组件”的本质
前端·javascript·面试
cxyxiaokui0015 分钟前
Exception和Error:一场JVM内部的“家庭伦理剧”
后端·面试
南篱6 分钟前
React 受控 vs 非受控组件:核心概念解析
前端·面试
新子y2 小时前
【操作记录】我的 MNN Android LLM 编译学习笔记记录(一)
android·学习·mnn
lincats3 小时前
一步一步学习使用FireMonkey动画(1) 使用动画组件为窗体添加动态效果
android·ide·delphi·livebindings·delphi 12.3·firemonkey
想想吴4 小时前
Android.bp 基础
android·安卓·android.bp
写点啥呢11 小时前
Android为ijkplayer设置音频发音类型usage
android·音视频·usage·mediaplayer·jikplayer
C4程序员12 小时前
北京JAVA基础面试30天打卡14
java·开发语言·面试
秋名山码民12 小时前
面试压力测试破解:如何从容应对棘手问题与挑战
面试·职场和发展·压力测试