Android 之内存相关
内存泄漏和内存溢出的区别
内存溢出
内存溢出是指当对象的内存占用已经超过分配内存的空间大小,这时未经处理的异常就会抛出。比如常见的内存溢出的情况有:bitmap 过大;引用没释放;资源对象没关闭
内存溢出的原因
- 内存泄漏导致
由于我们程序的失误,长期保持某些资源(如 Context)的引用,垃圾回收器就无法回收它,当然该对象占用的内存就无法被使用,这就造成内存泄漏。
Android 的每个应用程序都会使用一个专有的 Dalvik 虚拟机实例来运行,它是由 Zygote 服务进程孵化出来的,也就是说每个应用程序都在属于自己的进程中运行的。Android 为不同类型的进程分配了不同的内存使用上限,如果程序在运行过程中出现了内存泄漏的而造成应用进程使用的内存超过了这个上限,则会被系统视为内存泄漏,从而被 kill 掉。
占用内存较多的对象
保存了多个耗用内存过大的对象(如 Bitmap)或加载单个超大的图片,造成内存超出限制。
内存泄漏
在对象的生命周期本该结束的时候,这个对象还被一系列的引用,这就会导致内存泄漏。随着泄漏的累积,app 将消耗完内存。
比如,在 Activity 的onDestroy()
被调用之后,View 树以及相关的 bitmap 都应该被垃圾回收。但是如果一个正在运行的后台线程继续持有这个 Activity 的引用,那么相关的内存将不会被回收,这最终将导致 OutOfMemoryError 崩溃。
八种可能
static Activities、static Views、inner Classes、Anonymous Classes、Handler、Thread、TimerTask、Sensor Manager
其实就是生命周期长的持有了生命周期短的对象的引用。
解决:静态内部类不持有外部类的引用,打破了链式引用。
Android 内存优化之 OOM
Android 的内存管理机制
共享内存
- Android 应用的进程都是从一个叫做 Zygote 的进程 fork 出来的。Zygote 进程在系统启动并且载入通用的 framework 的代码与资源后开始启动。为了启动一个新的程序进程,系统会 fork Zygote 进程生成一个新的进程,然后再新的进程中加载并运行应用程序的代码。这使得大多数的 RAM pages 被用来分配给 framework 的代码,同时使得 RAM 资源能够在应用的所有进程之间进行共享。
- 大多数 static 的数据被 mmapped 到一个进程中。这不仅仅使得同样的数据能够在进程间进行共享,而且使得它能够在需要的时候被 paged out。常见的 static 数据包括 Dalvik Code,app resources,so 文件等。
- 大多数情况下,Android 通过显示的分配共享内存区域(例如 ashmem 或者 gralloc)来实现动态 RAM 区域能够在不同进程之间进行共享的机制。例如,Window Surface 在 App 与 Screen Compositor 之间使用共享的内存,Cursor Buffers 在 Content Provider 与 Clients 之间共享内存。
如何避免 OOM 总结
减小对象的内存占用、内存对象的重复利用、避免对象的内存泄漏、内存使用策略优化。
减小对象的内存占用
- 使用更加轻量的数据结构:例如我们可以考虑使用 ArrayMap/SparseArray 而不是 HashMap 等传统数据结构
- 避免在 Android 里面使用 Enum
- 减小 Bitmap 对象的内存占用:
- inSampleSize:缩放比例
- decode format:解码格式
- 使用更小的图片
内存对象的重复利用
大多数对象的复用,最终实施的方案都是利用对象池技术,要么是在编写代码的时候显示的在程序里去创建对象池,然后处理好复用的实现逻辑;要么就是利用系统框架既有的某些复用特性减少对象的重复创建,从而减少内存的分配与回收。
在 Android 上面最常用的一个缓存算法是 LRU (Least Recently Use)
- 复用系统自带资源:字符串、颜色、图片、动画、样式以及简单布局等
- 注意 ListView 、GridView 等出现大量重复子组件的视图对 ConvertView 的复用
- Bitmap 对象的复用
- 在 ListView 与 GridView 等显示大量图片的控件里面需要使用 LRU 的机制来缓存 Bitmap
- 利用 inBitmap 的高级特性提高 Android 系统在 Bitmap 分配与释放执行效率上的提升(3.0 以及 4.4 以后存在一些使用限制上的差异)。使用 inBitmap 属性可以告知 Bitmap 解码器去尝试使用已经存在的内存区域,新解码的 bitmap 会尝试去使用之前那张 bitmap 在 heap 中所占据的 pixel data 内存区域,而不是去问内存重新申请一块区域来存放 bitmap。利用这种特性,即使是上千张的图片,也只会仅仅只需要占用屏幕所能够显示的图片数量的内存大小。
- 在 SDK11 ~ 18 之间,重用的 bitmap 大小必须是一致的,例如给 inBitmap 赋值的图片大小为 100-100,那么新申请的 bitmap 必须也为 100-100 才能够被重用。从 SDK 19 开始,新申请的 bitmap 大小比逊小于或者等于已经赋值过的 bitmap 大小。
- 新申请的 bitmap 与旧的 bitmap 必须有相同的解码格式
- 另外,在 2.X 的系统上,尽管 bitmap 是分配在native 层,但是还是无法避免被计算到 OOM 的引用计算器里面。不少应用hi通过反射 BitmapFactory.Options 里面的 inNativeAlloc 来达到扩大使用内存的目的,但是如果大家都这么做,对系统整体会造成一定的负面影响,建议谨慎采纳。、
- 避免在
onDraw()
执行对象的创建 - StringBuilder
避免对象的内存泄漏
工具:LeakCanary、MAT
注意 Activity 的泄漏
- 内部类引用导致 Activity 的泄漏:Handler
- Activity Context 被传递到其他实例中,这可能导致自身被引用而发生泄漏
- 解决:static + WeakReference
考虑使用 Application Context 而不是 Activity Context:
对于大部分非必须使用 Activity Context 的情况(Dialog 的 Context 就必须是 Activity Context),考虑使用 Application Context 而不是 Activity Context。
注意临时 Bitmap 对象的及时回收
注意监听器的注销
注意缓存容器中的对象泄漏
注意 WebView 的泄漏
可以为 WebView 开启另外一个进程,通过 AIDL 与主进程进行通信,WebView 所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。
注意 Cursor 对象是否及时关闭
内存使用策略优化
谨慎使用 large heap
综合考虑设备内存阈值与其他因素设计合适的缓存大小
onLowMemory()
与onTrimMemory()
onLowMemory()
:当所有的 background 应用都被 kill 掉的时候,foreground 应用都会受到onLowMemory()
的回调。在这种情况下,需要尽快释放当前应用的非必须的内存资源,从而确保系统能够继续稳定运行。onTrimMemory(int)
:当系统内存达到某些条件的时候,所有正在运行的应用都会收到这个回调,同时在这个回调里面会传递参数来告知当前内存的使用情况,应用合理的选择释放自身的一些内存占用
资源文件需要选择合适的文件夹进行存放
hdpi/xhdpi/xxhdpi/
等等不同 dpi 的文件夹下的图片在不同设备上会经过 scale 处理。在这种情况下,内存占用是会显著提高的。对于不想被拉伸的图片,需要放在 assets 或者 nodpi 的目录下。try catch 某些大内存分配的操作
在某些情况下,我们需要事先评估那些可能发生 OOM 的代码,对于这些可能发生 OOM 的代码,加入 catch 机制,可以考虑在 catch 里面尝试一次降级的内存分配操作。例如 decode bitmap 的时候,catch 到 OOM,可以尝试把采样比例再增加一倍之后,再次尝试 decode。
谨慎使用 static 对象
特别留意单例对象中不合理的持有
珍惜 Services 资源:建议使用 IntentService
优化布局层次,减少内存消耗
谨慎使用“抽象”编程
使用 nano protobufs 序列化数据
谨慎使用依赖注入框架
谨慎使用多进程
使用 ProGuard 来剔除不需要的代码
谨慎使用第三方 libraries
考虑不同的实现方式来优化内存占用