周星驰的电影《功夫》里面借火云邪神之口说出了一句至理名言:“天下武功,唯快不破”。
在移动互联网时代,同样如此,如何把握住这个时机,迅速开发出产品,成为至关重要的一环。但是快速开发出来的产品代码运行的效率怎么样呢?我们的App 给用户的体验如何呢?
我们的App在低端机上经常ANR、闪退、卡顿等
我们的App在其他分辨率上显示惨不忍睹?
我们的App在不同网络的情况下如何处理的
…
我们的App体验如此之差,导致大量的用户流失。这些迫使我们认识到性能优化是非常重要,某种程度上甚至超过了新功能的开发。
也验证了一句话:“别人有的我们也有,而且比他们的要好要快。
UI性能问题分析优化
UI可谓是一个应用的脸,所以每一款应用在开发阶段我们的交互、视觉都拼命的想让它变得自然大方美丽,可是现实总是不尽人意,视觉和交互总会觉得开发做出来的应用用上去感觉不自然,没有达到他们心目中的自然流畅细节; 用户要是能够感觉出来,少则影响心情,多则卸载应用;所以一 个应用的UI显示性能问题就不得不被开发人员重视
Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,那么整个过程如果保证在16ms以内就能达到一个流畅的画面。如果系统发出了VSYNC信号,而此时无法进行渲染,还在做别的操作,那么就会导致丢帧的现象。这样的话,绘制就会在下一个16ms的时候才进行绘制,即使只丢一帧,用户也会发现卡顿的
所谓的卡顿其实是可以量化的,每次是否能够成功渲染是非常重要的问题,16ms能否完整的做完一次操作直接决定了UI卡顿与否。
这是因为人眼与大脑之间的协作无法感知超过60fps的画面更新。12fps大概类似手动快速翻动书籍的帧率,这明显是可以感知到不够顺滑的。24fps使得人眼感知的是连续线性的运动,这其实是归功于运动模糊的 效果。24fps是电影胶圈通常使用的帧率,因为这个帧率已经足够支撑大部分电影画面需要表达的内容,同时能够最大的减少费用支出。但是要顺畅表现绚丽的画面内容的,此时就需要用到60fps来达到想要的效果,当然超过60fps是没有必要的
VSync机制就像是一台转速固定的发动机(60转/s)。每一转会带动着去做一些UI相关的事情,但不是每一转都会有工作去做(就像有时在空挡,有时在D档)。有时候因为各种阻力某一圈工作量比较重超过了16.6ms,那么这台发动机这秒内就不是60转了,当然也有可能被其他因素影响,比如给油不足(主线程里干的活太多)等等,就会出现转速降低的状况。我们把这个转速叫做流畅度。
应用UI卡顿常见原因
我们在使用App时会发现有些界面启动卡顿、动画不流畅、列表等滑动时也会卡顿,究其原因,很多都是丢帧导致的;通过上面卡顿原理的简单说明,我们从应用开发的角度往回推理可以得出常见卡顿原因,如下:
人为在UI线程中做轻微耗时操作,导致UI线程卡顿;
布局Layout过于复杂,无法在16ms内完成渲染;
View过度绘制,导致某些像素在同一帧时间内被绘制多次,从而使CPU或GPU负载过重;
View频繁的触发measure、layout,导致measure、layout累计耗时过多及整个View频繁的重新渲染;
内存频繁触发GC过多(同一帧中频繁创建内存),导致暂时阻塞渲染操作;虚拟机在执行GC垃圾回收操作时所有线程(包括UI线程)都需要暂停,当GC垃圾回收完成之后所有线程才能够继续执行 也就是说当在16ms内进行渲染等操作时如果刚好遇上大量GC操作则会导致渲染时间明显不足,也就从而导致了丢帧卡顿问题。
冗余资源及逻辑等导致加载和执行缓慢;臭名昭著的ANR;
可以看见,上面这些导致卡顿的原因都是我们平时开发中非常常见的。有些人可能会觉得自己的应用用着还蛮OK的,其实那是因为你没进行一些瞬时测试和压力测试,一旦在这种环境下运行你的App你就会发现很多性能问题。
应用的主UI线程的概念及其重要性是每个Android开发者都应理解。当一个应用启动,系统会为应用创建一个名为“main”的主线程。这个主线程(也就是UI主线程)主要负责把事件分发给合适的view或者widget。例如,如果你点击了屏幕上的一个按钮,UI线程会把点击时间交给view处理,view接到事件后会设置它的pressed状态,然后向事件队列中发送一个invalidate请求。 UI线程会依次读取队列并且告诉view去重绘自己。
解决方法
分析UI卡顿我一般都借助工具,通过工具一般都可以直观的分析出问题原因,从而反推寻求优化方案,具体如下细说各种强大的工具。
- 使用GPU过绘分析UI过度绘制问题
如果我们粉刷过一个房间或一所房子,就会知道给墙壁涂上颜色需要做大量的工作。假如你还要重新粉刷一次的话,第二次粉刷的颜色会覆盖住第一次的颜色,第一次的颜色就永远不可见了,等于你第一次粉刷做的大量工作就完全被浪费掉。
同样的道理,如果在我们的应用程序中浪费精力去绘制一些东西同样会产生性能问题。过度绘制这个名词就是用来描述屏幕上一个像素在1帧中被重绘了多少次
过度绘制其实是一个性能和设计的交叉点。我们在设计上追求很华丽的视觉效果,但一般来说这种视觉效果会采用非常多的层叠组件来实现,这时候就会带来过度绘制的问题。
过度绘制也许是因为你的UI布局中存在大量重叠的view,但一个更为普遍的情况是因为那些不必要的重叠着的背景。例如某个Activity有一个背景,Layout也有自己的背景,同时它的子View又分别有自己的背景。
设置-开发者选项-调试GPU过度绘制(过度渲染等,不同机器可能不同)
开启后,启动我们的应用,可以看到各种颜色的区域,其中:
蓝色 1x过度绘制
绿色 2x过度绘制
淡红色 3x过度绘制
红色 超过4x过度绘制
最理想的是蓝色,一个像素只绘制一次。合格的页面绘制是白色、蓝色为主,颜色越浅越好。
过度绘制产生的原因:
太多重叠的背景
太多叠加的View,本来这个UI布局就很复杂或者你是为了追求一个炫丽的视觉效果,这都有可能使得很多view叠加在一起。
复杂的layout层级
我们可以根据这些原因来查找代码中存在的问题,并做出适当的修改
- Hierarchy Viewer
Hierarchy Viewer是随AndroidSDK发布的工具,位置在tools文件夹下,名为hierarchyviewer.bat。它是Android自带的非常有用而且使用简单的工具,可以帮助我们更好地检视和设计用户界面(UI),从可视化的角度直观地获得UI布局设计结构和各种属性的信息,帮助我们优化布局设计;
一个Activity的View树,通过这个树可以分析出View嵌套的冗余层级
Hierarchy Viewer是随AndroidSDK发布的工具,位置在tools文件夹下,名为hierarchyviewer.bat。它是Android自带的非常有用而且使用简单的工具,可以帮助我们更好地检视和设计用户界面(UI),从可视化的角度直观地获得UI布局设计结构和各种属性的信息,帮助我们优化布局设计;
类似上图可以很方便的查看到当前View的许多信息;上图最底那三个彩色原点代表了当前View的性能指标,从左到右依次代表测量、布局、绘制的渲染时间,红色和黄色的点代表速度渲染较慢的View(当然了,有些时候较慢不代表有问题,譬如ViewGroup子节点越多、结构越复杂,性能就越差)
- lint
作为移动应用开发者,我们总希望发布的apk文件越小越好,不希望资源文件没有用到的图片资源也被打包进apk,不希望应用中使用了高于minSdk的api,也不希望AndroidManifest文件存在异常,lint就能解决我们的这些问题。静态代码分析工具,无需运行,无需测试用例 扫描整个项目,分析以下潜在的问题,分类指出问题描述、问题位置,并提供合理的修改建议
Android lint是在ADT 16提供的新工具,它是一个代码扫描工具,能够帮助我们识别代码结构存在的问题,主要包括:
1)性能 布局性能(以前是 layoutopt工具,可以解决无用布局、嵌套太多、布局太多、overdraw) 其他性能(如:draw/layout 时进行对象的声明等)
2)未使用到资源、资源缺少(不同资源的适配)
3)有更高性能的资源替换 ---- eg:SparseBooleanArray SparseIntArray
4)国际化问题(硬编码)
5)图标的问题(重复的图标,错误的大小)
6)可用性问题(如不指定的文本字段的输入型)
7)manifest文件的错误 -- 未注册activity service等等
8)内存泄露 --- 如:handle的不当使用 。
9)占内存的资源及时回收 --- 如:TypedArray未回收资源等
如何使用lint检索我们代码中存在的问题呢?
在编辑窗口邮件调出菜单选项
然后弹出选择检测目标project/module/files等
通过lint我们可以直观的看到我们代码中存在的不易被发现的问题,
下方的视图是检索出我的代码中存在问题的列表,点击后可以在右边视图展示问题详细信息,以及给出我们一些建议
- 布局优化建议
布局优化的一些建议
首先删除布局中无用的控件和层级,其次有选择的使用性能较低的ViewGroup,比如RelativeLayout.如果布局中既可以使用LinearLayout也可以使用RelativeLayout,那么就采用LinearLayout,这是因为RelativeLayout的功能比较复杂,它的布局过程需要花费更多的CPU时间.
布局优化的另外一种手段是抽象布局标签
采用<include>标签,<merge>标签和,<include>标签主要用于布局的重用, ,<merge>标签一般和<include>配合使用,它可以降低减少布局的层级, 某布局作为子布局被其他布局include时,使用merge当作该布局的顶节点,这样在被引入时顶结点会自动被忽略,而将其子节点全部合并到主布局中。去除不必要的嵌套和View节点
首次不需要使用的节点设置为GONE或使用viewstub
viewstub标签同include标签一样可以用来引入一个外部布局,不同的是,viewstub引入的布局默认不会扩张,即既不会占用显示也不会占用位置,按需加载,当需需要时才会将ViewStup中的布局加载到内存,这提高了程序的初始功率.
从而在解析layout时节省cpu和内存。viewstub常用来引入那些默认不会显示,只在特殊情况下显示的布局,如进度布局、网络失败显示的刷新布局、信息出错出现的提示布局等。减少不必要的infalte
对于inflate的布局可以直接缓存,用全局变量代替局部变量,避免下次需再次inflate用SurfaceView或TextureView代替普通View
SurfaceView或TextureView可以通过将绘图操作移动到另一个单独线程上提高性能。
普通View的绘制过程都是在主线程(UI线程)中完成,如果某些绘图操作影响性能就不好优化了,这时我们可以考虑使用SurfaceView和TextureView,他们的绘图操作发生在UI线程之外的另一个线程上。
因为SurfaceView在常规视图系统之外,所以无法像常规试图一样移动、缩放或旋转一个SurfaceView。TextureView是Android4.0引入的,除了与SurfaceView一样在单独线程绘制外,还可以像常规视图一样被改变。
通过上面UI性能的原理、原因、工具分析总结可以发现,我们在开发应用时一定要时刻重视性能问题,如若真的没留意出现了性能问题,不妨使用上面的一些案例方式进行分析。但是那终归是补救措施,在我们知道上面UI卡顿原理之后我们应该尽量从项目代码架编写时就避免一些UI性能问题,
当然了,上面只是列出了我们项目中常见的一些UI性能注意事项而已,相信还有很多其他的情况这里没有说到,欢迎补充。还有一点就是我们上面所谓的UI性能优化分析总结等都是建议性的,因为性能这个问题是一个涉及面很广很泛的问题,有些优化不是必需的,有些优化是必需的,有些优化掉以后又是得不偿失的,所以我们一般着手解决那些必须的就可以了。
Memory内存性能问题
说完了应用开发中的UI性能问题后我们就该来关注应用开发中的另一个重要的性能问题了,那就是内存性能优化分析。Android其实就是嵌入式设备,嵌入式设备核心关注点之一就是内存资源;有人说现在的设备都在硬件配置已经很厉害了,所以内存不会再像以前那么紧张了,其实这句话听着没错,但为啥再牛逼配置的Android设备还是越用系统越卡呢?
大家先想一个问题,假设有一个内存为1G的Android设备,上面运行了一个非常非常吃内存的应用,如果没有任何机制的情况下是不是用着用着整个设备会因为我们这个应用把1G内存吃光然后整个系统运行瘫痪呢
为了能够使得Android应用程序安全且快速的运行,Android 的每个应用程序都会使用一个专有的Dalvik虚拟机实例来运行,也就是说每个应用程序都是在属于自己的进程中运行的。一方面,如果程序在运行过程中出现了内存溢出的问题,仅仅会使得自己的进程被杀掉,而不会影响其他进程(如果是system_process 等系统进程出问题的话,则会引起系统重启)。另一方面Android为这些进程分配了内存使用上限,如果应用进程使用的内存超过了这个上限, 就会被杀掉。
Android把这些进程都保留在内存中,直到系统需要更多内存时才选择性的释放一些,保留在内存中是当再次启动这些保留在内存的进程时可以明显提高启动速度,不需要再去加载。在Android系统中框架会定义如下几类进程、在系统内存达到规定的不同level阈值时触发清空不同level的进程类型。
Android内存泄露性能分析
千里之堤, 毁于蚁穴
在Android开发过程中,最为让我们头疼的就是内存的泄露问题了,很可能你很小的一个错误都会引起内存的泄露,
一些对象有着有限的生命周期。当这些对象所要做的事情完成了,我们希望他们会被回收掉。但是如果这个对象被超过自己生命周期以外的对象强引用,那么在我们期待这个对象生命周期结束的时候被收回的时候,它是不会被回收的。它还会占用内存,这就造成了内存泄露。持续累加,内存很快被耗尽。
在Java中有些对象的生命周期是有限的,当它们完成了特定的逻辑后将会被垃圾回收;
但是,如果在对象的生命周期本来该被垃圾回收时这个对象还被别的对象所持有引用,那就会导致内存泄漏;这样的后果就是随着我们的应用被长时间使用,他所占用的内存越来越大。
造成内存泄露泄露的最核心原理就是一个对象持有了超过自己生命周期以外的对象强引用导致该对象无法被正常垃圾回收;可以发现,应用内存泄露是个相当棘手重要的问题,我们必须重视。
- Android应用开发规避内存泄露建议
关于规避内存泄露我在下面列出了我在项目中经常遇见的一些情况,肯定不全面,欢迎补充!
Activity中生成的对象原则上是应该在Activity生命周期结束之后就释放的。Activity对象本身也是,所以应该尽量避免有appliction进程级别的对象来引用Activity级别的对象,如果有的话也应该在Activity结束的时候解引用。如不应用applicationContext在Activity中获取资源。
线程未终止造成的内存泄露;譬如在Activity中关联了一个生命周期超过Activity的Thread,在退出Activity时切记结束线程。一个典型的例子就是HandlerThread的run方法是一个死循环,它不会自己结束,线程的生命周期超过了Activity生命周期,我们必须手动在Activity的销毁方法中中调运thread.getLooper().quit();才不会泄露。
对象的注册与反注册没有成对出现造成的内存泄露;BroadCastReceiver、Service 解绑。
创建与关闭没有成对出现造成的泄露;譬如Cursor资源必须手动关闭,WebView必须手动销毁,I/O等对象必须手动关闭等。
不要在执行频率很高的方法或者循环中创建对象,如onDraw中创建新的局部对象。
避免代码设计模式的错误造成内存泄露;譬如循环引用,A持有B,B持有C,C持有A,这样的设计谁都得不到释放。
属性动画导致的内存泄露从Android 3.0开始,Google提供了属性动画,属性动画中有一类无限循环的动画,如果在Activity中播放此类动画且没有在onDestroy中去停止动画,那么动画会一直播放下去,尽管已经无法在界面上看到动画效果了,并且这个时候Activity的View会被动画持有,而View又持有了Activity,最终Activity无法被释放.
内存泄露察觉工具
知道了内存泄露的概念之后肯定就是想办法来确认自己的项目是否存在内存泄露了,那该如何察觉自己项目是否存在内存泄露呢?
排查内存泄露是一个全手工的过程
以下几个关键步骤:
重现问题。为了重现问题,机型非常重要,因为一些问题只在特定的设备上会出现。为了找到特定的机型,你需要想尽一切办法,你可能需要去买,去借,。 当然,为了确定复现步骤,你需要一遍一遍地去尝试。一切都是非常原始和粗暴的。
在发生内存泄露的时候,把内存 Dump 出来。具体看这里。然后,你需要在 MAT 的内存分析工具中反复查看,找到那些原本该被回收掉的对象。
计算这个对象到 GC roots 的最短强引用路径。
确定引用路径中的哪个引用是不该有的,然后修复问题。
很复杂对吧?
如果有一个类库能在发生 OOM 之前把这些事情全部都搞定,然后你只要修复这些问题就好了,岂不妙哉!
直白的展现Android中的内存泄露
LeakCanary 是一个检测内存泄露的开源类库。我们可以在debug 包种轻松检测内存泄露。
只需几行代码,LeakCanary就能自动检测Activity的泄漏:比如我们在android studio中可以直接引入这个类库
当存在内存泄漏时,会有一个通知和良好的展示界面:
如何使用:
在 build.gradle 中加入引用,不同的编译使用不同的引用:
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
}
在 Application 中:
public class ExampleApplication extends Application {
@Override public void onCreate() {
super.onCreate();
LeakCanary.install(this);
}
}
这样,就万事俱备了! 在 debug build 中,如果检测到某个 activity 有内存泄露,LeakCanary 就是自动地显示一个通知。
Android内存溢出OOM
上面我们探讨了Android内存管理和应用开发中的内存泄露问题,可以知道内存泄露一般影响就是导致应用卡顿,但是极端的影响是使应用挂掉。前面也提到过应用的内存分配是有一个阈值的,超过阈值就会出问题,这里我们就来看看这个问题—–内存溢出(OOM–OutOfMemoryError)。
内存溢出的主要导致原因有两类:
应用代码存在内存泄露,长时间积累无法释放导致OOM;
应用的某些逻辑操作疯狂的消耗掉大量内存(譬如加载一张不经过处理的超大超高清图片等)导致超过阈值OOM;
可以发现,无论哪种类型,导致内存溢出(OutOfMemoryError)的核心原因就是应用的内存超过阈值了。
- Android应用规避内存溢出OOM建议
还是那句话,等待OOM发生是为时已晚的事,我们应该将其扼杀于萌芽之中,至于如何在开发中规避OOM,如下给出一些我们应用开发中的常用的策略建议:
时刻记得不要加载过大的Bitmap对象;譬如对于类似图片加载我们要通过BitmapFactory.Options,对相关参数进行配置来减少加载的像素,设置图片的一些采样比率和复用等,在BitmapFactory.Options中指定inSampleSize参数,这将表明一旦加载时结果Bitmap图像所占的比例。例如,在这里将inSampleSize设置为4,这会产生一幅大小是原始图像大小1/4的图像
优化界面交互过程中频繁的内存使用;譬如在列表等操作中只加载可见区域的Bitmap、滑动时不加载、停止滑动后再开始加载。
避免各种内存泄露的存在导致OOM。
对批量加载等操作进行缓存设计,譬如列表图片显示,Adapter的convertView缓存等。
尽可能的复用资源;譬如系统本身有很多字符串、颜色、图片、动画、样式以及简单布局等资源可供我们直接使用,我们自己也要尽量复用style等资源达到节约内存。
对于有缓存等存在的应用尽量实现onLowMemory()和onTrimMemory()方法来需要的时候释放缓存。
尽量使用线程池替代多线程操作,这样可以节约内存及CPU占用率。
尽量管理好自己的Service、Thread等后台的生命周期,不要浪费内存占用。
尽量在做一些大内存分配等可疑内存操作时进行try catch操作,避免不必要的应用闪退。
尽量的优化自己的代码,减少冗余,避免类加载时浪费内存。
可以发现,上面只是列出了我们开发中常见的导致OOM异常的一些规避原则,还有很多相信还没有列出来,大家可以自行追加参考即可。
- Android应用OnTrimMemory()实现性能建议
OnTrimMemory是Android 4.0之后加入的一个回调方法,作用是通知应用在不同的情况下进行自身的内存释放,以避免被系统直接杀掉,提高应用程序的用户体验。系统会根据当前不同等级的内存使用情况调用这个方法,并且传入当前内存等级,这个等级有很多种,我们可以依据情况实现不同的等级.
可以实现OnTrimMemory方法的系统组件有Application、Activity、Fragement、Service、ContentProvider;
关于OnTrimMemory释放哪些内存其实在架构阶段就要考虑清楚哪些对象是要常驻内存的,哪些是伴随组件周期存在的,一般需要释放的都是缓存。
OnLowMemory是Android提供的API,在系统内存不足,所有后台程序(优先级为background的进程,不是指后台运行的进程)都被杀死时,系统会调用OnLowMemory。
TRIM_MEMORY_COMPLETE:内存不足,并且该进程在后台进程列表最后一个,马上就要被清理
TRIM_MEMORY_MODERATE:内存不足,并且该进程在后台进程列表的中部。
TRIM_MEMORY_BACKGROUND:内存不足,并且该进程是后台进程。
TRIM_MEMORY_UI_HIDDEN:内存不足,并且该进程的UI已经不可见了。
以上4个是4.0增加
TRIM_MEMORY_RUNNING_CRITICAL:内存不足(后台进程不足3个),并且该进程优先级比较高,需要清理内存
TRIM_MEMORY_RUNNING_LOW:内存不足(后台进程不足5个),并且该进程优先级比较高,需要清理内存
TRIM_MEMORY_RUNNING_MODERATE:内存不足(后台进程超过5个),并且该进程优先级比较高,需要清理内存
无论是什么电子设备的开发,内存问题永远都是一个很深奥、无底洞的话题,上面的这些内存分析建议也单单只是Android应用开发中一些常见的场景而已,真正的达到合理的优化还是需要很多知识和功底的。
合理的应用架构设计、设计风格选择、开源库选择、代码逻辑规范等都会决定到应用的内存性能,我们必须时刻头脑清醒的意识到这些问题潜在的风险与优劣,因为内存优化必须要有一个度,不能一味的优化,亦不能置之不理。