其实有点不想写这篇文章的,但是又想写,有些矛盾。不想写的原因是随便上网一搜一堆关于性能的建议,感觉大家你一总结、我一总结的都说到了很多优化注意事项,但是看过这些文章后大多数存在一个问题就是只给出啥啥啥不能用,啥啥啥该咋用等,却很少有较为系统的进行真正性能案例分析的,大多数都是嘴上喊喊或者死记住规则而已(当然了,这话我自己听着都有些刺耳,实在不好意思,其实关于性能优化的优质博文网上也还是有很多的,譬如google官方都已经推出了优化专题,我这里只是总结下自的感悟而已,若有得罪欢迎拍砖,我愿挨打,因为我之前工作的一半时间都是负责性能优化)。
android应用的性能问题其实可以划分为几个大的模块的,而且都具有相对不错的优化调试技巧,下面我们就会依据一个项目常规开发的大类型来进行一些分析讲解。
ps:之前呆过一家初创医疗互联网公司,别提性能优化了,老板立完新项目后一个月就要求见到上线成品,这种压迫下谈何性能优化,纯属扯蛋,所以不到三个月时间我主动选择撤了,这种现象后来我一打听发现在很多初创公司都很严重,都想速成却忽略了体验。
ppps:本文只是达到抛砖引玉的作用,很多东西细究下去都是值得深入研究的,再加上性能优化本来就是一个需要综合考量的任务,不是说会了本文哪一点就能做性能分析了,需要面面俱到才可高效定位问题原因。
ui可谓是一个应用的脸,所以每一款应用在开发阶段我们的交互、视觉、动画工程师都拼命的想让它变得自然大方美丽,可是现实总是不尽人意,动画和交互总会觉得开发做出来的应用用上去感觉不自然,没有达到他们心目中的自然流畅细节;这种情况之下就更别提发布给终端用户使用了,用户要是能够感觉出来,少则影响心情,多则卸载应用;所以一个应用的ui显示性能问题就不得不被开发人员重视。
人类大脑与眼睛对一个画面的连贯性感知其实是有一个界限的,譬如我们看电影会觉得画面很自然连贯(帧率为24fps),用手机当然也需要感知屏幕操作的连贯性(尤其是动画过度),所以android索性就把达到这种流畅的帧率规定为60fps。
有了上面的背景,我们开发app的帧率性能目标就是保持在60fps,也就是说我们在进行app性能优化时心中要有如下准则:
从上面可以看出来,所谓的卡顿其实是可以量化的,每次是否能够成功渲染是非常重要的问题,16ms能否完整的做完一次操作直接决定了卡顿性能问题。
当然了,针对android系统的设计我们还需要知道另一个常识;虚拟机在执行gc垃圾回收操作时所有线程(包括ui线程)都需要暂停,当gc垃圾回收完成之后所有线程才能够继续执行(这个细节下面小节会有详细介绍)。也就是说当在16ms内进行渲染等操作时如果刚好遇上大量gc操作则会导致渲染时间明显不足,也就从而导致了丢帧卡顿问题。
有了上面这两个简单的理论基础之后我们下面就会探讨一些ui卡顿的原因分析及解决方案。
我们在使用app时会发现有些界面启动卡顿、动画不流畅、列表等滑动时也会卡顿,究其原因,很多都是丢帧导致的;通过上面卡顿原理的简单说明我们从应用开发的角度往回推理可以得出常见卡顿原因,如下:
人为在ui线程中做轻微耗时操作,导致ui线程卡顿;
布局layout过于复杂,无法在16ms内完成渲染;
同一时间动画执行的次数过多,导致cpu或gpu负载过重;
view过度绘制,导致某些像素在同一帧时间内被绘制多次,从而使cpu或gpu负载过重;
view频繁的触发measure、layout,导致measure、layout累计耗时过多及整个view频繁的重新渲染;
内存频繁触发gc过多(同一帧中频繁创建内存),导致暂时阻塞渲染操作;
冗余资源及逻辑等导致加载和执行缓慢;
臭名昭著的anr;
可以看见,上面这些导致卡顿的原因都是我们平时开发中非常常见的。有些人可能会觉得自己的应用用着还蛮ok的,其实那是因为你没进行一些瞬时测试和压力测试,一旦在这种环境下运行你的app你就会发现很多性能问题。
分析ui卡顿我们一般都借助工具,通过工具一般都可以直观的分析出问题原因,从而反推寻求优化方案,具体如下细说各种强大的工具。
我们可以通过sdk提供的工具hierarchyviewer来进行ui布局复杂程度及冗余等分析,如下:
选中一个window界面item,然后点击右上方hierarchy window或者pixel perfect window(这里不介绍,主要用来检查像素属性的)即可操作。
先看下hierarchy window,如下:
一个activity的view树,通过这个树可以分析出view嵌套的冗余层级,左下角可以输入view的id直接自动跳转到中间显示;save as png用来把左侧树保存为一张图片;capture layers用来保存psd的photoshop分层素材;右侧剧中显示选中view的当前属性状态;右下角显示当前view在activity中的位置等;左下角三个进行切换;load view hierarchy用来手动刷新变化(不会自动刷新的)。当我们选择一个view后会如下图所示:
类似上图可以很方便的查看到当前view的许多信息;上图最底那三个彩色原点代表了当前view的性能指标,从左到右依次代表测量、布局、绘制的渲染时间,红色和黄色的点代表速度渲染较慢的view(当然了,有些时候较慢不代表有问题,譬如viewgroup子节点越多、结构越复杂,性能就越差)。
当然了,在自定义view的性能调试时,hierarchyviewer上面的invalidate layout和requestlayout按钮的功能更加强大,它可以帮助我们debug自定义view执行invalidate()和requestlayout()过程,我们只需要在代码的相关地方打上断点就行了,接下来通过它观察绘制即可。
可以发现,有了hierarchyviewer调试工具,我们的ui性能分析变得十分容易,这个工具也是我们开发中调试ui的利器,在平时写代码时会时常伴随我们左右。
我们对于ui性能的优化还可以通过开发者选项中的gpu过度绘制工具来进行分析。在设置->开发者选项->调试gpu过度绘制(不同设备可能位置或者叫法不同)中打开调试后可以看见如下图(对settings当前界面过度绘制进行分析):
可以发现,开启后在我们想要调试的应用界面中可以看到各种颜色的区域,具体含义如下:
颜色
含义
无色
webview等的渲染区域
蓝色
1x过度绘制
绿色
2x过度绘制
淡红色
3x过度绘制
红色
4x(+)过度绘制
由于过度绘制指在屏幕的一个像素上绘制多次(譬如一个设置了背景色的textview就会被绘制两次,一次背景一次文本;这里需要强调的是activity设置的theme主题的背景不被算在过度绘制层级中),所以最理想的就是绘制一次,也就是蓝色(当然这在很多绚丽的界面是不现实的,所以大家有个度即可,我们的开发性能优化标准要求最极端界面下红色区域不能长期持续超过屏幕三分之一,可见还是比较宽松的规定),因此我们需要依据此颜色分布进行代码优化,譬如优化布局层级、减少没必要的背景、暂时不显示的view设置为gone而不是invisible、自定义view的ondraw方法设置canvas.cliprect()指定绘制区域或通过canvas.quickreject()减少绘制区域等。
android界面流畅度除过视觉感知以外是可以考核的(测试妹子专用),常见的方法就是通过gpu呈现模式图或者实时fps显示进行考核,这里我们主要针对gpu呈现模式图进行下说明,因为fps考核测试方法有很多(譬如自己写代码实现、第三方app测试、固件支持等),所以不做统一说明。
通过开发者选项中gpu呈现模式图工具来进行流畅度考量的流程是(注意:如果是在开启应用后才开启此功能,记得先把应用结束后重新启动)在设置->开发者选项->gpu呈现模式(不同设备可能位置或者叫法不同)中打开调试后可以看见如下图(对settings当前界面上下滑动列表后的图表):
当然,也可以在执行完ui滑动操作后在命令行输入如下命令查看命令行打印的gpu渲染数据(分析依据:draw + process + execute = 完整的显示一帧时间 < 16ms):
打开上图可视化工具后,我们可以在手机画面上看到丰富的gpu绘制图形信息,分别展示了statusbar、navgationbar、activity区域等的gpu渲染时间信息,随着界面的刷新,界面上会以实时柱状图来显示每帧的渲染时间,柱状图越高表示渲染时间越长,每个柱状图偏上都有一根代表16ms基准的绿色横线,每一条竖着的柱状线都包含三部分(蓝色代表测量绘制display list的时间,红色代表opengl渲染display list所需要的时间,黄色代表cpu等待gpu处理的时间),只要我们每一帧的总时间低于基准线就不会发生ui卡顿问题(个别超出基准线其实也不算啥问题的)。
可以发现,这个工具是有局限性的,他虽然能够看出来有帧耗时超过基准线导致了丢帧卡顿,但却分析不到造成丢帧的具体原因。所以说为了配合解决分析ui丢帧卡顿问题我们还需要借助traceview和systrace来进行原因追踪,下面我们会介绍这两种工具的。
上面说了,冗余资源及逻辑等也可能会导致加载和执行缓慢,所以我们就来看看lint这个工具是如何发现优化这些问题的(当然了,lint实际的功能是非常强大的,我们开发中也是经常使用它来发现一些问题的,这里主要有点针对ui性能的说明了,其他的雷同)。
在android studio 1.4版本中使用lint最简单的办法就是将鼠标放在代码区点击右键->analyze->inspect code–>界面选择你要检测的模块->点击确认开始检测,等待一下后会发现如下结果:
可以看见,lint检测完后给了我们很多建议的,我们重点看一个关于ui性能的检测结果;上图中高亮的那一行明确说明了存在冗余的ui层级嵌套,所以我们是可以点击跳进去进行优化处理掉的。
当然了,lint还有很多功能,大家可以自行探索发挥,这里只是达到抛砖引玉的作用。
关于android的内存管理机制下面的一节会详细介绍,这里我们主要针对gc导致的ui卡顿问题进行详细说明。
android系统会依据内存中不同的内存数据类型分别执行不同的gc操作,常见应用开发中导致gc频繁执行的原因主要可能是因为短时间内有大量频繁的对象创建与释放操作,也就是俗称的内存抖动现象,或者短时间内已经存在大量内存暂用介于阈值边缘,接着每当有新对象创建时都会导致超越阈值触发gc操作。
如下是我工作中一个项目的一次经历(我将代码回退特意抓取的),出现这个问题的场景是一次压力测试导致整个系统卡顿,瞬间杀掉应用就ok了,究其原因最终查到是一个api的调运位置写错了方式,导致一直被狂调,当普通使用时不会有问题,压力测试必现卡顿。具体内存参考图如下:
与此抖动图对应的logcat抓取如下:
我们知道,类似上面logcat打印一样,触发垃圾回收的主要原因有以下几种:
gc_malloc——内存分配失败时触发;
gc_concurrent——当分配的对象大小超过一个限定值(不同系统)时触发;
gc_explicit——对垃圾收集的显式调用(system.gc()) ;
gc_external_alloc——外部内存分配失败时触发;
可以看见,这种不停的大面积打印gc导致所有线程暂停的操作必定会导致ui视觉的卡顿,所以我们要避免此类问题的出现,具体的常见优化方式如下:
检查代码,尽量避免有些频繁触发的逻辑方法中存在大量对象分配;
尽量避免在多次for循环中频繁分配对象;
避免在自定义view的ondraw()方法中执行复杂的操作及创建对象(譬如paint的实例化操作不要写在ondraw()方法中等);
对于并发下载等类似逻辑的实现尽量避免多次创建线程对象,而是交给线程池处理。
当然了,有了上面说明gc导致的性能后我们就该定位分析问题了,可以通过运行ddms->allocation tracker标签打开一个新窗口,然后点击start tracing按钮,接着运行你想分析的代码,运行完毕后点击get allocations按钮就能够看见一个已分配对象的列表,如下:
点击上面第一个表格中的任何一项就能够在第二个表格中看见导致该内存分配的栈信息,通过这个工具我们可以很方便的知道代码分配了哪类对象、在哪个线程、哪个类、哪个文件的哪一行。譬如我们可以通过allocation tracker分别做一次paint对象实例化在ondraw与构造方法的一个自定义view的内存跟踪,然后你就明白这个工具的强大了。
ps一句,android studio新版本除过ddms以外在memory视图的左侧已经集成了allocation tracker功能,只是用起来还是没有ddms的方便实用,如下图:
关于ui卡顿问题我们还可以通过运行traceview工具进行分析,他是一个分析器,记录了应用程序中每个函数的执行时间;我们可以打开ddms然后选择一个进程,接着点击上面的“start method profiling”按钮(红色小点变为黑色即开始运行),然后操作我们的卡顿ui(小范围测试,所以操作最好不要超过5s),完事再点一下刚才按的那个按钮,稍等片刻即可出现下图,如下:
花花绿绿的一幅图我们怎么分析呢?下面我们解释下如何通过该工具定位问题:
整个界面包括上下两部分,上面是你测试的进程中每个线程运行的时间线,下面是每个方法(包含parent及child)执行的各个指标的值。通过上图的时间面板可以直观发现,整个trace时间段main线程做的事情特别多,其他的做的相对较少。当我们选择上面的一个线程后可以发现下面的性能面板很复杂,其实这才是traceview的核心图表,它主要展示了线程中各个方法的调用信息(cpu使用时间、调用次数等),这些信息就是我们分析ui性能卡顿的核心关注点,所以我们先看几个重要的属性说明,如下:
属性名
name
线程中调运的方法名;
incl cpu time
当前方法(包含内部调运的子方法)执行占用的cpu时间;
excl cpu time
当前方法(不包含内部调运的子方法)执行占用的cpu时间;
incl real time
当前方法(包含内部调运的子方法)执行的真实时间,ms单位;
excl real time
当前方法(不包含内部调运的子方法)执行的真实时间,ms单位;
calls+recur calls/total
当前方法被调运的次数及递归调运占总调运次数百分比;
cpu time/call
当前方法调运cpu时间与调运次数比,即当前方法平均执行cpu耗时时间;
real time/call
当前方法调运真实时间与调运次数比,即当前方法平均执行真实耗时时间;(重点关注)
有了对上面traceview图表的一个认识之后我们就来看看具体导致ui性能后该如何切入分析,一般traceview可以定位两类性能问题:
方法调运一次需要耗费很长时间导致卡顿;
方法调运一次耗时不长,但被频繁调运导致累计时长卡顿。
譬如我们来举个实例,有时候我们写完app在使用时不觉得有啥大的影响,但是当我们启动完app后静止在那却十分费电或者导致设备发热,这种情况我们就可以打开traceview然后按照cpu time/call或者real time/call进行降序排列,然后打开可疑的方法及其child进行分析查看,然后再回到代码定位检查逻辑优化即可;当然了,我们也可以通过该工具来trace我们自定义view的一些方法来权衡性能问题,这里不再一一列举喽。
可以看见,traceview能够帮助我们分析程序性能,已经很方便了,然而traceview家族还有一个更加直观强大的小工具,那就是可以通过dmtracedump生成方法调用图。具体做法如下:
通过这个生成的方法调运图我们可以更加直观的发现一些方法的调运异常现象。不过本人优化到现在还没怎么用到它,每次用到traceview分析就已经搞定问题了,所以说dmtracedump自己酌情使用吧。
systrace其实有些类似traceview,它是对整个系统进行分析(同一时间轴包含应用及surfaceflinger、windowmanagerservice等模块、服务运行信息),不过这个工具需要你的设备内核支持trace(命令行检查/sys/kernel/debug/tracing)且设备是eng或userdebug版本才可以,所以使用前麻烦自己确认一下。
我们在分析ui性能时一般只关注图形性能(所以必须选择graphics和view,其他随意),同时一般对于卡顿的抓取都是5s,最多10s。启动systrace进行数据抓取可以通过两种方式,命令行方式如下:
图形模式:
打开ddms->capture system wide trace using android systrace->设置时间与选项点击ok就开始了抓取,接着操作app,完事生成一个trace.html文件,用chrome打开即可如下图:
在chrome中浏览分析该文件我们可以通过键盘的w-a-s-d键来搞定,由于上面我们在进行trace时选择了一些选项,所以上图生成了左上方相关的cpu频率、负载、状态等信息,其中的cpu n代表了cpu核数,每个cpu行的柱状图表代表了当前时间段当前核上的运行信息;下面我们再来看看surfaceflinger的解释,如下:
可以看见上面左边栏的surfaceflinger其实就是负责绘制android程序ui的服务,所以surfaceflinger能反应出整体绘制情况,可以关注上图vsync-app一行可以发现前5s多基本都能够达到16ms刷新间隔,5s多开始到7s多大于了15ms,说明此时存在绘制丢帧卡顿;同时可以发现surfaceflinger一行明显存在类似不规律间隔,这是因为有的地方是不需要重新渲染ui,所以有大范围不规律,有的是因为阻塞导致不规律,明显可以发现0到4s间大多是不需要渲染,而5s以后大多是阻塞导致;对应这个时间点我们放大可以看到每个部分所使用的时间和正在执行的任务,具体如下:
可以发现具体的执行明显存在超时性能卡顿(原点不是绿色的基本都代表存在一定问题,下面和右侧都会提示你选择的帧相关详细信息或者alert信息),但是遗憾的是通过systrace只能大体上发现是否存在性能问题,具体问题还需要通过traceview或者代码中嵌入trace工具类等去继续详细分析,总之很蛋疼。
ps:如果你想使用systrace很轻松的分析定位所有问题,看明白所有的行含义,你还需要具备非常扎实的android系统框架的原理才可以将该工具使用的得心应手。
anr(application not responding)是android中ams与wms监测应用响应超时的表现;之所以把臭名昭著的anr单独作为ui性能卡顿的分析来说明是因为anr是直接卡死ui不动且必须要解掉的bug,我们必须尽量在开发时避免他的出现,当然了,万一出现了那就用下面介绍的方法来分析吧。
我们应用开发中常见的anr主要有如下几类:
按键触摸事件派发超时anr,一般阈值为5s(设置中开启anr弹窗,默认有事件派发才会触发弹框anr);
广播阻塞anr,一般阈值为10s(设置中开启anr弹窗,默认不弹框,只有log提示);
服务超时anr,一般阈值为20s(设置中开启anr弹窗,默认不弹框,只有log提示);
当anr发生时除过logcat可以看见的log以外我们还可以在系统指定目录下找到traces文件或dropbox文件进行分析,发生anr后我们可以通过如下命令得到anr trace文件:
然后我们用txt编辑器打开可以发现如下结构分析:
至此常见的应用开发中anr分析定位就可以解决了。
可以看见,关于android ui卡顿的性能分析还是有很多工具的,上面只是介绍了应用开发中我们经常使用的一些而已,还有一些其他的,譬如oprofile等工具不怎么常用,这里就不再详细介绍。
布局优化;尽量使用include、merge、viewstub标签,尽量不存在冗余嵌套及过于复杂布局(譬如10层就会直接异常),尽量使用gone替换invisible,使用weight后尽量将width和heigh设置为0dp减少运算,item存在非常复杂的嵌套时考虑使用自定义item view来取代,减少measure与layout次数等。
列表及adapter优化;尽量复用getview方法中的相关view,不重复获取实例导致卡顿,列表尽量在滑动过程中不进行ui元素刷新等。
背景和图片等内存分配优化;尽量减少不必要的背景设置,图片尽量压缩处理显示,尽量避免频繁内存抖动等问题出现。
自定义view等绘图与布局优化;尽量避免在draw、measure、layout中做过于耗时及耗内存操作,尤其是draw方法中,尽量减少draw、measure、layout等执行次数。
当然了,上面只是列出了我们项目中常见的一些ui性能注意事项而已,相信还有很多其他的情况这里没有说到,欢迎补充。还有一点就是我们上面所谓的ui性能优化分析总结等都是建议性的,因为性能这个问题是一个涉及面很广很泛的问题,有些优化不是必需的,有些优化是必需的,有些优化掉以后又是得不偿失的,所以我们一般着手解决那些必须的就可以了。
系统级内存管理:
android系统内核是基于linux,所以说android的内存管理其实也是linux的升级版而已。linux在进程停止后就结束该进程,而android把这些停止的进程都保留在内存中,直到系统需要更多内存时才选择性的释放一些,保留在内存中的进程默认(不包含后台service与thread等单独ui线程的进程)不会影响整体系统的性能(速度与电量等)且当再次启动这些保留在内存的进程时可以明显提高启动速度,不需要再去加载。
可以看见,所谓的我们的service在后台跑着跑着挂了,或者盒子上有些大型游戏启动起来就挂(之前我在上家公司做盒子时遇见过),有一个直接的原因就是这个阈值定义的太大,导致系统一直认为已经达到阈值,所以进行优先清除了符合类型的进程。所以说,该阈值的设定是有一些讲究的,额,扯多了,我们主要是针对应用层内存分析的,系统级内存回收了解这些就基本够解释我们应用在设备上的一些表现特征了。
应用级内存管理:
在说应用级别内存管理原理时大家先想一个问题,假设有一个内存为1g的android设备,上面运行了一个非常非常吃内存的应用,如果没有任何机制的情况下是不是用着用着整个设备会因为我们这个应用把1g内存吃光然后整个系统运行瘫痪呢?
哈哈,其实google的工程师才不会这么傻的把系统设计这么差劲。为了使系统不存在我们上面假想情况且能安全快速的运行,android的框架使得每个应用程序都运行在单独的进程中(这些应用进程都是由zygote进程孵化出来的,每个应用进程都对应自己唯一的虚拟机实例);如果应用在运行时再存在上面假想的情况,那么瘫痪的只会是自己的进程,不会直接影响系统运行及其他进程运行。
既然每个android应用程序都执行在自己的虚拟机中,那了解java的一定明白,每个虚拟机必定会有堆内存阈值限制(值得一提的是这个阈值一般都由厂商依据硬件配置及设备特性自己设定,没有统一标准,可以为64m,也可以为128m等;它的配置是在android的属性系统的/system/build.prop中配置dalvik.vm.heapsize=128m即可,若存在dalvik.vm.heapstartsize则表示初始申请大小),也即一个应用进程同时存在的对象必须小于阈值规定的内存大小才可以正常运行。
接着我们运行的app在自己的虚拟机中内存管理基本就是遵循java的内存管理机制了,系统在特定的情况下主动进行垃圾回收。但是要注意的一点就是在android系统中执行垃圾回收(gc)操作时所有线程(包含ui线程)都必须暂停,等垃圾回收操作完成之后其他线程才能继续运行。这些gc垃圾回收一般都会有明显的log打印出回收类型,常见的如下:
通过上面这几点的分析可以发现,应用的内存管理其实就是一个萝卜一个坑,坑都一般大,你在开发应用时要保证的是内存使用同一时刻不能超过坑的大小,否则就装不下了。
有了关于android的一些内存认识,接着我们来看看关于android应用开发中常出现的一种内存问题—-内存泄露。
众所周知,在java中有些对象的生命周期是有限的,当它们完成了特定的逻辑后将会被垃圾回收;但是,如果在对象的生命周期本来该被垃圾回收时这个对象还被别的对象所持有引用,那就会导致内存泄漏;这样的后果就是随着我们的应用被长时间使用,他所占用的内存越来越大。如下就是一个最常见简单的泄露例子(其它的泄露不再一一列举了):
可以看见,上面例子中我们让一个单例模式的对象持有了当前activity的强引用,那在当前acvitivy执行完ondestroy()后,这个activity就无法得到垃圾回收,也就造成了内存泄露。
内存泄露可以引发很多的问题,常见的内存泄露导致问题如下:
应用卡顿,响应速度慢(内存占用高时jvm虚拟机会频繁触发gc);
应用被从后台进程干为空进程(上面系统内存原理有介绍,也就是超过了阈值);
应用莫名的崩溃(上面应用内存原理有介绍,也就是超过了阈值oom);
造成内存泄露泄露的最核心原理就是一个对象持有了超过自己生命周期以外的对象强引用导致该对象无法被正常垃圾回收;可以发现,应用内存泄露是个相当棘手重要的问题,我们必须重视。
知道了内存泄露的概念之后肯定就是想办法来确认自己的项目是否存在内存泄露了,那该如何察觉自己项目是否存在内存泄露呢?如下提供了几种常用的方式:
察觉方式
场景
as的memory窗口
平时用来直观了解自己应用的全局内存情况,大的泄露才能有感知。
ddms-heap内存监测工具
同上,大的泄露才能有感知。
dumpsys meminfo命令
常用方式,可以很直观的察觉一些泄露,但不全面且常规足够用。
leakcanary神器
比较强大,可以感知泄露且定位泄露;实质是mat原理,只是更加自动化了,当现有代码量已经庞大成型,且无法很快察觉掌控全局代码时极力推荐;或者是偶现泄露的情况下极力推荐。
as的memory窗口如下,详细的说明这里就不解释了,很简单很直观(使用频率高):
ddms-heap内存监测工具窗口如下,详细的说明这里就不解释了,很简单(使用频率不高):
dumpsys meminfo命令如下(使用频率非常高,非常高效,我的最爱之一,平时一般关注几个重要的object个数即可判断一般的泄露;当然了,adb shell dumpsys meminfo不跟参数直接展示系统所有内存状态):
leakcanary神器使用这里先不说,下文会专题介绍,你会震撼的一b。有了这些工具的定位我们就能很方便的察觉我们app的内存泄露问题,察觉到以后该怎么定位分析呢,继续往下看。
leakcanary是一个开源项目,一个内存泄露自动检测工具,是著名的github开源组织square贡献的,它的主要优势就在于自动化过早的发觉内存泄露、配置简单、抓取贴心,缺点在于还存在一些bug,不过正常使用百分之九十情况是ok的,其核心原理与mat工具类似。
ps:之前在优化性能时发现我们有一个应用有两个界面退出后activity没有被回收(dumpsys meminfo发现一直在加),所以就怀疑可能存在内存泄露。但是问题来了,这两个activity的逻辑十分复杂,代码也不是我写的,相关联的代码量也十分庞大,更加郁闷的是很难判断是哪个版本修改导致的,这时候只知道有泄露,却无法定位具体原因,使用mat分析解决掉了一个可疑泄露后发现泄露又变成了概率性的。可以发现,对于这种概率性的泄露用mat去主动抓取肯定是很耗时耗力的,所以决定直接引入leakcanary神器来检测项目,后来很快就彻底解决了项目中所有必现的、偶现的内存泄露。
总之一点,工具再强大也只是帮我们定位可能的泄露点,而最核心的gc root泄露信息推导出泄露问题及如何解决还是需要你把住代码逻辑及泄露核心概念去推理解决。
ps:这是开发中使用频率非常高的一个工具之一,麻烦务必掌握其核心使用技巧,虽然android studio已经实现了部分功能,但是真的很难用,遇到问题目前还是使用eclipse memory analysis tools吧。
原谅我该小节的放荡不羁!!!!(其实我是困了,呜呜!)
有了上面的原理及案例处理其实还不够,因为上面这些处理办法是补救的措施,我们正确的做法应该是在开发过程中就养成良好的习惯和敏锐的嗅觉才对,所以下面给出一些应用开发中常见的规避内存泄露建议:
context使用不当造成内存泄露;不要对一个activity context保持长生命周期的引用(譬如上面概念部分给出的示例)。尽量在一切可以使用应用applicationcontext代替context的地方进行替换(原理我前面有一篇关于context的文章有解释)。
非静态内部类的静态实例容易造成内存泄漏;即一个类中如果你不能够控制它其中内部类的生命周期(譬如activity中的一些特殊handler等),则尽量使用静态类和弱引用来处理(譬如viewroot的实现)。
警惕线程未终止造成的内存泄露;譬如在activity中关联了一个生命周期超过activity的thread,在退出activity时切记结束线程。一个典型的例子就是handlerthread的run方法是一个死循环,它不会自己结束,线程的生命周期超过了activity生命周期,我们必须手动在activity的销毁方法中中调运thread.getlooper().quit();才不会泄露。
对象的注册与反注册没有成对出现造成的内存泄露;譬如注册广播接收器、注册观察者(典型的譬如数据库的监听)等。
创建与关闭没有成对出现造成的泄露;譬如cursor资源必须手动关闭,webview必须手动销毁,流等对象必须手动关闭等。
不要在执行频率很高的方法或者循环中创建对象,可以使用hashtable等创建一组对象容器从容器中取那些对象,而不用每次new与释放。
避免代码设计模式的错误造成内存泄露;譬如循环引用,a持有b,b持有c,c持有a,这样的设计谁都得不到释放。
关于规避内存泄露上面我只是列出了我在项目中经常遇见的一些情况而已,肯定不全面,欢迎拍砖!当然了,只有我们做到好的规避加上强有力的判断嗅觉泄露才能让我们的应用驾驭好自己的一亩三分地。
上面谈论了android应用开发的内存泄露,下面谈谈内存溢出(oom);其实可以认为内存溢出与内存泄露是交集关系,具体如下图:
下面我们就来看看内存溢出(oom)相关的东东吧。
上面我们探讨了android内存管理和应用开发中的内存泄露问题,可以知道内存泄露一般影响就是导致应用卡顿,但是极端的影响是使应用挂掉。前面也提到过应用的内存分配是有一个阈值的,超过阈值就会出问题,这里我们就来看看这个问题—–内存溢出(oom–outofmemoryerror)。
内存溢出的主要导致原因有如下几类:
应用代码存在内存泄露,长时间积累无法释放导致oom;
应用的某些逻辑操作疯狂的消耗掉大量内存(譬如加载一张不经过处理的超大超高清图片等)导致超过阈值oom;
可以发现,无论哪种类型,导致内存溢出(outofmemoryerror)的核心原因就是应用的内存超过阈值了。
通过上面的oom概念和那幅交集图可以发现,要想分析oom原因和避免oom需要分两种情况考虑,泄露导致的oom,申请过大导致的oom。
内存泄露导致的oom分析:
这种oom一旦发生后会在logcat中打印相关outofmemoryerror的异常栈信息,不过你别高兴太早,这种情况下导致的oom打印异常信息是没有太大作用,因为这种oom的导致一般都如下图情况(图示为了说明问题数据和场景有夸张,请忽略):
从图片可以看见,这种oom我们有时也遇到,第一反应是去分析oom异常打印栈,可是后来发现打印栈打印的地方没有啥问题,没有可优化的余地了,于是就郁闷了。其实这时候你留心观察几个现象即可,如下:
留意你执行触发oom操作前的界面是否有卡顿或者比较密集的gc打印;
使用命令查看下当前应用占用内存情况;
确认了以上这些现象你基本可以断定该oom的log真的没用,真正导致问题的原因是内存泄露,所以我们应该按照上节介绍的方式去着手排查内存泄露问题,解决掉内存泄露后红色空间都能得到释放,再去显示一张0.8m的优化图片就不会再报oom异常了。
不珍惜内存导致的oom分析:
上面说了内存泄露导致的oom异常,下面我们再来看一幅图(数据和场景描述有夸张,请忽略),如下:
可见,这种类型的oom就很好定位原因了,一般都可以从oom后的log中得出分析定位。
如下例子,我们在activity中的imageview放置一张未优化的特大的(30多m)高清图片,运行直接崩溃如下:
通过上面的log可以很方便的看出来问题原因所在地,那接下来的做法就是优化呗,降低图片的相关规格即可(譬如使用bitmapfactory的option类操作等)。
ps:提醒一句的是记得应用所属的内存是区分java堆和native堆的!
还是那句话,等待oom发生是为时已晚的事,我们应该将其扼杀于萌芽之中,至于如何在开发中规避oom,如下给出一些我们应用开发中的常用的策略建议:
优化界面交互过程中频繁的内存使用;譬如在列表等操作中只加载可见区域的bitmap、滑动时不加载、停止滑动后再开始加载。
有些地方避免使用强引用,替换为弱引用等操作。
避免各种内存泄露的存在导致oom。
对批量加载等操作进行缓存设计,譬如列表图片显示,adapter的convertview缓存等。
尽可能的复用资源;譬如系统本身有很多字符串、颜色、图片、动画、样式以及简单布局等资源可供我们直接使用,我们自己也要尽量复用style等资源达到节约内存。
对于有缓存等存在的应用尽量实现onlowmemory()和ontrimmemory()方法。
尽量使用线程池替代多线程操作,这样可以节约内存及cpu占用率。
尽量管理好自己的service、thread等后台的生命周期,不要浪费内存占用。
尽可能的不要使用依赖注入,中看不中用。
尽量在做一些大内存分配等可疑内存操作时进行try catch操作,避免不必要的应用闪退。
尽量的优化自己的代码,减少冗余,进行编译打包等优化对齐处理,避免类加载时浪费内存。
可以发现,上面只是列出了我们开发中常见的导致oom异常的一些规避原则,还有很多相信还没有列出来,大家可以自行追加参考即可。
无论是什么电子设备的开发,内存问题永远都是一个很深奥、无底洞的话题,上面的这些内存分析建议也单单只是android应用开发中一些常见的场景而已,真正的达到合理的优化还是需要很多知识和功底的。
合理的应用架构设计、设计风格选择、开源lib选择、代码逻辑规范等都会决定到应用的内存性能,我们必须时刻头脑清醒的意识到这些问题潜在的风险与优劣,因为内存优化必须要有一个度,不能一味的优化,亦不能置之不理。
在我们开发中除过常规的那些经典ui、内存性能问题外其实还存在很多潜在的性能优化、这种优化不是十分明显,但是在某些场景下却是非常有必要的,所以我们简单列举一些常见的其他潜在性能优化技巧,具体如下探讨。
字符串操作在android应用开发中是十分常见的操作,也就是这个最简单的字符串操作却也暗藏很多潜在的性能问题,下面我们实例来说说。
先看下面这个关于string和stringbuffer的对比例子:
通过这个例子可以看出来,string对象(记得是对象,不是常量)和stringbuffer对象的主要性能区别在于string对象是不可变的,所以每次对string对象做改变操作(譬如“+”操作)时其实都生成了新的string对象实例,所以会导致内存消耗性能问题;而stringbuffer对象做改变操作每次都会对自己进行操作,所以不需要消耗额外的内存空间。
我们再看一个关于string和stringbuffer的对比例子:
在这种情况下你会发现stringbuffer的性能反而没有string的好,原因是在jvm解释时认为
<code>string str = "name:" + "gjrs";</code>就是<code>string str = "name:gjrs";</code>,所以自然比stringbuffer快了。
可以发现,如果我们拼接的是字符串常量则string效率比stringbuffer高,如果拼接的是字符串对象,则stringbuffer比string效率高,我们在开发中要酌情选择。当然,除过注意stringbuffer和string的效率问题,我们还应该注意另一个问题,那就是stringbuffer和stringbuilder的区别,其实stringbuffer和stringbuilder都继承自同一个父类,只是stringbuffer是线程安全的,也就是说在不考虑多线程情况下stringbuilder的性能又比stringbuffer高。
ps:如果想追究清楚他们之间具体细节差异,麻烦自己查看实现源码即可。
ontrimmemory是android 4.0之后加入的一个回调方法,作用是通知应用在不同的情况下进行自身的内存释放,以避免被系统直接杀掉,提高应用程序的用户体验(冷启动速度是热启动的2~3倍)。系统会根据当前不同等级的内存使用情况调用这个方法,并且传入当前内存等级,这个等级有很多种,我们可以依据情况实现不同的等级,这里不详细介绍,但是要说的是我们应用应该至少实现如下等级:
trim_memory_background
内存已经很低了,系统准备开始根据lru缓存来清理进程。这时候如果我们手动释放一些不重要的缓存资源,则当用户返回我们应用时会感觉到很顺畅,而不是重新启动应用。
可以实现ontrimmemory方法的系统组件有application、activity、fragement、
service、contentprovider;关于ontrimmemory释放哪些内存其实在架构阶段就要考虑清楚哪些对象是要常驻内存的,哪些是伴随组件周期存在的,一般需要释放的都是缓存。
如下给出一个我们项目中常用的例子:
通常在我们代码实现了ontrimmemory后很难复显这种内存消耗场景,但是你又怕引入新bug,想想办法测试。好在我们有一个快捷的方式来模拟触发该水平内存释放,如下命令:
packagename为包名或者进程id,value为componentcallbacks2.java里面定义的值,可以为80、60、40、20、5等,我们模拟触发其中的等级即可。
在android开发中涉及到数据逻辑部分大部分用的都是java的api(譬如hashmap),但是对于android设备来说有些java的api并不适合,可能会导致系统性能下降,好在google团队已经意识到这些问题,所以他们针对android设备对java的一些api进行了优化,优化最多就是使用了arraymap及sparsearray替代hashmap来获得性能提升。
hashmap:
hashmap内部使用一个默认容量为16的数组来存储数据,数组中每一个元素存放一个链表的头结点,其实整个hashmap内部结构就是一个哈希表的拉链结构。hashmap默认实现的扩容是以2倍增加,且获取一个节点采用了遍历法,所以相对来说无论从内存消耗还是节点查找上都是十分昂贵的。
sparsearray:
sparsearray比hashmap省内存是因为它避免了对key进行自动装箱(int转integer),它内部是用两个数组来进行数据存储的(一个存key,一个存value),它内部对数据采用了压缩方式来表示稀疏数组数据,从而节约内存空间,而且其查找节点的实现采用了二分法,很明显可以看见性能的提升。
arraymap:
arraymap内部使用两个数组进行数据存储,一个记录key的hash值,一个记录value值,它和sparsearray类似,也会在查找时对key采用二分法。
有了上面的基本了解我们可以得出结论供开发时参考,当数据量不大(千位级内)且key为int类型时使用sparsearray替换hashmap效率高;当数据量不大(千位级内)且数据类型为map类型时使用arraymap替换hashmap效率高;其他情况下hashmap效率相对高于二者。
contentprovider是android应用开发的核心组件之一,有时候在开发中需要使用contentprovider对多行数据进行操作,我们的做法一般是多次调运相关操作方法,殊不知这种实现方式是非常低性能的,取而代之的做法应该是使用批量操作,具体为了使批量更新、插入、删除数据操作更加方便官方提供了contentprovideroperation工具类。所以在我们开发中遇到类似情景时请务必使用批量操作,具体的优势如下:
所有的操作都在一个事务中执行,可以保证数据的完整性。
批量操作在一个事务中执行,所以只用打开、关闭一个事务。
减轻应用程序与contentprovider间的多次频繁交互,提升性能。
可以看见,这对于数据库操作来说是一个非常有用的优化措施,烦请务必重视(我们项目优化过,的确有很大提升)。
关于api及逻辑性能优化其实有多知识点的,这里无法一一列出,只能给出一些重要的知识点,下面再给出一些常见的优化建议:
避免在android中使用java的枚举类型,因为编译后不但占空间,加载也费时,完全没有static final的变量好用、高效。
handler发送消息时尽量使用obtain去获取已经存在的message对象进行复用,而不是新new message对象,这样可以减轻内存压力。
在使用后台service时尽量将能够替换为intentservice的地方替换为此,这样可以减轻系统压力、省电、省内存、省cpu占用率。
在当前类内部尽量不要通过自己的getxxx、setxxx对自己内部成员进行操作,而是直接使用,这样可以提高代码执行效率。
不要一味的为了设计模式而过分的抽象代码,因为代码抽象系数与代码加载执行时间成正比。
尽量减少锁个数、减小锁范围,避免造成性能问题。
合理的选择使用for循环与增强型for循环,譬如不要在arraylist上使用增强型for循环等。
哎呀,类似的小优化技巧有很多,这里不一一列举了,自行发挥留意即可。
有了ui性能优化、内存性能优化、代码编写优化之后我们在来说说应用开发中很重要的一个优化模块—–电量优化。
在盒子等开发时可能电量优化不是特别重视(视盒子待机真假待机模式而定),但是在移动设备开发中耗电量是一个非常重要的指标,如果用户一旦发现我们的应用非常耗电,不好意思,他们大多会选择卸载来解决此类问题,所以耗电量是一个十分重要的问题。
其实我们一款应用耗电量最大的部分不是ui绘制显示等,常见耗电量最大原因基本都是因为网络数据交互、gps定位、大量内存性能问题、冗余的后台线程和service等造成。
优化电量使用情况我们不仅可以使用系统提供的一些api去处理,还可以在平时编写代码时就养成好的习惯。具体的一些建议如下:
在需要网络的应用中,执行某些操作前尽量先进行网络状态判断。
在网络应用传输中使用高效率的数据格式和解析方法,譬如json等。
在传输用户反馈或者下载ota升级包等不是十分紧急的操作时尽量采用压缩数据进行传输且延迟到设备充电和wifi状态时进行。
在有必要的情况下尽量通过powermanager.wakelock和jobscheduler来控制一些逻辑操作达到省电优化。
对定位要求不太高的场景尽量使用网络定位,而不是gps定位。
对于定时任务尽量使用alarmmanager,而不是sleep或者timer进行管理。
尽可能的减少网络请求次数和减小网络请求时间间隔。
后台任务要尽可能少的唤醒cpu,譬如im通信的长连接心跳时间间隔、一些应用的后台定时唤醒时间间隔等要设计合理。
特殊耗电业务情况可以进行弹窗等友好的交互设计提醒用户该操作会耗用过多电量。
可以看见,上面只是一些常见的电量消耗优化建议。总之,作为应用开发者的我们要意识到电量损耗对于用户来说是非常敏感的,只有我们做到合理的电量优化才能赢得用户的芳心。
性能优化是一个很大的话题,上面我们谈到的只是应用开发中常见的性能问题,也是应用开发中性能问题的冰山一角,更多的性能优化技巧和能力不是靠看出来,而是靠经验和实战结果总结出来的,所以说性能优化是一个涉及面非常广的话题,如果你想对你的应用进行性能你必须对你应用的整个框架有一个非常清晰的认识。
当然了,如果在我们开发中只是一味的追求各种极致的优化也是不对的。因为优化本来就是存在风险的,甚至有些过度的优化会直接导致项目的臃肿,所以不要因为极致的性能优化而破坏掉了你项目的合理架构。
总之一句话,性能优化适可而止,请酌情优化。