OOM相关问题分析
什么是OOM
当前占用的内存加上我们申请的内存资源超过虚拟机的最大内存限制就会抛出OOM(out of memory )异常;
android中,系统会为每一个app分配内存空间,其实就是zygote进程通过fork操作拷贝自己,
这样每个app都可以运行在独立的进程空间内,不受其他app的影响
但是虚拟机会为每个app对应的虚拟机都设置了最大内存限制,如果当前占用内存加上申请的内存资源超过这个最大限制,就会报OOM;
开发过程中最常见的是大图加载操作造成OOM
大部分OOM问题都和Bitmap大图加载有关。
一些容易混淆的概念 内存溢出/内存抖动/内存泄漏
内存溢出
内存溢出就是前面说的OOM
当前占用的内存加上申请的内存资源超过系统限制的最大内存就会产生OOM
内存抖动
短时间内大量对象被创建,又在短时间内被销毁释放
瞬间产生的对象会严重占用内存区域;
什么是内存抖动:
以下总结来自 抖音视频号 扔物线
在程序里,每创建一个对象,就会有一块内存分配给它,
每分配一块内存,程序的可用内存就少了一块,
当程序占用的内存达到一定程度,
GC也就是垃圾回收器(Garbage Collector)就会出动,来回收掉一部分不再使用的内存,
Android里的View.onDraw()方法在每次需要被重绘时都会被调用,
这就意味着,如果你在onDraw方法里写了创建对象的代码,那么在界面频繁刷新的时候,
你也就会频繁的创建出一大批只被使用一次的对象,这就会导致内存的迅速攀升。
然后可能很快就会触发GC的回收动作,也就是那些被你创造出来的对象被GC回收掉。
垃圾内存太多了,就被清理掉,这是java的工作机制,这不是问题。
问题在于,频繁创建这些对象会导致内存不断地攀升,在刚回收了之后又迅速涨起来,那么紧接着的就是又一次回收。
这么往复下来就导致一种循环,一种在短时间内反复的发生内存增长和回收的循环。
这种循环往复的状态就像是水波纹的颤动一样,它的专业称呼叫做Memory churn。
Android官方文档里将其翻译为内存抖动
所以内存抖动并不是我们的内存在整体的进行摇晃这样神奇的事情,而仅仅是类似有一根搅拌棒轻轻的在内存的边际上进行搅动。
我们也可以通过android studio的memory profiler工具来更直观的观察到这种现象。
内存的回收虽然很快,时间成本很低,但终究是有时间成本的,一两次内存回收不容易被用户察觉,
但多次内存回收行为在短时间内集中爆发,这就造成了比较大的界面卡顿的风险。
这也是为什么Android 在官方文档和Androidstudio里面都建议我们避免在onDraw方法里创建对象;
同样的道理不止是在onDraw,在次数比较大的循环里创建对象,同样会导致内存抖动。
不过在实践中,我们在onDraw方法里创建的对象往往是和绘制相关的对象,而这些对象又经常包含通往系统底层的native对象的引用。
这就导致在onDraw里创建对象所导致的内存回收的耗时会更高,更直白的说就是界面更卡顿。
另外呢,内存抖动有时候也会抖着抖着就变成内存溢出了,
这个是更严重的结果,因为内存溢出的直接结果就是软件崩溃。
内存泄漏
内存泄漏就是本该回收的内存没有被回收掉,
什么是本该回收呢,比如Activity执行了onDestroy方法后,生命周期结束,就应该被回收了。
为什么没有被回收掉,在垃圾回收的可达性分析算法条件下,可能是对象直接或者间接的跟GC root对象产生了引用链。导致无法被回收。
比如一个还在运行的Handler仍然持有这个Activity的引用,导致Activity无法被回收。
内存泄漏到一定程度也会导致OOM.
如何解决OOM
1. bitmap处理
图片显示
加载合适尺寸的图片,当显示缩略图的时候不要调用网络请求去加载大图
比如在listview,通过监听滑动事件,滑动的时候不去调用网络请求,只有监听到listview滑动停止的时候再去加载大图,把图片显示出来。
及时释放内存
由于java的垃圾回收机制是不定期的回收内存空间,注意是不定期,不能指定一个时间段去回收内存。
由于bitmap创建是通过BitmapFactory来生成bitmap对象,而生成bitmap对象是通过jni。
所以说加载Bitmap到内存以后,是包含两部分内存,一部分是java内存区域,可以被垃圾回收清理,一部分是native内存,这部分虚拟机是无法回收的。
这块区域只能调用底层功能释放。所以这里释放内存,释放的就是native区域的内存。
看下bitmap源码,其recycle方法也是调用jni实现的。
public void recycle() {
if (!mRecycled) {
nativeRecycle(mNativePtr);
mNinePatchChunk = null;
mRecycled = true;
}
}
那么如果不调用recycle是否就一定会造成内存泄漏进而造成OOM呢?
其实也不一定,因为android的每个应用都运行在独立的进程当中,有独立的内存,如果这个进程被杀死了,内存也就被释放掉了,其实也包括native部分的内存。
图片压缩
我们开始的时候可能会需要加载一张很大的图片,这个大图直接超过了内存分配的大小。这样就肯定会导致内存溢出。
所以这时候就需要对加载bitmap的大小进行控制。也就是进行图片压缩。
而对bitmap进行压缩,需要用到BitmapFactory的一个叫inSampleSize的属性,
在bitmap加载到内存之前,计算一个合适的缩放比例,避免不必要的大图载入。
/**
* If set to a value > 1, requests the decoder to subsample the original
* image, returning a smaller image to save memory. The sample size is
* the number of pixels in either dimension that correspond to a single
* pixel in the decoded bitmap. For example, inSampleSize == 4 returns
* an image that is 1/4 the width/height of the original, and 1/16 the
* number of pixels. Any value <= 1 is treated the same as 1. Note: the
* decoder uses a final value based on powers of 2, any other value will
* be rounded down to the nearest power of 2.
*/
public int inSampleSize;
inBitmap属性
BitmapFactory的inBitmap属性,可以提高android系统在bitmap的分配和释放的效率。
这个inBitmap属性,可以告知bitmapdecode解码器,使用已经存在的内存区域,而不是重新申请一块内存区域放bitmap.
即复用之前在堆内存中分配的bitmap内存区域,
就是说你即便有成百上千张图片,也只占用你屏幕能放下图片的内存。
这也是android系统对bitmap的内存优化。
public Bitmap inBitmap;
捕获异常
在android系统里,虚拟机对分配给bitmap的内存大小是有限制的,为了避免系统在为bitmap分配内存时出现OOM,我们在实例化一个bitmap时一定要对其进行异常捕获。
在java或者android中,捕获Exception是捕获不到OutOfMemoryError这个异常的。
因为OutofMemoryError它是个错误,不是一个异常。
所以这里要注意,捕获的是Error,是异常属性。
其他方法
listview convertView / lru
通过convertView复用view,
通过lru机制缓存bitmap
避免在onDraw方法里面执行对象的创建
上面内存抖动已经介绍。
谨慎使用多进程
多进程就是把进程中的部分组件运行在单进程中。
比如可以把app当中的定位,webview可以开启一个单独的进程避免内存泄漏。这样可以扩大应用的内存占用范围。
因为开启了其他进程就不用占用主进程。
但是这个技术是必须谨慎使用的,如果你的app业务没有大到一定程度,尽量少用多进程。
一方面是因为牵扯到多进程,代码必然更加复杂,逻辑更加烦繁琐。
而且一旦使用不当,不仅仅是造成内存增长,也会造成其他莫名其妙的crash