前言
在内存使用过程中使用不当或者超过 heap size limit 的时候就会出现 OOM, 那一般 OOM 是怎么产生的, 会导致什么样的结果呢?
OOM 简介
OOM 全称为 Out of memory, 解释为内存溢出.
为了整个 Android 系统的内存控制需要, Android 系统为每一个应用程序都设置了一个硬性的 Dalvik Heap Size 最大限制阈值, 这个阈值在不同的设备上会因为 RAM 大小不同而各有差异. 如果你的应用占用内存空间已经接近这个阈值, 此时再尝试分配内存的话, 很容易引起 OutOfMemoryError 的错误.
ActivityManager.getMemoryClass()可以用来查询当前应用的 Heap Size 阈值(prop dalvik.vm.heapgrowthlimit 也可以), 这个方法会返回一个整数, 表明你的应用的 Heap Size 阈值是多少 Mb(megabates).
OOM 产生原因
关于 Native Heap,Dalvik Heap,Pss 等内存管理机制比较复杂, 这里不展开描述. 简单的说, 通过不同的内存分配方式 (malloc/mmap/JNIEnv/etc) 对不同的对象 (bitmap,etc) 进行操作会因为 Android 系统版本的差异而产生不同的行为, 对 Native Heap 与 Dalvik Heap 以及 OOM 的判断条件都会有所影响. 在 2.x 的系统上, 我们常常可以看到 Heap Size 的 total 值明显超过了通过 getMemoryClass()获取到的阈值而不会发生 OOM 的情况, 那么针对 2.x 与 4.x 的 Android 系统, 到底是如何判断会发生 OOM 呢?
Android 2.x 系统 GC LOG 中的 dalvik allocated + external allocated + 新分配的大小>= getMemoryClass()值的时候就会发生 OOM. 例如, 假设有这么一段 Dalvik 输出的 GC LOG:GC_FOR_MALLOC free 2K, 13% free 32586K/37455K, external 8989K/10356K, paused 20ms, 那么 32586+8989+(新分配 23975)=65550>64M 时, 就会发生 OOM.
Android 4.x 系统 Android 4.x 的系统废除了 external 的计数器, 类似 bitmap 的分配改到 dalvik 的 java heap 中申请, 只要 allocated + 新分配的内存>= getMemoryClass()的时候就会发生 OOM, 如下图所示(虽然图示演示的是 art 运行环境, 但是统计规则还是和 dalvik 保持一致)
如何避免 OOM
前面介绍了 OOM 的基础知识, 那么在实践中有什么方法来减少 OOM 的出现呢? 总结下来大概分下面几个方面:
减小对象的内存占用
内存对象的重复使用
避免对象的内存泄漏
内存使用策略优化
减小对象的内存占用
1, 使用更轻量级的数据结构
例如, 我们可以考虑使用 ArrayMap/SparseArray 而不是 HashMap 等传统数据结构, 下图演示了 HashMap 的简要工作原理, 相比起 Android 系统专门为移动操作系统编写的 ArrayMap 容器, 在大多数情况下, 都显示效率低下, 更占内存. 通常的 HashMap 的实现方式更加消耗内存, 因为它需要一个额外的实例对象来记录 Mapping 操作. 另外, SparseArray 更加高效在于他们避免了对 key 与 value 的 autobox 自动装箱, 并且避免了装箱后的解箱.
下图是 HashMap 的工作原理:
下面是 ArrayMap 的 delete 原理:
两者是有区别的
2, 避免在 Android 中使用 enum
Android 官方的 Training 中有这样一句话 "Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android."
关于 enum 的效率, 请看下面的讨论. 假设我们有这样一份代码, 编译之后的 dex 大小是 2556 bytes, 在此基础之上, 添加一些如下代码, 这些代码使用普通 static 常量相关作为判断值:
增加上面那段代码之后, 编译成 dex 的大小是 2680 bytes, 相比起之前的 2556 bytes 只增加 124 bytes. 假如换做使用 enum, 情况如下:
使用 enum 之后的 dex 大小是 4188 bytes, 相比起 2556 增加了 1632 bytes, 增长量是使用 static int 的 13 倍. 不仅仅如此, 使用 enum, 运行时还会产生额外的内存占用, 如下图所示:
Android 官方强烈建议不要在 Android 程序里面使用到 enum.
3, 减小 Bitmap 对象的内存占用
Bitmap 是一个极容易消耗内存的大胖子, 减小创建出来的 Bitmap 的内存占用是很重要的, 通常来说有下面 2 个措施:
inSampleSize: 缩放比例, 在把图片载入内存之前, 我们需要先计算出一个合适的缩放比例, 避免不必要的大图载入.
decode format: 解码格式, 选择 ARGB_8888/RBG_565/ARGB_4444/ALPHA_8, 存在很大差异.
4, 使用更小的图片
在设计给到资源图片的时候, 我们需要特别留意这张图片是否存在可以压缩的空间, 是否可以使用一张更小的图片. 尽量使用更小的图片不仅仅可以减少内存的使用, 还可以避免出现大量的 InflationException. 假设有一张很大的图片被 xml 文件直接引用, 很有可能在初始化视图的时候就会因为内存不足而发生 InflationException, 这个问题的根本原因其实是发生了 OOM.
内存对象的重复使用
大多数对象的复用, 最终实施的方案都是利用对象池技术, 要么是在编写代码的时候显式的在程序里面去创建对象池, 然后处理好复用的实现逻辑, 要么就是利用系统框架既有的某些复用特性达到减少对象的重复创建, 从而减少内存的分配与回收.
1, 复用系统自带的资源
Android 系统本身内置了很多的资源, 例如字符串 / 颜色 / 图片 / 动画 / 样式以及简单布局等等, 这些资源都可以在应用程序中直接引用. 这样做不仅仅可以减少应用程序的自身负重, 减小 APK 的大小, 另外还可以一定程度上减少内存的开销, 复用性更好. 但是也有必要留意 Android 系统的版本差异性, 对那些不同系统版本上表现存在很大差异, 不符合需求的情况, 还是需要应用程序自身内置进去.
2, 注意在 ListView/GridView 等出现大量重复子组件的视图里面对 ConvertView 的复用
3,Bitmap 对象的复用
4, 避免在 onDraw 方法里面执行对象的创建
类似 onDraw 等频繁调用的方法, 一定需要注意避免在这里做创建对象的操作, 因为他会迅速增加内存的使用, 而且很容易引起频繁的 gc, 甚至是内存抖动.
5,StringBuilder
在有些时候, 代码中会需要使用到大量的字符串拼接的操作, 这种时候有必要考虑使用 StringBuilder 来替代频繁的 "+".
避免对象的内存泄漏
内存对象的泄漏, 会导致一些不再使用的对象无法及时释放, 这样一方面占用了宝贵的内存空间, 很容易导致后续需要分配内存的时候, 空闲空间不足而出现 OOM. 显然, 这还使得每级 Generation 的内存区域可用空间变小, gc 就会更容易被触发, 容易出现内存抖动, 从而引起性能问题.
最新的 LeakCanary 开源控件, 可以很好的帮助我们发现内存泄露的情况, 更多关于 LeakCanary 的介绍, 请看这里(中文使用说明). 另外也可以使用传统的 MAT 工具查找内存泄露, 请参考这里(便捷的中文资料)
1, 注意 Activity 的泄漏
通常来说, Activity 的泄漏是内存泄漏里面最严重的问题, 它占用的内存多, 影响面广, 我们需要特别注意以下两种情况导致的 Activity 泄漏:
内部类引用导致 Activity 的泄漏
最典型的场景是 Handler 导致的 Activity 泄漏, 如果 Handler 中有延迟的任务或者是等待执行的任务队列过长, 都有可能因为 Handler 继续执行而导致 Activity 发生泄漏. 此时的引用关系链是 Looper -> MessageQueue -> Message -> Handler -> Activity. 为了解决这个问题, 可以在 UI 退出之前, 执行 remove Handler 消息队列中的消息与 runnable 对象. 或者是使用 Static + WeakReference 的方式来达到断开 Handler 与 Activity 之间存在引用关系的目的.
Activity Context 被传递到其他实例中, 这可能导致自身被引用而发生泄漏.
内部类引起的泄漏不仅仅会发生在 Activity 上, 其他任何内部类出现的地方, 都需要特别留意! 我们可以考虑尽量使用 static 类型的内部类, 同时使用 WeakReference 的机制来避免因为互相引用而出现的泄露.
2, 考虑使用 Application Context 而不是 Activity Contex
对于大部分非必须使用 Activity Context 的情况(Dialog 的 Context 就必须是 Activity Context), 我们都可以考虑使用 Application Context 而不是 Activity 的 Context, 这样可以避免不经意的 Activity 泄露.
3,Bitmap 对象的及时回收
虽然在大多数情况下, 我们会对 Bitmap 增加缓存机制, 但是在某些时候, 部分 Bitmap 是需要及时回收的. 例如临时创建的某个相对比较大的 bitmap 对象, 在经过变换得到新的 bitmap 对象之后, 应该尽快回收原始的 bitmap, 这样能够更快释放原始 bitmap 所占用的空间.
需要特别留意的是 Bitmap 类里面提供的 createBitmap()方法:
这个函数返回的 bitmap 有可能和 source bitmap 是同一个, 在回收的时候, 需要特别检查 source bitmap 与 return bitmap 的引用是否相同, 只有在不等的情况下, 才能够执行 source bitmap 的 recycle 方法.
4, 注意监听器的注销
在 Android 程序里面存在很多需要 register 与 unregister 的监听器, 我们需要确保在合适的时候及时 unregister 那些监听器. 自己手动 add 的 listener, 需要记得及时 remove 这个 listener.
5, 注意缓存容器中的对象泄漏
有时候, 我们为了提高对象的复用性把某些对象放到缓存容器中, 可是如果这些对象没有及时从容器中清除, 也是有可能导致内存泄漏的. 例如, 针对 2.3 的系统, 如果把 drawable 添加到缓存容器, 因为 drawable 与 View 的强应用, 很容易导致 activity 发生泄漏. 而从 4.0 开始, 就不存在这个问题. 解决这个问题, 需要对 2.3 系统上的缓存 drawable 做特殊封装, 处理引用解绑的问题, 避免泄漏的情况.
6, 注意 webView 的泄漏
Android 中的 WebView 存在很大的兼容性问题, 不仅仅是 Android 系统版本的不同对 WebView 产生很大的差异, 另外不同的厂商出货的 ROM 里面 WebView 也存在着很大的差异. 更严重的是标准的 WebView 存在内存泄露的问题, 看这里 WebView causes memory leak - leaks the parent Activity. 所以通常根治这个问题的办法是为 WebView 开启另外一个进程, 通过 AIDL 与主进程进行通信, WebView 所在的进程可以根据业务的需要选择合适的时机进行销毁, 从而达到内存的完整释放.
7, 注意 Cursor 对象是否及时关闭
在程序中我们经常会进行查询数据库的操作, 但时常会存在不小心使用 Cursor 之后没有及时关闭的情况. 这些 Cursor 的泄露, 反复多次出现的话会对内存管理产生很大的负面影响, 我们需要谨记对 Cursor 对象的及时关闭.
内存使用策略优化
1,Try catch 某些大内存的操作
在某些情况下, 我们需要事先评估那些可能发生 OOM 的代码, 对于这些可能发生 OOM 的代码, 加入 catch 机制, 可以考虑在 catch 里面尝试一次降级的内存分配操作. 例如 decode bitmap 的时候, catch 到 OOM, 可以尝试把采样比例再增加一倍之后, 再次尝试 decode.
2, 谨慎使用 static 对象
static 是 Java 中的一个关键字, 当用它来修饰成员变量时, 那么该变量就属于该类, 而不是该类的实例. 不少程序员喜欢用 static 这个关键字修饰变量, 因为他使得变量的生命周期大大延长啦, 并且访问的时候, 也极其的方便, 用类名就能直接访问, 各个资源间 传值也极其的方便, 所以, 它经常被我们使用. 但如果用它来引用一些资源耗费过多的实例(Context 的情况最多), 这时就要谨慎对待了.
- public class ClassName {
- private static Context mContext;
- // 省略
- }
以上的代码是很危险的, 如果将 Activity 赋值到么 mContext 的话. 那么即使该 Activity 已经 onDestroy, 但是由于仍有对象保存它的引用, 因此该 Activity 依然不会被释放, 并且, 如果该 activity 里面再持有一些资源, 那就糟糕了.
上面是直接的引用泄露, 我们再看 google 文档中的一个例子:
- private static Drawable sBackground;
- @Override
- protected void onCreate(Bundle state) {
- super.onCreate(state);
- TextView label = new TextView(this);
- label.setText("Leaks are bad");
- if (sBackground == null) {
- sBackground = getDrawable(R.drawable.large_bitmap);
- }
- label.setBackgroundDrawable(sBackground);
- setContentView(label);
- }
sBackground, 是一个静态的变量, 但是我们发现, 我们并没有显式的保存 Contex 的引用, 但是, 当 Drawable 与 View 连接之后, Drawable 就将 View 设置为一个回调, 由于 View 中是包含 Context 的引用的, 所以, 实际上我们依然保存了 Context 的引用. 这个引用链如下:
Drawable->TextView->Context
所以, 最终该 Context 也没有得到释放, 也发生了内存泄露.
那我们如何的避免这种泄露的发生呢?
应该尽量避免 static 成员变量引用资源耗费过多的实例, 比如 Context.
Context 尽量使用 Application Context, 因为 Application 的 Context 的生命周期比较长, 引用它不会出现内存泄露的问题.
使用 WeakReference 代替强引用. 比如可以使用 WeakReference<Context> mContextRef;
该部分的详细内容也可以参考 Android 文档中 Article 部分.
3, 特别留意单例模式的不合理持有
4, 珍惜 service 资源
如果你的应用需要在后台使用 service, 除非它被触发并执行一个任务, 否则其他时候 Service 都应该是停止状态. 另外需要注意当这个 service 完成任务之后因为停止 service 失败而引起的内存泄漏. 当你启动一个 Service, 系统会倾向为了保留这个 Service 而一直保留 Service 所在的进程. 这使得进程的运行代价很高, 因为系统没有办法把 Service 所占用的 RAM 空间腾出来让给其他组件, 另外 Service 还不能被 Paged out. 这减少了系统能够存放到 LRU 缓存当中的进程数量, 它会影响应用之间的切换效率, 甚至会导致系统内存使用不稳定, 从而无法继续保持住所有目前正在运行的 service. 建议使用 IntentService, 它会在处理完交代给它的任务之后尽快结束自己. 更多信息, 请阅读 Running in a Background Service.
5, 优化布局层次, 减少内存消耗
越扁平化的视图布局, 占用的内存就越少, 效率越高. 我们需要尽量保证布局足够扁平化, 当使用系统提供的 View 无法实现足够扁平的时候考虑使用自定义 View 来达到目的.
6, 谨慎使用 "抽象" 编程
很多时候, 开发者会使用抽象类作为 "好的编程实践", 因为抽象能够提升代码的灵活性与可维护性. 然而, 抽象会导致一个显著的额外内存开销: 他们需要同等量的代码用于可执行, 那些代码会被 mapping 到内存中, 因此如果你的抽象没有显著的提升效率, 应该尽量避免他们.
最后
如果你看到了这里, 觉得文章写得不错就点个赞呗? 如果你觉得那里值得改进的, 请给我留言. 一定会认真查询, 修正不足. 谢谢.
最后针对 Android 开发的同行, 小编这边给大家整理了一些资料, 其中分享内容包括但不限于 [高级 UI, 性能优化, 移动架构师, NDK, 混合式开发(ReactNative+Weex) 微信小程序, Flutter 等全方面的 Android 进阶实践技术] 希望能帮助大家学习提升进阶, 也节省大家在网上搜索资料的时间来学习, 也是可以分享给身边好友一起学习的!
为什么某些人会比你优秀, 是因为他本身就很优秀还一直在持续努力变得更优秀, 而你是不是还在满足于现状内心在窃喜! 希望读到这的您能转发分享和关注一下我, 以后还会更新技术干货, 谢谢您的支持!
来源: http://www.jianshu.com/p/8e02d8b235b3