Android性能优化(◍˃ᗜ˂◍)✩
Android作为移动设备,内存和CPU的性能上都受到了一定的限制。
- ♛ 过多的使用内存:导致程序内存溢出(OOM)
- ♛ 过多的使用CPU资源:一般指做大量耗时任务导致手机变得卡顿甚至出现ANR
而内存泄漏并不会导致程序功能异常,会导致Android程序的内存占用更大,提高内存溢出的几率。
✩ 布局优化
1. <include>
可以将一个指定布局文件加载到当前的布局文件中。除了<code>android:id</code>属性之外,只支持<code>android:layout_</code>开头的属性(<code>android:layout_width</code>等)。
<include layout="@layout/title" />
2. <merge>
一般和<code><include></code>一起配合使用减少布局层次。
3. ViewStub
继承自View,轻量级且宽高都为0,本身并不参与任何布局和绘制过程。它的存在在于按需分配所需的布局文件,在使用时进行加载,提高程序初始化性能。
<ViewStub
android:id="@+id/stub_import"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:inflatedId="@id/panel_import" // layout_network_error的根元素id
android:layout="@layout/layout_network_error"/>
加载布局方式
((ViewStub)) findViewById(R.id.stub_import)).setVisibility(View.VISIVLE);
或
View importPanel = ((ViewStub)) findViewById(R.id.stub_import)).inflate();
通过加载之后,ViewStub就会被它的内存布局替换掉,这个时候ViewStub就不再是布局结构的一部分,目前还不支持<merge>标签。
布局过程原则
- 尽量多使用RealtiveLayout,不要使用绝对布局(屏幕尺寸多样)
- 在ListView等列表组件中尽量避免使用LinearLayout的layout_weight属性
- 将可复用的自建抽取出来并通过<include>使用
- 使用<ViewStub>加载不常用的布局
- 使用merge减少布局嵌套
✩ 绘制优化
<code>onDraw</code>方法中避免执行大量操作。
- 不要创建新的布局,会被频繁调用,一瞬间产生大量临时对象,占用过多内存还会导致系统频繁gc,降低程序执行效率。
- 不要做耗时任务,也不要执行成千上万次循环操作,会抢占CPU时间片,造成View绘制不流畅。(每帧的绘制时间不超过16ms最佳)
✩ 内存优化
- 静态变量
- 单例模式
注意释放,否则Activity的对象被单例模式对象持有,单例模式的生命周期和Application保持一致,因此Activity对象无法被及时释放。 - 属性动画
当无法在界面看到动画效果但没有进行停止时,Activity的View会被动画持有,而View又持有Activity,最终Activity无法释放。
在onDestory方法中调用<code>animator.cancle()</code>停止动画。
♛ 1. 保守使用Service
当你启动service时, 系统总是优先保持服务的运行. 这会导致内存应用效率非常低, 因为被该服务使用的内存不能做其它事情. 也会减少系统一直保持的LRU缓存处理数目, 使不同的app切换效率降低. 当前所有service的运行会导致内存不够不能维持正常系统的运行时, 系统会发生卡顿的现象严重时能导致系统不断重启.
最好的方式是使用IntentSevice控制service的生命周期, 当使用intent开始任务后, 该service执行完所有的工作时会自动停止.
在android应用中当不需要使用常驻service执行业务功能而去使用是最糟糕的内存管理方式之一。所以不要贪婪的使用service使你的应用一直运行状态,这样不仅使你因为内存的限制提高了应用运行的风险, 也会导致用户发现这些异常行为后而卸载应用.
** ♛ 2. 当UI隐藏状态后释放内存**
当用户跳转到不同的应用并且你的视图不再显示时, 你应该释放应用视图所占的资源. 这时释放所占用的资源能显著的提高系统的缓存处理容量, 并且对用户的体验质量有直接的影响.
当实现当前Activity类的<code>onTrimMemory()</code>回调方法后, 用户离开视图时会得到通知. 使用该方法可以监听<code>TRIM_MEMORY_UI_HIDDEN</code>级别, 当你的视图元素从父视图中处于隐藏状态时释放视图所占用的资源.
注意只有当你应用的所有视图元素变为隐藏状态时你的应用才能收到<code>onTrimMemory()</code>回调方法的<code>TRIM_MEMORY_UI_HIDDEN.</code> 这个和onStop()回调方法不同, 该方法只有当Activity的实例变为隐藏状态, 或者有用户移动到应用中的另外的activity才会引发.。所以说你虽然实现了onStop()去释放activity的资源例如网络连接或者未注册的广播接收者, 但是应该直到你收到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)才去释放视图资源否则不应该释放视图所占用的资源. 这里可以确定的是如果用户通过后退键从另外的activity进入到你的应用中, 视图资源会一直处于可用的状态可以用来快速的恢复activity.
** ♛ 3. 内存资源紧张时释放内存**
<code>onTrimMemory()</code>回调方法都可以告诉你设备的内存已经开始紧张, 可以根据该方法推送的内存紧张级别来释放哪些资源。
- TRIM_MEMORY_RUNNING_MODERATE
App正在运行,并且不会被列为可杀死的,设备正处于低内存状态下,系统开始触发杀死LRU Cache中的process机制。
- TRIM_MEMORY_RUNNING_LOW **
App正在运行且没有被列为可杀死的。但是设备正处于更低内存的状态下,应该释放不用的资源**来提升系统性能(也会直接影响App性能)
- TRIM_MEMORY_RUNNING_CRITICAL **
App正在运行,但系统已经把LRU Cache中的大多数进程杀死,因此应该立即释放所有非必需资源**。如果系统不能回收到足够的RAM数量,系统将会清除所有的LRU缓存中的进程,并且开始杀死那些之前被认为不应该杀死的进程。(比如,包含了一个运行态Service的进程)。
当App进程正在被cached时,也可能接收到<code>onTrimMemory()</code>中返回的下列值。
- TRIM_MEMORY_BACKGROUND
系统处于低内存的运行状态中并且你的应用处于缓存应用列表的初级阶段。虽然你的应用不会处于被杀的高风险中, 但是系统已经开始清除缓存列表中的其它应用,必须释放资源使你的应用继续存留在列表中以便用户再次回到你的应用时能快速恢复进行使用。
- TRIM_MEMORY_MODERATE
系统处于低内存的运行状态,并且你的应用处于缓存应用列表的中级阶段。如果系统开始变得更加内存紧张,你的进程可能被杀死。
- TRIM_MEMORY_COMPLETE
系统处于低内存的运行状态,你的进程已经接近LRU名单最容易被杀死的位置,应该释放任何不影响你的App恢复状态的资源。
PS:<code>onTrimMemory()</code>在API14才被加进来,在老版本可以使用<code>onLowMemory()</code>回掉进行兼容。<code>onLowMemory()</code>相当于<code>TRIM_MEMORY_COMPLETE</code>。
当系统开始清除缓存应用列表中的应用时, 虽然系统的主要工作机制是自下而上,但是也会通过杀掉消费大内存的应用从而使系统获得更多的内存,所以在缓存应用列表中消耗更少的内存将会有更大的机会留存下来以便用户再次使用时进行快速恢复。
** ♛ 4. 检查可以使用多大的内存**
不同的android设备系统拥有的运行内存各自都不同, 从而不同的应用堆内存的限制大小也不一样。
通过调用<code>ActivityManager</code>中的<code>getMemoryClass()</code>函数通过以兆为单位获取当前应用可用的内存大小, 如果你想获取超过最大限度的内存则会发生OutOfMemoryError。
PS:在manifest文件中的<application>标签中设置<code>largeHeap</code>属性的值为true, 当前应用就可以获取到系统分配的最大堆内存。如果你设置了该值, 可以通过ActivityManager的getLargeMemoryClass() 函数获取最大的堆内存。
但是,只有一小部分应用需要消耗大量堆内存(比如大照片编辑应用)。从来不需要使用大量内存仅仅是因为你已经消耗了大量的内存并且必须快速修复它, 你必须使用它是因为你恰好知道所有的内存已经被分配完,而你必须要保留当前应用不会被清除掉。尽量少使用,使用额外的内存会影响系统整体的用户体验,并且会使GC每次运行时间更长,任务切换时,系统性能也会大打折扣。
<code>largeHeap</code>不一定能够获得更大的heap,在某些有严格限制的机器上,<code>largeHeap</code>的大小和通常的<code>heap size</code>是一样的。因此,即使申请了<code>largeHeap</code>,还应该通过执行<code>getMemoryClass()</code>来检查实际获取的heap大小。
** ♛ 5. 避免bitmaps的浪费**
当你加载bitmap时, 需要根据分辨率来保持它的内存时最大为当前设备的分辨率, 如果下载下来的原图为高分辨率则要拉伸它。bitmap的分辨率增加后所占用的内存也要进行相应的增加, 因为它是根据x和y的大小来增加内存占用的。
PS: 在Android 2.3.x(api level 10)以下, 无论图片的分辨率多大bitmap对象在内存中始终显示相同大小, 实际的像素数据被存储在底层native的内存中(c++内存)。 因为内存分析工具无法跟踪native的内存状态所有调试 bitmap内存分配变得非常困难。
然而, 从Android 3.0(api level 11)开始, bitmap 对象的内存数据开始在应用程序所在Dalvik虚拟机堆内存中进行分配, 提高了回收机率和调试的可能性。 如果你在老版本中发现bitmap对象占用的内存大小始终一样时, 切换设备到系统3.0或以上来进行调试。
** ♛ 6. 使用优化后的数据容器 **
利用Android框架优化后的数据容器, 比如<code>SparseArray</code>, <code>SparseBooleanArray</code>和<code>LongSparseArray</code>. 传统的HashMap在内存上的实现十分的低效,因为它需要为map中每一项在内存中建立映射关系。另外,<code>SparseArray</code>类非常高效,因为它避免系统中需要自动封箱(autobox)的key和有些值。
SparseArray:key为int,存取使用二分查找,避免对key自动装箱,内部两个数组key,value,数据采取压缩的方式来表示系数数组,节约空间。
和ArrayMap区别在于:key是否为int
替代HashMap情况
- 数据量不大,千级以内
- key为int
** ♛ 7. 注意内存的开销**
- 当枚举(enum)成为静态常量时超过正常两倍以上的内存开销, 在android中你需要严格避免使用枚举
- java中的每个类(包含匿名内部类)大约使用500个字节
- 每个类实例在运行内存(RAM)中占用12到16个字节
- 在hashmap中放入单项数据时, 需要为额外的其它项分配内存, 总共占用32个字节
** ♛ 8. 当心抽象代码**
通常来说, 使用简单的抽象是一种好的编程习惯, 因为一定程度上的抽象可以提供代码的伸缩性和可维护性.。
但是,抽象会带来非常显著的开销
- 需要执行更多的代码
- 需要更长时间和更多的运行内存把代码映射到内存
所以如果抽象没有带来显著的效果就尽量避免。
** ♛ 9. 为序列化数据使用nano protobufs**
<code>Protocol buffers</code>是Google为序列化结构数据而设计的一种语言无关、平台无关,具有良好扩展性的协议。类似xml,比其更加轻量、快速、简单。
如果需要为你的数据实现协议化,在客户端代码使用<code>nano protobufsbuffers</code>。
通常协议化操作会生成大量繁琐的代码,容易给App带来许多问题:
- 增加RAM使用量
- 增加APK大小
- 更慢的执行速度
- 更容易达到DEX的字符限制
** ♛ 10. 尽量避免使用依赖注入框架**
使用像Guice和RoboGuice依赖注入框架会有很大的吸引力, 因为它使我们可以写一些更简单的代码和提供自适应的环境用来进行有用的测试和进行其它配置的更改。
然而,这些框架通过注解的方式扫描你的代码来执行一系列的初始化, 但是这些也会把一些不需要的大量的代码映射到内存中,被映射后的数据会被分配到干净的内存中,放入到内存中后很长一段时间都不会使用,这样造成内存大量的浪费。
** ♛ 11. 谨慎使用外部依赖库**
许多的外部依赖库往往不是在移动环境下写出来的,这样当在移动使用中使用这些库时就会非常低效。所以当你决定使用一个外部库时,你就要承担为优化为移动应用外部库带来的移植问题和维护负担。在项目计划前期就要分析该类库的授权条件,代码量,内存的占用再来决定是否使用该库。
不要因为1、2个功能导入整个library,如果没有合适的库与需求相吻合,应该考虑自己去实现。
PS:针对Android而设计的库也有潜在的风险,因为每个库做的事情都不一样。例如, 一个库可能使用的是 nano protobuf,另外一个库使用的是micro protobuf, 现在在你的App中有两个不同protobuf的实现,这将会有不同的日志, 分析, 图片加载框架, 缓存等冲突发生。
Proguard不会保存你的这些, 因为所有低级别的api依赖需要你依赖的库里所包含的特征。当你使用从外部库继承的activity时尤其会成为一个问题(因为这往往产生大量的依赖)。库要使用反射(这是常见的因为你要花许多时间去调整ProGuard使它工作)等.
** ♛ 12. 优化整体性能**
使用代码混淆工具ProGuard通过去除不需要的代码和通过语义模糊来重命名类, 字段和方法来缩小, 优化和混淆代码。能使代码更简洁, 更少量的RAM映射页。
** ♛ 13. 对最终的APK使用zipalign**
如果构建apk后你没有做后续的任何处理(包括根据你的证书进行签名), 你必须运行<code>zipalign</code>工具为你的apk重新签名, 如果不这样做会导致你的应用使用更多的内存, 因为像资源这样的东西不会再从apk中进行映射(mmaped)。
PS:goole play store不接受没有签名的apk。
** ♛ 15.使用多进程**
一种更高级的技术能管理应用中的内存,分离组件技术能把单进程内存划分为多进程内存。 该技术一定要谨慎的使用并且大多数的应用都不会跑多进程,因为如果你操作不当反而会浪费更多的内存而不是减少内存。它主要用于后台和前台能各自负责不同业务的应用程序。
当你构建一个音乐播放器应用并且长时间从一个service中播放音乐时使用多进程处理对你的应用来说更恰当. 如果整个应用只有一个进程, 当前用户却在另外一个应用或服务中控制播放时, 却为了播放音乐而运行着许多不相关的用户界面会造成许多的内存浪费. 像这样的应用可以分隔为两个进程:
- 一个进程负责 UI 工作
- 另外一个则在后台服务中运行其它的工作
在各个应用的manifest文件中为各个组件申明<code>android:process</code>属性就可以分隔为不同的进程。你可以指定你一运行的服务从主进程中分隔成一个新的进程来并取名为background(当然名字可以任意取)。
<service android:name=".PlaybackService"
android:process=":background" />
空进程的内存占用是相当显著的, 当你的应用加入了许多业务后会增长得更加迅速。
如果你的想在应用中使用多进程。只能有一个进程来负责UI的工作, 在其它进程中不能出现任何UI的工作, 否则会迅速提高内存的使用率(尤其当你加载bitmap资源和其它资源时)。一旦加入了UI的绘制工作就不可能会减少内存的使用了。
✩ 响应速度优化
核心思想:避免在主线程中做耗时操作。可以采用异步的方式执行耗时操作。响应速度过慢更多体现在Activity的启动速度上,如果主线程做太多事情,Activity启动会出现黑屏,甚至ANR。
Android规定
- Activity超过5s之内无法响应屏幕触摸事件或者键盘输入事件就会出现ANR。
- BroadcastReceiver10s之内还未执行完操作也会出现ANR。
ANR日志分析
当一个进程发生ANR之后,系统会在<code>/data/anr</code>目录下创建一个文件<code>traces.txt</code>,通过分析该文件就能定位出ANR原因。
✩ ListView和Bitmap优化
✩ 线程优化
采用线程池,避免程序中存在大量Thread。线程池可以重用内部的线程,从而避免了线程创建销毁带来的性能开销,同时线程池可以有效控制线程池的最大并发数,避免大量线程因抢占系统资源从而发生阻塞现象。