其实有点不想写这篇文章的,可是又想写,有些矛盾。不想写的缘由是随便上网一搜一堆关于性能的建议,感受你们你一总结、我一总结的都说到了不少优化注意事项,可是看过这些文章后大多数存在一个问题就是只给出啥啥啥不能用,啥啥啥该咋用等,却不多有较为系统的进行真正性能案例分析的,大多数都是嘴上喊喊或者死记住规则而已(固然了,这话我本身听着都有些刺耳,实在很差意思,其实关于性能优化的优质博文网上也仍是有不少的,譬如Google官方都已经推出了优化专题,我这里只是总结下自的感悟而已,如有得罪欢迎拍砖,我愿挨打,由于我工做的一半时间都是负责性能优化)。html
固然了,本文不会就此编辑这么一次,由于技术在发展,工具在强大(写着写着Android Studio 1.4版本都推送了),本身的经验也在增长,因此本文天然不会覆盖全部性能优化及分析;解决的办法就是该文章会长期维护更新,同时在评论区欢迎你关于性能优化点子的探讨。java
Android应用的性能问题其实能够划分为几个大的模块的,并且都具备相对不错的优化调试技巧,下面咱们就会依据一个项目常规开发的大类型来进行一些分析讲解。python
PS:以前呆过一家初创医疗互联网公司,别提性能优化了,老板立完新项目后一个月就要求见到上线成品,这种压迫下谈何性能优化,纯属扯蛋,因此不到三个月时间我主动选择撤了,这种现象后来我一打听发如今不少初创公司都很严重,都想速成却忽略了体验。android
PPPS:本文只是达到抛砖引玉的做用,不少东西细究下去都是值得深刻研究的,再加上性能优化原本就是一个须要综合考量的任务,不是说会了本文哪一点就能作性能分析了,须要面面俱到才可高效定位问题缘由。git
UI可谓是一个应用的脸,因此每一款应用在开发阶段咱们的交互、视觉、动画工程师都拼命的想让它变得天然大方美丽,但是现实老是不尽人意,动画和交互总会以为开发作出来的应用用上去感受不天然,没有达到他们心目中的天然流畅细节;这种状况之下就更别提发布给终端用户使用了,用户要是可以感受出来,少则影响心情,多则卸载应用;因此一个应用的UI显示性能问题就不得不被开发人员重视。github
人类大脑与眼睛对一个画面的连贯性感知实际上是有一个界限的,譬如咱们看电影会以为画面很天然连贯(帧率为24fps),用手机固然也须要感知屏幕操做的连贯性(尤为是动画过分),因此Android索性就把达到这种流畅的帧率规定为60fps。shell
有了上面的背景,咱们开发App的帧率性能目标就是保持在60fps,也就是说咱们在进行App性能优化时心中要有以下准则:数据库
1
2
3
|
换算关系:
60帧/秒-----------16ms/帧;
准则:尽可能保证每次在
16ms内处理完全部的CPU与GPU计算、绘制、渲染等操做,不然会形成丢帧卡顿问题。
|
从上面能够看出来,所谓的卡顿实际上是能够量化的,每次是否可以成功渲染是很是重要的问题,16ms可否完整的作完一次操做直接决定了卡顿性能问题。canvas
固然了,针对Android系统的设计咱们还须要知道另外一个常识;虚拟机在执行GC垃圾回收操做时全部线程(包括UI线程)都须要暂停,当GC垃圾回收完成以后全部线程才可以继续执行(这个细节下面小节会有详细介绍)。也就是说当在16ms内进行渲染等操做时若是恰好赶上大量GC操做则会致使渲染时间明显不足,也就从而致使了丢帧卡顿问题。设计模式
有了上面这两个简单的理论基础以后咱们下面就会探讨一些UI卡顿的缘由分析及解决方案。
咱们在使用App时会发现有些界面启动卡顿、动画不流畅、列表等滑动时也会卡顿,究其缘由,不少都是丢帧致使的;经过上面卡顿原理的简单说明咱们从应用开发的角度往回推理能够得出常见卡顿缘由,以下:
能够看见,上面这些致使卡顿的缘由都是咱们平时开发中很是常见的。有些人可能会以为本身的应用用着还蛮OK的,其实那是由于你没进行一些瞬时测试和压力测试,一旦在这种环境下运行你的App你就会发现不少性能问题。
分析UI卡顿咱们通常都借助工具,经过工具通常均可以直观的分析出问题缘由,从而反推寻求优化方案,具体以下细说各类强大的工具。
咱们能够经过SDK提供的工具HierarchyViewer来进行UI布局复杂程度及冗余等分析,以下:
1
|
xxx@ThinkPad:~$ hierarchyviewer //经过命令启动HierarchyViewer
|
选中一个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):
1
|
adb shell dumpsys gfxinfo [应用包名]
|
打开上图可视化工具后,咱们能够在手机画面上看到丰富的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的调运位置写错了方式,致使一直被狂调,当普通使用时不会有问题,压力测试必现卡顿。具体内存参考图以下:
1
2
3
4
5
6
7
|
//截取其中比较密集一段LogCat,与上图Memory检测到的抖动图对应,其中xxx为应用包名
......
10-06 00:59:45.619 xxx I/art: Explicit concurrent mark sweep GC freed 72515(3MB) AllocSpace objects, 65(2028KB) LOS objects, 80% free, 17MB/89MB, paused 3.505ms total 60.958ms
10-06 00:59:45.749 xxx I/art: Explicit concurrent mark sweep GC freed 5396(193KB) AllocSpace objects, 0(0B) LOS objects, 75% free, 23MB/95MB, paused 2.079ms total 100.522ms
......
10-06 00:59:48.059 xxx I/art: Explicit concurrent mark sweep GC freed 4693(172KB) AllocSpace objects, 0(0B) LOS objects, 75% free, 23MB/95MB, paused 2.227ms total 101.692ms
......
|
咱们知道,相似上面logcat打印同样,触发垃圾回收的主要缘由有如下几种:
能够看见,这种不停的大面积打印GC致使全部线程暂停的操做一定会致使UI视觉的卡顿,因此咱们要避免此类问题的出现,具体的常见优化方式以下:
固然了,有了上面说明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生成方法调用图。具体作法以下:
1
|
dmtracedump -g result.png target.trace //结果png文件 目标trace文件
|
经过这个生成的方法调运图咱们能够更加直观的发现一些方法的调运异常现象。不过本人优化到如今还没怎么用到它,每次用到Traceview分析就已经搞定问题了,因此说dmtracedump本身酌情使用吧。
PS一句,Android Studio新版本除过DDMS之外在CPU视图的左侧已经集成了Traceview(start Method Tracing)功能,只是用起来仍是没有DDMS的方便实用(这里有一篇AS MT我的以为不错的分析文章(引用自网络,连接属于原做者功劳)),以下图:
Systrace其实有些相似Traceview,它是对整个系统进行分析(同一时间轴包含应用及SurfaceFlinger、WindowManagerService等模块、服务运行信息),不过这个工具须要你的设备内核支持trace(命令行检查/sys/kernel/debug/tracing)且设备是eng或userdebug版本才能够,因此使用前麻烦本身确认一下。
咱们在分析UI性能时通常只关注图形性能(因此必须选择Graphics和View,其余随意),同时通常对于卡顿的抓取都是5s,最多10s。启动Systrace进行数据抓取能够经过两种方式,命令行方式以下:
1
|
python systrace.py --time=10 -o mynewtrace.html sched gfx view wm
|
图形模式:
打开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发生时除过logcat能够看见的log之外咱们还能够在系统指定目录下找到traces文件或dropbox文件进行分析,发生ANR后咱们能够经过以下命令获得ANR trace文件:
1
|
adb pull /data/anr/traces.txt ./
|
而后咱们用txt编辑器打开能够发现以下结构分析:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
//显示进程id、ANR发生时间点、ANR发生进程包名
----- pid 19073 at 2015-10-08 17:24:38 -----
Cmd line: com.example.yanbo.myapplication
//一些GC等object信息,一般能够忽略
......
//ANR方法堆栈打印信息!重点!
DALVIK THREADS (18):
"main" prio=5 tid=1 Sleeping
| group="main" sCount=1 dsCount=0 obj=0x7497dfb8 self=0x7f9d09a000
| sysTid=19073 nice=0 cgrp=default sched=0/0 handle=0x7fa106c0a8
| state=S schedstat=( 125271779 68162762 280 ) utm=11 stm=1 core=0 HZ=100
| stack=0x7fe90d3000-0x7fe90d5000 stackSize=8MB
| held mutexes=
at java.lang.Thread.sleep!(Native method)
- sleeping on <0x0a2ae345> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:1031)
- locked <0x0a2ae345> (a java.lang.Object)
//真正致使ANR的问题点,能够发现是onClick中有sleep致使。咱们平时能够类比分析便可,这里不详细说明。
at java.lang.Thread.sleep(Thread.java:985)
at com.example.yanbo.myapplication.MainActivity$1.onClick(MainActivity.java:21)
at android.view.View.performClick(View.java:4908)
at android.view.View$PerformClick.run(View.java:20389)
at android.os.Handler.handleCallback(Handler.java:815)
at android.os.Handler.dispatchMessage(Handler.java:104)
at android.os.Looper.loop(Looper.java:194)
at android.app.ActivityThread.main(ActivityThread.java:5743)
at java.lang.reflect.Method.invoke!(Native method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:988)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:783)
......
//省略一些不常关注堆栈打印
......
|
至此常见的应用开发中ANR分析定位就能够解决了。
能够看见,关于Android UI卡顿的性能分析仍是有不少工具的,上面只是介绍了应用开发中咱们常用的一些而已,还有一些其余的,譬如Oprofile等工具不怎么经常使用,这里就再也不详细介绍。
经过上面UI性能的原理、缘由、工具分析总结能够发现,咱们在开发应用时必定要时刻重视性能问题,如若真的没留意出现了性能问题,不妨使用上面的一些案例方式进行分析。可是那终归是补救措施,在咱们知道上面UI卡顿原理以后咱们应该尽可能从项目代码架构搭建及编写时就避免一些UI性能问题,具体项目中常见的注意事项以下:
固然了,上面只是列出了咱们项目中常见的一些UI性能注意事项而已,相信还有不少其余的状况这里没有说到,欢迎补充。还有一点就是咱们上面所谓的UI性能优化分析总结等都是建议性的,由于性能这个问题是一个涉及面很广很泛的问题,有些优化不是必需的,有些优化是必需的,有些优化掉之后又是得不偿失的,因此咱们通常着手解决那些必须的就能够了。
说完了应用开发中的UI性能问题后咱们就该来关注应用开发中的另外一个重要、严重、很是重要的性能问题了,那就是内存性能优化分析。Android其实就是嵌入式设备,嵌入式设备核心关注点之一就是内存资源;有人说如今的设备都在堆硬件配置(譬如国产某米的某兔跑分手机、盒子等),因此内存不会再像之前那么紧张了,其实这句话听着没错,但为啥再牛逼配置的Android设备上有些应用仍是越用系统越卡呢?这里面的缘由有不少,不过相信有了这一章下面的内容分析,做为一个移动开发者的你就有能力打理好本身应用的那一亩三分地内存了,能作到这样就足以了。关于Android内存优化,这里有一篇Google的官方指导文档,可是本文为本身项目摸索,会有不少不同的地方。
系统级内存管理:
Android系统内核是基于Linux,因此说Android的内存管理其实也是Linux的升级版而已。Linux在进程中止后就结束该进程,而Android把这些中止的进程都保留在内存中,直到系统须要更多内存时才选择性的释放一些,保留在内存中的进程默认(不包含后台service与Thread等单独UI线程的进程)不会影响总体系统的性能(速度与电量等)且当再次启动这些保留在内存的进程时能够明显提升启动速度,不须要再去加载。
再直白点就是说Android系统级内存管理机制其实相似于Java的垃圾回收机制,这下明白了吧;在Android系统中框架会定义以下几类进程、在系统内存达到规定的不一样level阈值时触发清空不一样level的进程类型。
能够看见,所谓的咱们的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中有些对象的生命周期是有限的,当它们完成了特定的逻辑后将会被垃圾回收;可是,若是在对象的生命周期原本该被垃圾回收时这个对象还被别的对象所持有引用,那就会致使内存泄漏;这样的后果就是随着咱们的应用被长时间使用,他所占用的内存愈来愈大。以下就是一个最多见简单的泄露例子(其它的泄露再也不一一列举了):
1
2
3
4
5
6
7
8
9
10
11
12
|
public final class MainActivity extends Activity {
private DbManager mDbManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//DbManager是一个单例模式类,这样就持有了MainActivity引用,致使泄露
mDbManager = DbManager.getInstance(this);
}
}
|
能够看见,上面例子中咱们让一个单例模式的对象持有了当前Activity的强引用,那在当前Acvitivy执行完onDestroy()后,这个Activity就没法获得垃圾回收,也就形成了内存泄露。
内存泄露能够引起不少的问题,常见的内存泄露致使问题以下:
形成内存泄露泄露的最核心原理就是一个对象持有了超过本身生命周期之外的对象强引用致使该对象没法被正常垃圾回收;能够发现,应用内存泄露是个至关棘手重要的问题,咱们必须重视。
知道了内存泄露的概念以后确定就是想办法来确认本身的项目是否存在内存泄露了,那该如何察觉本身项目是否存在内存泄露呢?以下提供了几种经常使用的方式:
察觉方式 | 场景 |
---|---|
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工具相似。
关于leakcanary工具的配置使用方式这里再也不详细介绍,由于真的很简单,详情点我参考官方教程学习使用便可。
PS:以前在优化性能时发现咱们有一个应用有两个界面退出后Activity没有被回收(dumpsys meminfo发现一直在加),因此就怀疑可能存在内存泄露。可是问题来了,这两个Activity的逻辑十分复杂,代码也不是我写的,相关联的代码量也十分庞大,更加郁闷的是很难判断是哪一个版本修改致使的,这时候只知道有泄露,却没法定位具体缘由,使用MAT分析解决掉了一个可疑泄露后发现泄露又变成了几率性的。能够发现,对于这种几率性的泄露用MAT去主动抓取确定是很耗时耗力的,因此决定直接引入leakcanary神器来检测项目,后来很快就完全解决了项目中全部必现的、偶现的内存泄露。
总之一点,工具再强大也只是帮咱们定位可能的泄露点,而最核心的GC ROOT泄露信息推导出泄露问题及如何解决仍是须要你把住代码逻辑及泄露核心概念去推理解决。
Eclipse Memory Analysis Tools(点我下载)是一个专门分析Java堆数据内存引用的工具,咱们能够使用它方便的定位内存泄露缘由,核心任务就是找到GC ROOT位置便可,哎呀,关于这个工具的使用我是真的不想说了,本身搜索吧,实在简单、传统的不行了。
PS:这是开发中使用频率很是高的一个工具之一,麻烦务必掌握其核心使用技巧,虽然Android Studio已经实现了部分功能,可是真的很难用,遇到问题目前仍是使用Eclipse Memory Analysis Tools吧。
原谅我该小节的放荡不羁!!!!(其实我是困了,呜呜!)
有了上面的原理及案例处理其实还不够,由于上面这些处理办法是补救的措施,咱们正确的作法应该是在开发过程当中就养成良好的习惯和敏锐的嗅觉才对,因此下面给出一些应用开发中常见的规避内存泄露建议:
关于规避内存泄露上面我只是列出了我在项目中常常碰见的一些状况而已,确定不全面,欢迎拍砖!固然了,只有咱们作到好的规避加上强有力的判断嗅觉泄露才能让咱们的应用驾驭好本身的一亩三分地。
上面谈论了Android应用开发的内存泄露,下面谈谈内存溢出(OOM);其实能够认为内存溢出与内存泄露是交集关系,具体以下图:
下面咱们就来看看内存溢出(OOM)相关的东东吧。
上面咱们探讨了Android内存管理和应用开发中的内存泄露问题,能够知道内存泄露通常影响就是致使应用卡顿,可是极端的影响是使应用挂掉。前面也提到过应用的内存分配是有一个阈值的,超过阈值就会出问题,这里咱们就来看看这个问题—–内存溢出(OOM–OutOfMemoryError)。
内存溢出的主要致使缘由有以下几类:
能够发现,不管哪一种类型,致使内存溢出(OutOfMemoryError)的核心缘由就是应用的内存超过阈值了。
经过上面的OOM概念和那幅交集图能够发现,要想分析OOM缘由和避免OOM须要分两种状况考虑,泄露致使的OOM,申请过大致使的OOM。
内存泄露致使的OOM分析:
这种OOM一旦发生后会在logcat中打印相关OutOfMemoryError的异常栈信息,不过你别高兴太早,这种状况下致使的OOM打印异常信息是没有太大做用,由于这种OOM的致使通常都以下图状况(图示为了说明问题数据和场景有夸张,请忽略):
从图片能够看见,这种OOM咱们有时也遇到,第一反应是去分析OOM异常打印栈,但是后来发现打印栈打印的地方没有啥问题,没有可优化的余地了,因而就郁闷了。其实这时候你留心观察几个现象便可,以下:
确认了以上这些现象你基本能够判定该OOM的log真的没用,真正致使问题的缘由是内存泄露,因此咱们应该按照上节介绍的方式去着手排查内存泄露问题,解决掉内存泄露后红色空间都能获得释放,再去显示一张0.8M的优化图片就不会再报OOM异常了。
不珍惜内存致使的OOM分析:
上面说了内存泄露致使的OOM异常,下面咱们再来看一幅图(数据和场景描述有夸张,请忽略),以下:
可见,这种类型的OOM就很好定位缘由了,通常均可以从OOM后的log中得出分析定位。
以下例子,咱们在Activity中的ImageView放置一张未优化的特大的(30多M)高清图片,运行直接崩溃以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
//抛出OOM异常
10-10 09:01:04.873 11703-11703/? E/art: Throwing OutOfMemoryError "Failed to allocate a 743620620 byte allocation with 4194208 free bytes and 239MB until OOM"
10-10 09:01:04.940 11703-11703/? E/art: Throwing OutOfMemoryError "Failed to allocate a 743620620 byte allocation with 4194208 free bytes and 239MB until OOM"
//堆栈打印
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: FATAL EXCEPTION: main
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: Process: com.example.application, PID: 11703
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.application/com.example.myapplication.MainActivity}: android.view.InflateException: Binary XML file line #21: Error inflating class <unknown>
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2610)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2684)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.app.ActivityThread.access$800(ActivityThread.java:177)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1542)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:111)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.os.Looper.loop(Looper.java:194)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:5743)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at java.lang.reflect.Method.invoke(Method.java:372)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:988)
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:783)
//出错地点,缘由是21行的ImageView设置的src是一张未优化的31M的高清图片
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: Caused by: android.view.InflateException: Binary XML file line #21: Error inflating class <unknown>
10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: at android.view.LayoutInflater.createView(LayoutInflater.java:633)
|
经过上面的log能够很方便的看出来问题缘由所在地,那接下来的作法就是优化呗,下降图片的相关规格便可(譬如使用BitmapFactory的Option类操做等)。
PS:提醒一句的是记得应用所属的内存是区分Java堆和native堆的!
仍是那句话,等待OOM发生是为时已晚的事,咱们应该将其扼杀于萌芽之中,至于如何在开发中规避OOM,以下给出一些咱们应用开发中的经常使用的策略建议:
能够发现,上面只是列出了咱们开发中常见的致使OOM异常的一些规避原则,还有不少相信尚未列出来,你们能够自行追加参考便可。
不管是什么电子设备的开发,内存问题永远都是一个很深奥、无底洞的话题,上面的这些内存分析建议也单单只是Android应用开发中一些常见的场景而已,真正的达到合理的优化仍是须要不少知识和功底的。
合理的应用架构设计、设计风格选择、开源Lib选择、代码逻辑规范等都会决定到应用的内存性能,咱们必须时刻头脑清醒的意识到这些问题潜在的风险与优劣,由于内存优化必需要有一个度,不能一味的优化,亦不能置之不理。
在咱们开发中除过常规的那些经典UI、内存性能问题外其实还存在不少潜在的性能优化、这种优化不是十分明显,可是在某些场景下倒是很是有必要的,因此咱们简单列举一些常见的其余潜在性能优化技巧,具体以下探讨。
字符串操做在Android应用开发中是十分常见的操做,也就是这个最简单的字符串操做却也暗藏不少潜在的性能问题,下面咱们实例来讲说。
先看下面这个关于String和StringBuffer的对比例子:
1
2
3
4
5
6
7
8
|
//性能差的实现
String str1 = "Name:";
String str2 = "GJRS";
String Str = str1 + str2;
//性能好的实现
String str1 = "Name:";
String str2 = "GJRS";
StringBuffer str = new StringBuilder().append(str1).append(str2);
|
经过这个例子能够看出来,String对象(记得是对象,不是常量)和StringBuffer对象的主要性能区别在于String对象是不可变的,因此每次对String对象作改变操做(譬如“+”操做)时其实都生成了新的String对象实例,因此会致使内存消耗性能问题;而StringBuffer对象作改变操做每次都会对本身进行操做,因此不须要消耗额外的内存空间。
咱们再看一个关于String和StringBuffer的对比例子:
1
2
3
4
|
//性能差的实现
StringBuffer str = new StringBuilder().append("Name:").append("GJRS");
//性能好的实现
String Str = "Name:" + "GJRS";
|
在这种状况下你会发现StringBuffer的性能反而没有String的好,缘由是在JVM解释时认为String Str = "Name:" + "GJRS";
就是String Str = "Name:GJRS";
,因此天然比StringBuffer快了。
能够发现,若是咱们拼接的是字符串常量则String效率比StringBuffer高,若是拼接的是字符串对象,则StringBuffer比String效率高,咱们在开发中要酌情选择。固然,除过注意StringBuffer和String的效率问题,咱们还应该注意另外一个问题,那就是StringBuffer和StringBuilder的区别,其实StringBuffer和StringBuilder都继承自同一个父类,只是StringBuffer是线程安全的,也就是说在不考虑多线程状况下StringBuilder的性能又比StringBuffer高。
PS:若是想追究清楚他们之间具体细节差别,麻烦本身查看实现源码便可。
OnTrimMemory是Android 4.0以后加入的一个回调方法,做用是通知应用在不一样的状况下进行自身的内存释放,以免被系统直接杀掉,提升应用程序的用户体验(冷启动速度是热启动的2~3倍)。系统会根据当前不一样等级的内存使用状况调用这个方法,而且传入当前内存等级,这个等级有不少种,咱们能够依据状况实现不一样的等级,这里不详细介绍,可是要说的是咱们应用应该至少实现以下等级:
能够实现OnTrimMemory方法的系统组件有Application、Activity、Fragement、
Service、ContentProvider;关于OnTrimMemory释放哪些内存其实在架构阶段就要考虑清楚哪些对象是要常驻内存的,哪些是伴随组件周期存在的,通常须要释放的都是缓存。
以下给出一个咱们项目中经常使用的例子:
1
2
3
4
5
6
|
@Override
public void onTrimMemory(int level) {
if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
clearCache();
}
}
|
一般在咱们代码实现了onTrimMemory后很难复显这种内存消耗场景,可是你又怕引入新Bug,想一想办法测试。好在咱们有一个快捷的方式来模拟触发该水平内存释放,以下命令:
1
|
adb shell dumpsys gfxinfo packagename -cmd trim value
|
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工具类。因此在咱们开发中遇到相似情景时请务必使用批量操做,具体的优点以下:
能够看见,这对于数据库操做来讲是一个很是有用的优化措施,烦请务必重视(咱们项目优化过,的确有很大提高)。
关于API及逻辑性能优化其实有多知识点的,这里没法一一列出,只能给出一些重要的知识点,下面再给出一些常见的优化建议:
哎呀,相似的小优化技巧有不少,这里不一一列举了,自行发挥留意便可。
有了UI性能优化、内存性能优化、代码编写优化以后咱们在来讲说应用开发中很重要的一个优化模块—–电量优化。
在盒子等开发时可能电量优化不是特别重视(视盒子待机真假待机模式而定),可是在移动设备开发中耗电量是一个很是重要的指标,若是用户一旦发现咱们的应用很是耗电,很差意思,他们大多会选择卸载来解决此类问题,因此耗电量是一个十分重要的问题。
关于咱们应用的耗电量状况咱们能够进行定长时间测试,至于具体的耗电量统计等请参考此文,同时咱们还能够直接经过Battery Historian Tool来查看详细的应用电量消耗状况。最简单经常使用办法是经过命令直接查看,以下:
1
|
adb shell dumpsys batterystats
|
其实咱们一款应用耗电量最大的部分不是UI绘制显示等,常见耗电量最大缘由基本都是由于网络数据交互、GPS定位、大量内存性能问题、冗余的后台线程和Service等形成。
优化电量使用状况咱们不只能够使用系统提供的一些API去处理,还能够在平时编写代码时就养成好的习惯。具体的一些建议以下:
能够看见,上面只是一些常见的电量消耗优化建议。总之,做为应用开发者的咱们要意识到电量损耗对于用户来讲是很是敏感的,只有咱们作到合理的电量优化才能赢得用户的芳心。
性能优化是一个很大的话题,上面咱们谈到的只是应用开发中常见的性能问题,也是应用开发中性能问题的冰山一角,更多的性能优化技巧和能力不是靠看出来,而是靠经验和实战结果总结出来的,因此说性能优化是一个涉及面很是广的话题,若是你想对你的应用进行性能你必须对你应用的整个框架有一个很是清晰的认识。
固然了,若是在咱们开发中只是一味的追求各类极致的优化也是不对的。由于优化原本就是存在风险的,甚至有些过分的优化会直接致使项目的臃肿,因此不要由于极致的性能优化而破坏掉了你项目的合理架构。
总之一句话,性能优化适可而止,请酌情优化。
PS:附上Google关于Android开发的一些专题建议视频连接,不过在天朝须要自备梯