本篇主要是对 google推出的性能优化典范 进行一个通篇的整理… 主要在于一些具体的优化技巧、至于 60fps、掉帧、gc、内存抖动、阈值…等等这些性能术语的概念里面不做多概括,请自行查阅…
本篇从以下几点延伸扩展…
绘制/渲染
优化布局的结构
- 避免复杂的View层级。布局越复杂就越臃肿,就越容易出现性能问题,寻找最节省资源的方式去展示嵌套的内容;
- 尽量避免在视图层级的顶层使用相对布局 RelativeLayout 。相对布局 RelativeLayout 比较耗资源,因为一个相对布局 RelativeLayout 需要两次度量来确保自己处理了所有的布局关系,而且这个问题会伴随着视图层级中的相对布局 RelativeLayout 的增多,而变得更严重;
- 布局层级一样的情况建议使用线性布局 LinearLayout 代替相对布局 RelativeLayout,因为线性布局 LinearLayout 性能要更高一些;确实需要对分支进行相对布局 RelativeLayout 的时候,可以考虑更优化的网格布局 GridLayout ,它已经预处理了分支视图的关系,可以避免两次度量的问题;
- 相对复杂的布局建议采用相对布局 RelativeLayout ,相对布局 RelativeLayout 可以简单实现线性布局 LinearLayout 嵌套才能实现的布局;
- 将可重复使用的组件抽取出来并用 include 标签进行重用。如果应用多个地方的 UI 用到某个布局,就将其写成一个布局部件,便于各个 UI 重用;
- 使用 merge 标签减少布局的嵌套层次;
- 去掉多余的不可见背景。有多层背景颜色的布局,只留最上层的对用户可见的颜色即可,其他用户不可见的底层颜色可以去掉,减少无效的绘制操作;
- 尽量避免使用 layout_weight 属性。使用包含 layout_weight 属性的线性布局 LinearLayout 每一个子组件都需要被测量两次,会消耗过多的系统资源;
- 合理的界面的布局结构应是宽而浅,而不是窄而深;
优化处理逻辑
- 按需载入视图。某些不怎么重用的耗资源视图,可以等到需要的时候再加载,提高UI渲染速度;
- 使用 ViewStub 标签来加载一些不常用的布局;
- 动态地 inflation view 性能要比用 ViewStub 标签的 setVisiblity 性能要好,当然某些功能的实现采用 ViewStub 标签更合适;
- 尽量避免不必要的耗资源操作,节省宝贵的运算时间;
- 避免在 UI 线程进行繁重的操作。耗资源的操作(比如 IO 操作、网络操作、SQL 操作、列表刷新等)耗资源的操作应用后台进程去实现,不能占用 UI 线程,UI 线程是主线程,主线程是保持程序流畅的关键,应该只操作那些核心的 UI 操作,比如处理视图的属性和绘制;
- 最小化唤醒机制。我们常用广播来接收那些期望响应的消息和事件,但过多的响应超过本身需求的话,会消耗多余的 Android 设备性能和资源。所以应该最小化唤醒机制,当应用不关心这些消失和事件时,就关闭广播,并慎重选择那些要响应的 Intent ;
- 为低端设备考虑,比如 512M 内存、双核 CPU 、低分辨率,确保你的应用可以满足不同水平的设备;
- 优化应用的启动速度。当应用启动一个应用时,界面的尽快反馈显示可以给用户一个良好的体验。为了启动更快,可以延迟加载一些 UI 以及避免在应用 Application 层级初始化代码。
UI调试工具
- Hierarchy View :Hierarchy View 在Android SDK里自带,常用来查看界面的视图结构是否过于复杂,用于了解哪些视图过度绘制,又该如何进行改进;
- Lint:Lint 是 ADT 自带的静态代码扫描工具,可以给 XML 布局文件和 项目代码中不合理的或存在风险的模块提出改善性建议。官方关于 Lint 的实际使用的提示,列举几点如下:
- 包含无用的分支,建议去除;
- 包含无用的父控件,建议去除;
- 警告该布局深度过深;
- 建议使用 compound drawables ;
- 建议使用 merge 标签;
……
- Systrace:Systrace 在Android DDMS 里自带,可以用来跟踪 graphics 、view 和 window 的信息,发现一些深层次的问题。很麻烦,限制大,实际调试中我基本用不到;
- Track:Track 在 Android DDMS里自带,是个很棒的用来跟踪构造视图的时候哪些方法费时,精确到每一个函数,无论是应用函数还是系统函数,我们可以很容易地看到掉帧的地方以及那一帧所有函数的调用情况,找出问题点进行优化。
- OverDraw:通过在 Android 设备的设置 APP 的开发者选项里打开 “ 调试 GPU 过度绘制 ” ,来查看应用所有界面及分支界面下的过度绘制情况,方便进行优化;
- GPU 呈现模式分析:通过在 Android 设备的设置 APP 的开发者选项里启动 “ GPU 呈现模式分析 ” ,可以得到最近 128 帧 每一帧渲染的时间,分析性能渲染的性能及性能瓶颈;
- StrictMode:通过在 Android 设备的设置 APP 的开发者选项里启动 “ 严格模式 ” ,来查看应用哪些操作在主线程上执行时间过长。当一些操作违背了严格模式时屏幕的四周边界会闪烁红色,同时输出 StrictMode 的相关信息到 LOGCAT 日志中;
- Animator duration scale:通过在 Android 设备的设置 APP 的开发者选项里打开 “ 窗口动画缩放 ” / “ 过渡动画缩放 ” / “ 动画程序时长缩放 ”,来加速或减慢动画的时间,以查看加速或减慢状态下的动画是否会有问题;
- Show hardware layer updates:通过在 Android 设备的设置 APP 的开发者选项里启动 “ 显示硬件层更新 ”,当 Flash 硬件层在进行更新时会显示为绿色。使用这个工具可以让你查看在动画期间哪些不期望更新的布局有更新,方便你进行优化,以获得应用更好的性能。
内存/CPU
减小对象的内存占用
- 使用更加轻量的数据结构:例如考虑使用ArrayMap/SparseArray(SparseBoolMap,SparseIntMap,SparseLongMap,LongSparseMap)代替HashMap等传统数据结构。(http://hukai.me/android-performance-patterns-season-3/);
- 避免使用枚举:在Android官网上已经明确指出应该在Android避免使用Enum,因为与静态常量相比,它对内存的占用要大很多。http://hukai.me/android-performance-patterns-season-3/ ;
- 传输经过压缩 gzip的数据;
- 资源文件需要选择合适的文件夹进行存放:我们知道hdpi/xhdpi/xxhdpi等等不同dpi的文件夹下的图片在不同的设备上会经过scale的处理。例如我们只在hdpi的目录下放置了一张100100的图片,那么根据换算关系,xxhdpi的手机去引用那张图片就会被拉伸到200200。需要注意到在这种情况下,内存占用是会显著提高的。对于不希望被拉伸的图片,需要放到assets或者nodpi的目录下。
- 对图片进行有损压缩以减小内存:如 使用 tinyPNG;
- 减小创建出来的Bitmap的内存占用:对Bitmap等比缩放 或者选择解码格式(有透明度:ARGB_4444,无透明度:RGB_565)。
复用对象
- 复用系统自带的资源:Android系统本身内置了很多的资源,例如字符串/颜色/图片/动画/样式以及简单布局等等,这些资源都可以在应用程序中直接引用。这样做不仅仅可以减少应用程序的自身负重,减小APK的大小,另外还可以一定程度上减少内存的开销,复用性更好。但是也有必要留意Android系统的版本差异性,对那些不同系统版本上表现存在很大差异,不符合需求的情况,还是需要应用程序自身内置进去。
- listView 内子控件的复用
- Bitmap对象的复用:
- 用LRU的机制来缓存处理好的Bitmap;
- 利用inBitmap的高级特性提高Android系统在Bitmap分配与释放执行效率上的提升(3.0以及4.4以后存在一些使用限制上的差异)。使用inBitmap属性可以告知Bitmap解码器去尝试使用已经存在的内存区域,新解码的bitmap会尝试去使用之前那张bitmap在heap中所占据的pixel data内存区域,而不是去问内存重新申请一块区域来存放bitmap。利用这种特性,即使是上千张的图片,也只会仅仅只需要占用屏幕所能够显示的图片数量的内存大小。
- 避免在onDraw方法里面执行对象的创建:类似onDraw等频繁调用的方法,一定需要注意避免在这里做创建对象的操作,因为他会迅速增加内存的使用,而且很容易引起频繁的gc,甚至是内存抖动;
- StringBuilder:在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用StringBuilder来替代频繁的“+”;
- 避免在for语句内创建对象;
- …
避免对象的内存泄露
内存对象的泄漏,会导致一些不再使用的对象无法及时释放,这样一方面占用了宝贵的内存空间,很容易导致后续需要分配内存的时候,空闲空间不足而出现OOM。
显然,这还使得每级Generation的内存区域可用空间变小,gc就会更容易被触发,容易出现内存抖动,从而引起性能问题。
最新的LeakCanary开源控件,可以很好的帮助我们发现内存泄露的情况,更多关于LeakCanary的介绍,请看这里 https://github.com/square/leakcanary(中文使用说明。另外也可以使用传统的MAT工具查找内存泄露,请参考这里(便捷的中文资料)
注意Activity的泄漏
- 考虑使用Application Context而不是Activity Context;
- 注意临时Bitmap对象的及时回收
- 虽然在大多数情况下,我们会对Bitmap增加缓存机制,但是在某些时候,部分Bitmap是需要及时回收的。例如临时创建的某个相对比较大的bitmap对象,在经过变换得到新的bitmap对象之后,应该尽快回收原始的bitmap,这样能够更快释放原始bitmap所占用的空间。
需要特别留意的是Bitmap类里面提供的createBitmap()方法:
这个函数返回的bitmap有可能和source bitmap是同一个,在回收的时候,需要特别检查source bitmap与return bitmap的引用是否相同,只有在不等的情况下,才能够执行source bitmap的recycle方法。
- 注意监听器的注销:
- 在Android程序里面存在很多需要register与unregister的监听器,我们需要确保在合适的时候及时unregister那些监听器。自己手动add的listener,需要记得及时remove这个listener。
注意缓存容器中的对象泄漏:
- 有时候,我们为了提高对象的复用性把某些对象放到缓存容器中,可是如果这些对象没有及时从容器中清除,也是有可能导致内存泄漏的。例如,针对2.3的系统,如果把drawable添加到缓存容器,因为drawable与View的强应用,很容易导致activity发生泄漏。而从4.0开始,就不存在这个问题。解决这个问题,需要对2.3系统上的缓存drawable做特殊封装,处理引用解绑的问题,避免泄漏的情况。
注意WebView的泄漏:
- Android中的WebView存在很大的兼容性问题,不仅仅是Android系统版本的不同对WebView产生很大的差异,另外不同的厂商出货的ROM里面WebView也存在着很大的差异。更严重的是标准的WebView存在内存泄露的问题,看这里WebView causes memory leak - leaks the parent Activity。所以通常根治这个问题的办法是为WebView开启另外一个进程,通过AIDL与主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。
注意Cursor对象是否及时关闭:
- 在程序中我们经常会进行查询数据库的操作,但时常会存在不小心使用Cursor之后没有及时关闭的情况。这些Cursor的泄露,反复多次出现的话会对内存管理产生很大的负面影响,我们需要谨记对Cursor对象的及时关闭。
优化策略
谨慎使用large heap:
- Android设备根据硬件与软件的设置差异而存在不同大小的内存空间,他们为应用程序设置了不同大小的Heap限制阈值。你可以通过调用
getMemoryClass()
来获取应用的可用Heap大小。在一些特殊的情景下,你可以通过在manifest的application标签下添加largeHeap=true的属性来为应用声明一个更大的heap空间。然后,你可以通过getLargeMemoryClass()来获取到这个更大的heap size阈值。然而,声明得到更大Heap阈值的本意是为了一小部分会消耗大量RAM的应用(例如一个大图片的编辑应用)。不要轻易的因为你需要使用更多的内存而去请求一个大的Heap Size。只有当你清楚的知道哪里会使用大量的内存并且知道为什么这些内存必须被保留时才去使用large heap。因此请谨慎使用large heap属性。使用额外的内存空间会影响系统整体的用户体验,并且会使得每次gc的运行时间更长。在任务切换时,系统的性能会大打折扣。另外, large heap并不一定能够获取到更大的heap。在某些有严格限制的机器上,large heap的大小和通常的heap size是一样的。因此即使你申请了large heap,你还是应该通过执行getMemoryClass()来检查实际获取到的heap大小。
综合考虑设备内存阈值与其他因素设计合适的缓存大小:
- 例如,在设计ListView或者GridView的Bitmap LRU缓存的时候,需要考虑的点有:
- 应用程序剩下了多少可用的内存空间?
- 有多少图片会被一次呈现到屏幕上?有多少图片需要事先缓存好以便快速滑动时能够立即显示到屏幕?
- 设备的屏幕大小与密度是多少? 一个xhdpi的设备会比hdpi需要一个更大的Cache来hold住同样数量的图片。
- 不同的页面针对Bitmap的设计的尺寸与配置是什么,大概会花费多少内存?
- 页面图片被访问的频率?是否存在其中的一部分比其他的图片具有更高的访问频繁?如果是,也许你想要保存那些最常访问的到内存中,或者为不同组别的位图(按访问频率分组)设置多个LruCache容器。
onLowMemory()与onTrimMemory()
:
Android用户可以随意在不同的应用之间进行快速切换。为了让background的应用能够迅速的切换到forground,每一个background的应用都会占用一定的内存。Android系统会根据当前的系统的内存使用情况,决定回收部分background的应用内存。如果background的应用从暂停状态直接被恢复到forground,能够获得较快的恢复体验,如果background应用是从Kill的状态进行恢复,相比之下就显得稍微有点慢。
- onLowMemory():Android系统提供了一些回调来通知当前应用的内存使用情况,通常来说,当所有的background应用都被kill掉的时候,forground应用会收到onLowMemory()的回调。在这种情况下,需要尽快释放当前应用的非必须的内存资源,从而确保系统能够继续稳定运行。
onTrimMemory(int):Android系统从4.0开始还提供了onTrimMemory()的回调,当系统内存达到某些条件的时候,所有正在运行的应用都会收到这个回调,同时在这个回调里面会传递以下的参数,代表不同的内存使用情况,收到onTrimMemory()回调的时候,需要根据传递的参数类型进行判断,合理的选择释放自身的一些内存占用,一方面可以提高系统的整体运行流畅度,另外也可以避免自己被系统判断为优先需要杀掉的应用。下图介绍了各种不同的回调参数:
TRIM_MEMORY_UI_HIDDEN
:你的应用程序的所有UI界面被隐藏了,即用户点击了Home键或者Back键退出应用,导致应用的UI界面完全不可见。这个时候应该释放一些不可见的时候非必须的资源
- 当程序正在前台运行的时候,可能会接收到从onTrimMemory()中返回的下面的值之一:
TRIM_MEMORY_RUNNING_MODERATE
:你的应用正在运行并且不会被列为可杀死的。但是设备此时正运行于低内存状态下,系统开始触发杀死LRU Cache中的Process的机制。
TRIM_MEMORY_RUNNING_LOW
:你的应用正在运行且没有被列为可杀死的。但是设备正运行于更低内存的状态下,你应该释放不用的资源用来提升系统性能。
TRIM_MEMORY_RUNNING_CRITICAL
:你的应用仍在运行,但是系统已经把LRU Cache中的大多数进程都已经杀死,因此你应该立即释放所有非必须的资源。如果系统不能回收到足够的RAM数量,系统将会清除所有的LRU缓存中的进程,并且开始杀死那些之前被认为不应该杀死的进程,例如那个包含了一个运行态Service的进程。
当应用进程退到后台正在被Cached的时候,可能会接收到从onTrimMemory()中返回的下面的值之一:
Try catch某些大内存分配的操作:
- 在某些情况下,我们需要事先评估那些可能发生OOM的代码,对于这些可能发生OOM的代码,加入catch机制,可以考虑在catch里面尝试一次降级的内存分配操作。例如decode bitmap的时候,catch到OOM,可以尝试把采样比例再增加一倍之后,再次尝试decode。
资源文件需要选择合适的文件夹进行存放
- 谨慎使用static对象:
- 因为static的生命周期过长,和应用的进程保持一致,使用不当很可能导致对象泄漏,在Android中应该谨慎使用static对象。
- 特别留意单例对象中不合理的持有:
- 虽然单例模式简单实用,提供了很多便利性,但是因为单例的生命周期和应用保持一致,使用不合理很容易出现持有对象的泄漏。
- 珍惜Services资源
- 如果你的应用需要在后台使用service,除非它被触发并执行一个任务,否则其他时候Service都应该是停止状态。另外需要注意当这个service完成任务之后因为停止service失败而引起的内存泄漏。 当你启动一个Service,系统会倾向为了保留这个Service而一直保留Service所在的进程。这使得进程的运行代价很高,因为系统没有办法把Service所占用的RAM空间腾出来让给其他组件,另外Service还不能被Paged out。这减少了系统能够存放到LRU缓存当中的进程数量,它会影响应用之间的切换效率,甚至会导致系统内存使用不稳定,从而无法继续保持住所有目前正在运行的service。 建议使用IntentService,它会在处理完交代给它的任务之后尽快结束自己。更多信息,请阅读Running in a Background Service。
- 优化布局层次,减少内存消耗
- 越扁平化的视图布局,占用的内存就越少,效率越高。我们需要尽量保证布局足够扁平化,当使用系统提供的View无法实现足够扁平的时候考虑使用自定义View来达到目的。
谨慎使用“抽象”编程
- 很多时候,开发者会使用抽象类作为”好的编程实践”,因为抽象能够提升代码的灵活性与可维护性。然而,抽象会导致一个显著的额外内存开销:他们需要同等量的代码用于可执行,那些代码会被mapping到内存中,因此如果你的抽象没有显著的提升效率,应该尽量避免他们。
谨慎使用依赖注入框架
- 使用类似Guice或者RoboGuice等框架注入代码,在某种程度上可以简化你的代码。
代码是简化了不少。然而,那些注入框架会通过扫描你的代码执行许多初始化的操作,这会导致你的代码需要大量的内存空间来mapping代码,而且mapped pages会长时间的被保留在内存中。除非真的很有必要,建议谨慎使用这种技术。
谨慎使用多进程:
- 使用多进程可以把应用中的部分组件运行在单独的进程当中,这样可以扩大应用的内存占用范围,但是这个技术必须谨慎使用,绝大多数应用都不应该贸然使用多进程,一方面是因为使用多进程会使得代码逻辑更加复杂,另外如果使用不当,它可能反而会导致显著增加内存。当你的应用需要运行一个常驻后台的任务,而且这个任务并不轻量,可以考虑使用这个技术。
- 一个典型的例子是创建一个可以长时间后台播放的Music Player。如果整个应用都运行在一个进程中,当后台播放的时候,前台的那些UI资源也没有办法得到释放。类似这样的应用可以切分成2个进程:一个用来操作UI,另外一个给后台的Service。
使用ProGuard来剔除不需要的代码
- ProGuard能够通过移除不需要的代码,重命名类,域与方法等等对代码进行压缩,优化与混淆。使用ProGuard可以使得你的代码更加紧凑,这样能够减少mapping代码所需要的内存空间。
谨慎使用第三方libraries:
- 很多开源的library代码都不是为移动网络环境而编写的,如果运用在移动设备上,并不一定适合。即使是针对Android而设计的library,也需要特别谨慎,特别是在你不知道引入的library具体做了什么事情的时候。例如,其中一个library使用的是nano protobufs, 而另外一个使用的是micro protobufs。这样一来,在你的应用里面就有2种protobuf的实现方式。这样类似的冲突还可能发生在输出日志,加载图片,缓存等等模块里面。另外不要为了1个或者2个功能而导入整个library,如果没有一个合适的库与你的需求相吻合,你应该考虑自己去实现,而不是导入一个大而全的解决方案。
测量工具
- Memory Monitor:跟踪整个app的内存变化情况。
- Heap Viewer:查看当前内存快照,便于对比分析哪些对象有可能发生了泄漏。
- Allocation Tracker:追踪内存对象的来源。
电量
网络 and 电量消耗
一个处于完全工作状态的无线电会大量消耗电量,因此需要学习如何在不同能量状态下进行过渡,当无线电没有工作时,节省电量,当需要时尝试最小化与无线电波供电有关的延迟。
典型的 3G 无线电网络有三种能量状态:
策略
根据电池状态区别操作
如:
- 充电状态或者电量高于20%直接播放视频
- 当电池低于20%时 给出提示“请充电…”
- ….
唤醒工作与电量消耗
高效的保留更多的电量与不断促使用户使用你的App会消耗电量,这是矛盾的选择题。不过我们可以使用一些更好的办法来平衡两者。
假设你的手机里面装了大量的社交类应用,即使手机处于待机状态,也会经常被这些应用唤醒用来检查同步新的数据信息。Android会不断关闭各种硬件来延长手机的待机时间,首先屏幕会逐渐变暗直至关闭,然后CPU进入睡眠,这一切操作都是为了节约宝贵的电量资源。但是即使在这种睡眠状态下,大多数应用还是会尝试进行工作,他们将不断的唤醒手机。一个最简单的唤醒手机的方法是使用PowerManager.WakeLock的API来保持CPU工作并防止屏幕变暗关闭。这使得手机可以被唤醒,执行工作,然后回到睡眠状态。知道如何获取WakeLock是简单的,可是及时释放WakeLock也是非常重要的,不恰当的使用WakeLock会导致严重错误。例如网络请求的数据返回时间不确定,导致本来只需要10s的事情一直等待了1个小时,这样会使得电量白白浪费了。这也是为何使用带超时参数的wakelock.acquice()方法是很关键的。
但是仅仅设置超时并不足够解决问题,例如设置多长的超时比较合适?什么时候进行重试等等?解决上面的问题,正确的方式可能是使用非精准定时器。通常情况下,我们会设定一个时间进行某个操作,但是动态修改这个时间也许会更好。例如,如果有另外一个程序需要比你设定的时间晚5分钟唤醒,最好能够等到那个时候,两个任务捆绑一起同时进行,这就是非精确定时器的核心工作原理。我们可以定制计划的任务,可是系统如果检测到一个更好的时间,它可以推迟你的任务,以节省电量消耗。
这正是JobScheduler API所做的事情。它会根据当前的情况与任务,组合出理想的唤醒时间,例如等到正在充电或者连接到WiFi的时候,或者集中任务一起执行。我们可以通过这个API实现很多免费的调度算法。
Battery Historian
Battery Historian是Android 5.0开始引入的新API。通过下面的指令,可以得到设备上的电量消耗信息:
$ adb shell dumpsys batterystats > xxx.txt //得到整个设备的电量消耗信息
$ adb shell dumpsys batterystats > com.package.name > xxx.txt //得到指定app相关的电量消耗信息
得到了原始的电量消耗数据之后,我们需要通过Google编写的一个python脚本把数据信息转换成可读性更好的html文件:
$ python historian.py xxx.txt > xxx.html
打开这个转换过后的html文件,可以看到类似TraceView生成的列表数据,这里的数据信息量很大,这里就不展开了。
网络
其它
多线程
根据业务判断何时开启线程…
开启一个线程 大概耗时4*10^6 纳秒 与 64K+内存;
- AsyncTask:
为UI线程与工作线程之间进行快速的切换提供一种简单便捷的机制。适用于当下立即需要启动,但是异步执行的生命周期短暂的使用场景。
- HandlerThread:
为某些回调方法或者等待某些任务的执行设置一个专属的线程,并提供线程任务的调度机制。
- ThreadPool:
把任务分解成不同的单元,分发到各个不同的线程上,进行同时并发处理。
- IntentService:
适合于执行由UI触发的后台Service任务,并可以把后台任务执行的情况通过一定的机制反馈给UI。
了解这些系统提供的多线程工具类分别适合在什么场景下,可以帮助我们选择合适的解决方案,避免出现不可预期的麻烦。虽然使用多线程可以提高程序的并发量,但是我们需要特别注意因为引入多线程而可能伴随而来的内存问题。举个例子,在Activity内部定义的一个AsyncTask,它属于一个内部类,该类本身和外面的Activity是有引用关系的,如果Activity要销毁的时候,AsyncTask还仍然在运行,这会导致Activity没有办法完全释放,从而引发内存泄漏。所以说,多线程是提升程序性能的有效手段之一,但是使用多线程却需要十分谨慎小心,如果不了解背后的执行机制以及使用的注意事项,很可能引起严重的问题。
由于时间关系先写到这里,待续…