Android开发进阶(从小工到专家)读书笔记——内存优化
开发的APP:MyApp
Random Access Memory(RAM)在任何软件开发环境中都是一个很宝贵的资源。 尽管 Android 的 Dalvik 虚拟机扮演了常规的垃圾回收角色,但并不意味着可以忽视 App 的内存分配与释放的时机与地点。这里总结一些内存管理的知识,以及在开发 Android 应用时如何主动减少内存的使用。
1. 珍惜 Services 资源
- Service 在非触发执行任务阶段,都应该是非运行状态。
- Service 在完成任务后可能会因为停止 Service 失败而引起泄露。
- 启动一个 Service,系统会倾向为了保留 Service 而一直保留 Service 所在进程,这使得进程的运行代价很高,因为系统没办法把 Service 所占用的 RAM 让给其他组件或者被 paged out。
解决途径
1. 使用 IntentService,它会在处理完扔给它的 intent 任务之后尽快结束自己。
2. 在 Service 不需要的时候结束他。
2. 当UI隐藏是释放内存
- 当切换到其他应用并且MyApp的UI不可见时,应释放UI上所占用的所有资源,可显著增加系统缓存进程的能力。
- 为了接收用户离开MyApp的UI通知,需实现 Activity 类里面的 onTrimMemory() 回调方法。
- 监听 TRIM_MEMORY_UI_HIDDEN 级别的的回调,此时意味着MyApp的UI已经隐藏,应该释放那些被MyApp使用的资源。
- 为了接收用户离开MyApp的UI通知,需实现 Activity 类里面的 onTrimMemory() 回调方法。
注意:只有在MyApp所有UI组件被隐藏是才会收到 onTrimMemory() 的回调并带有参数 TRIM_MEMORY_UI_HIDDEN。与 onStop() 的回调不同。onStop 会在 Activity 的实例隐藏时执行,如MyApp的某一个 Activity 跳转到另一个 Activity 时,onStop() 会被执行,因此,应该实现 onStop 回调,并且在此回调里面释放 Activity 的资源,例如,网络连接,unregister 广播接受者;除非接收到 onTrimMemory(TRIM_MEMORY_UI_HIDDEN) 的回调,否则不应该释放MyApp的UI资源。
3. 当内存紧张是释放部分内存
MyApp生命周期的任何阶段,onTrimMemory 回调方法可以获取整个设备的内存资源,可根据 onTrimMemory 方法中的内存级别来进一步决定释放哪些资源
- TRIM_MEMORY_RUNNING_MODERATE: MyApp正在运行并且不会被列为可杀死。但是,设备此时正运行于低内存状态下,系统开始出发杀死LRU Cache中的Process机制。
- TRIM_MEMORY_RUNNING_LOW: MyApp正在运行并且不会被列为可杀死的。但是,设备正运行于更低内存的状态下,应该释放不用的资源来提升系统性能。
- TRIM_MEMORY_RUNNING_CRITICAL: MyApp仍在运行,但是系统已经把 LRU Cache 中的大多数进程都杀死了,因此,应该立即释放所有非必须的资源。如果系统不能回收到足够的RAM数量,系统将会清除所有的 LRU 缓存中的资源,并且开始杀死那些之前被认为不应该杀死的进程。
在MyApp进程被 cached 时,可能会接受到从 onTrimMemory() 中返回的下面的值之一:
- TRIM_MEMORY_BACKGROUND: 系统正运行于低内存状态并且MyApp的进程正处于LRU缓存中最不容易杀掉的位置。此时系统可能已经开始杀掉 LRU Cache 中的其他进程了,应该释放那些容易恢复的资源,以便MyApp的进程可以保留下来。
- TRIM_MEMORY_MODERATE: 系统正运行于低内存状态并且MyApp的进程已经接近 LRU Cache 名单的中部位置,如果系统开始变得更加内存紧张,MyApp进程是有可能被杀死的。
- TRIM_MEMORY_COMPLETE: 系统正运行与低内存的状态并且MyApp进程正处于 LRU Cache 名单中最容易被杀掉的位置,应该释放任何不影响MyApp恢复状态的资源。
onTrimMemory() 的回调是在API 14 才被加进来的,对于低版本,可以使用 onLowMemory 回调来兼容,相当于 TRIM_MEMORY_COMPLETE
- Note: 当系统开始清除 LRU Cache 中的进程是,尽管他首先按照LRU的顺序来操作,但同样会考虑进程的内存使用量,消耗越低的进程越容易被留下来。
4. 检查应该使用多少内存
- 可通过 getMemoryClass() 来获取MyApp的可用 heap 大小,如果MyApp尝试申请更多的内存,会出现 OutOfMemory 的错误。
- 可通过在 manifest 的 application 标签下添加 largeHeap=true 的属性来声明一个更大的空间,此时可通过 getLargeMemoryClass() 来获取到一个更大的 heap size。
- 不要轻易地因为MyApp需要使用大量的内存而去请求一个大的 heap size,使用额外的内存会影响系统整体的用户体验,并且会使GC的每次运行时间更长,在任务切换时,系统的性能会变得大打则扣。large heap 并不一定能够取得更大的heap,在某些严格限制的机器上,large heap 的大小通常跟 heap size 的大小一样,此时,可以通过执行 getMemoryClass() 来检查实际获取到的 heap 大小。
5. 避免 Bitmaps 的浪费
- 当加载一个 bitmap 时,仅保留适配当前屏幕分辨率的数据即可,若原图高于设备分辨率,需要做缩小动作。增加 bitmap 的尺寸会对内存呈现出 2 次方的增加。(X 与 Y 都在增加)
- 如果MyApp需要使用图片加载功能,使用成熟的 ImageLoader 框架。如 Glide、Picasso、Fresco 等。
6. 使用优化的数据容器
- 利用 Android Framework 中优化过的容器类,如 SparseArray、SparseBooleanArray、LongSparseArray。通常的 HaspMap 实现方式需要一个额外的实例对象来记录 Mapping 操作,更加消耗内存,而 SparseArray 避免了对 key 与 value 的 autobox 自动封装,避免了装箱后的解箱。
7. 注意内存开销
- 对所使用的语言与库的成本与开销有所了解,在设计App的过程中谨记这些信息,如:
- Enums 的内存消耗通常是 static constants 的 2 倍,应尽量避免在 Android 上使用 enums 。
- Java 中的每一个类(包括匿名内部类)都会使用大概 500bytes 。
- 每一个类的实例产生的花销是 12 - 16 bytes。
- 往 HashMap 添加一个 entry 需要一个额外占用的 32 bytes 的 entry 对象。
8. 注意代码“抽象”
- 抽象能够提升代码的灵活性与可维护性,但抽象也会导致一个显著的开销:通常他们需要同等量的代码用于可执行,那些代码会被 map 到内存中。因此,如果抽象没有显著的提升效率,应尽量避免。
9. 为序列化的数据使用 nano protobufs
- Protocal buffers 是 Google 为序列化结构数据而设计的,一种语言无关,平台无关,具有良好扩展性的协议。类似 xml,却比 xml 更加轻量、快速、简单。若为数据实现协议化,应在客户端的代码中总是使用 nano protobufs。协议化操作会生成大量繁琐的代码,容易给APP带来许多问题:增加RAM的使用量,显著增加 APK 的大小,更慢的执行速度,更容易达到DEX的字符限制。
10. 避免使用依赖注入框架
- 依赖注入框架会通过扫描APP的代码执行许多初始化操作,这会导致代码需要大量的 RAM 来 map 代码,mapped pages 会长时间的被保留在 RAM 中。
11. 谨慎使用外部库
- 很多 library 的代码都不是为了移动开发环境而编写的,运用到移动开发是会导致影响APP效率。
- 每一个 library 所做的事情都是不一样的,不同的 lib 有不同的实现方式,这样的冲突可能会发生在输出日志、加载图片、缓存等模块里面。
- 不要陷入为了少数的功能而导入整个 library 的陷阱。
12. 优化整体代码
- 参考 Google 官方文档:Best Practices for Performance,阅读 optimizing your UI 来为 layout 进行优化。
13. 使用 ProGuard 剔除不需要的代码
14. 对最终的APK使用 zipalign
- 如果不做这个步骤,会导致 MyApp 需要更多的 RAM,因为一些类似图片资源的东西不能被 mapping。注意:Google Play 不接受没经过 zipalign 的 APK。
15. 使用多进程
- 通过把App的组件切分成多个组件,运行在不同的进程中。适用情况:当App需要在后台运行与前台一样的大量的任务的时候。
- 如:可长时间后台播放的 Music Player,这样的 App 可切分成 2 个进程:一个操作 UI,另一个用来后台的 Service。
- 可通过在 manifest 文件中声明
android:progress
属性来实现某个组件运行在另一个进程的操作:
<service android:name=".PlaybackService" android:process="backgroung" />