开篇废话
上一篇我们了解了Android里面相关的内存泄露以及相应的处理方案,这一篇,接着上一篇的内存泄露的内容,讲一下Android当中的内存溢出。
内存溢出与内存泄露,很多开发人员都容易产生混淆,有可能是因为这两个概念有点关系,又因为名称上也不太好区分吧。不过,我们依然要清楚,内存溢出(Out Of Memory Error) 与 内存泄露 (Memory Leak)还是有质的区别的。都我们的App多次出现内存泄露,可能就会导致内存溢出。
但是,我们的App出现内存溢出,不一定就是因为内存泄露,因为本身Android系统分配给每一个的App的空间就是那么一点。
另外,内存泄露也不一定就会出现内存溢出,因为还是泄露的速度比较慢,系统将进程杀死了,也就不会内存溢出咯,不过,发现内存泄露,我们还是要第一时间解决掉这个bug。
技术详情
讲述逻辑如下:
1.什么是内存溢出
2.有些内存里面容易混淆的概念
3.如何解决内存溢出
1.什么是内存溢出
内存溢出,OOM(Out Of Memory),表示当前占用的内存加上我们申请的内存资源超过了Dalvik虚拟机的最大内存限制就会抛出的Out Of Memory异常。大部分的OOM的问题,都会与Bitmap的加载有关系
2.内存里面容易混淆的一些概念
主要有三个概念:
1.内存溢出
2.内存抖动
3.内存泄露
其中第一个内存溢出,就是刚刚讲的OOM,第三个内存泄露,可以查看我的上一篇文章。
关于第二个内存抖动,出现的情况是,短时间内,大量的对象被创建,然后又马上被释放,瞬间产生的对象会严重占用内存区域,这个区域就是我们之前接触的那个年轻代区域,到达这个区域的阈值时就会触发minor gc,当出现频繁的minor gc的时候,就会出现内存抖动,我们能够通过我们的Android Studio的Memory Monitor能够非常直观的看到内存抖动
出现内存抖动的现象,可根据当前app处理的实际业务结合Memory Monitor中的现象来进行判断,然后有针对性的进行优化。
它们三者的重要等级分别:内存溢出 > 内存泄露 > 内存抖动
内存溢出对我们的App来说,影响是非常大的,整得不好,就有可能导致程序闪退,无响应等现象,因此,我们一定要优先解决OOM的问题。
3.如何解决内存溢出
如何解决OOM,这个问题范围比较大,我这边大概从两个方面去讲述:
1.关于Bitmap的OOM
2.除了Bitmap之外的OOM
3.1 关于Bitmap的OOM
关于Bitmap的OOM我们有几点需要注意的。
3.1.1 ImageView等控件图片的显示
意思就是加载合适属性的图片,当我们有些场景是可以显示缩略图的时候,就不要调用网络请求加载大图,例如在ListView中,我们在上下滑动的时候,就不要去调用网络请求,当监听到滑动结束的时候,才去加载大图,以免上下滑动的时候产生卡顿现象。
3.1.2 及时释放内存
我们知道,在Android系统中,本身就有自己的垃圾回收机制,系统会不定期进行垃圾回收的。但是,这个只是针对Java那一块的内存,但是我们需要知道Bitmap实例化的时候,是通过JNI的方式,所以还有一部分的内存是C那一块的,我们的GC没有办法回收,所以,我们在不用的时候,还是需要调用recycle()方法,源码里面,recycle()方法其实就是调用的JNI的函数,然后释放C那一块的内存。
3.1.3 把图片进行压缩
我们在实际开发过程当中,可能因为业务需要,需要加载一张很大的图片,大到直接可以超过系统分配给我们App的内存大小,这样,就会直接导致内存溢出,那么,这个时候,我们就应当控制图片的大小,那么就应该将bitmap进行压缩了。
下面大概讲一下对一张图片进行压缩的一个过程。
第一步:计算实际采样率
/**
* 计算压缩比例值
* @param options 解析图片的配置信息
* @param reqWidth 所需图片压缩尺寸最小宽度
* @param reqHeight 所需图片压缩尺寸最小高度
* @return
*/
public static int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
//保存图片原宽高值
final int height = options. outHeight;
final int width = options. outWidth;
//初始化压缩比例为1
int inSampleSize = 1;
//当图片宽高值任何一个大于所需压缩图片宽高值时,进入循环计算系统
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
//压缩比例值每次循环两倍增加,
//直到原图宽高值的一半除以压缩值后都~大于所需宽高值为止
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
第二步:根据得到的采样率对图片进行解析
/**
* 获取压缩后的图片
* @param res
* @param resId
* @param reqWidth 所需图片压缩尺寸最小宽度
* @param reqHeight 所需图片压缩尺寸最小高度
* @return
*/
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
//首先不加载图片,仅获取图片尺寸
final BitmapFactory.Options options = new BitmapFactory.Options();
//当inJustDecodeBounds设为true时,不会加载图片仅获取图片尺寸信息
options.inJustDecodeBounds = true;
//此时仅会将图片信息会保存至options对象内,decode方法不会返回bitmap对象
BitmapFactory.decodeResource(res, resId, options);
//计算压缩比例,如inSampleSize=4时,图片会压缩成原图的1/4
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
//当inJustDecodeBounds设为false时,BitmapFactory.decode...就会返回图片对象了
options. inJustDecodeBounds = false;
//利用计算的比例值获取压缩后的图片对象
return BitmapFactory.decodeResource(res, resId, options);
}
3.1.4 使用Bitmap的高级属性inBitmap
Bitmap的inBitmap高级属性主要是值复用内存块,不需要在重新给新的bitmap对象申请一块新的内存,避免了一次内存的分配和回收,从而提供了我们程序运行的效率。
不过这个属性还是有一些坑的,对于适配Android3.0以上 。而且,这个功能,google一直在优化当中,在Android4.4以前,只能复用相同大小的bitmap内存,而4.4之后,则只要比之前的内存小,就可以了。以下贴出inBitmap的简单使用方法:
第一步:首先判断当前图片是否能够使用inBitmap
/**
* 判断是否能够使用inBigmap
* @param candidate 比较标准
* @param targetOptions 判断目标对象属性
* @return
*/
public static boolean canUseForInBitmap(
Bitmap candidate, BitmapFactory.Options targetOptions) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// From Android 4.4 (KitKat) onward we can re-use
// if the byte size of the new bitmap is smaller than
// the reusable bitmap candidate
// allocation byte count.
int width = targetOptions.outWidth / targetOptions.inSampleSize;
int height =
targetOptions.outHeight / targetOptions.inSampleSize;
int byteCount = width * height
* getBytesPerPixel(candidate.getConfig());
return byteCount <= candidate.getAllocationByteCount();
}
// On earlier versions,
// the dimensions must match exactly and the inSampleSize must be 1
return candidate.getWidth() == targetOptions.outWidth
&& candidate.getHeight() == targetOptions.outHeight
&& targetOptions.inSampleSize == 1;
}
第二步:从缓存里面拿出bitmap,将此Bitmap赋值给inBitmap。
/**
* 将Bitmap赋值给inBitmap
* @param options 图片的配置信息
* @param cache 图片缓存
*/
private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) {
//inBitmap only works with mutable bitmaps, so force the decoder to
//return mutable bitmaps.
options.inMutable = true;
if (cache != null) {
// Try to find a bitmap to use for inBitmap.
Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
if (inBitmap != null) {
// If a suitable bitmap has been found,
// set it as the value of inBitmap.
options.inBitmap = inBitmap;
}
}
}
第三步:调用刚刚图片压缩时候的decode方法,把options参数传入
3.1.5 捕获异常
很多时候,当内存确实很吃紧的时候,难免还是会出现OOM,所以,根据经验之谈,我们在开发过程中,实例化Bitmap的时候,最好还是添加try catch,进行异常捕获。
需要注意,平常的Exception异常是捕获不到OOM Erro的,因为OOM是一个错误,我们编码的时候需要捕获错误,具体给出以下示例代码:
public static Bitmap createBitmap(int width, int height, Bitmap.Config config) {
Bitmap bitmap = null;
try {
bitmap = Bitmap.createBitmap(width, height, config);
} catch (OutOfMemoryError e) {
while(bitmap == null) {
System.gc();
System.runFinalization();
bitmap = createBitmap(width, height, config);
}
}
}
3.2 除了Bitmap之外的OOM
3.2.1 listview
这个listview确实提到了好多次,毕竟我们实际开发当中,用它来呈现一些数据确实的频率也蛮高,还是需要讲述一下。
使用listview的时候,一定要记得复用convertView
同时,在listview当中,如果需要显示大图的控件,记得使用LRU(最近最少使用,三级缓存)机制进行缓存图片
3.2.2 onDraw方法当中,尽量避免对象的创建
如果在onDraw方法中创建对象,会触发频繁的GC,也就是之前提到的内存抖动,当内存抖动积累到一定的程度,也会出现内存溢出。
3.2.3 使用多进程,一定要小心小心再小心
我们有的时候需要将一些服务,或者主件放到另外一个进程去运行,例如一些定位,推送等,这样确实可以分担主进程的内存压力。
但是,多进程中的一些通信真心没有那么简单。很多机制可能失效,从而影响业务的基本功能。可能会出现一些莫名其妙的crash.
所以,如果我们的App实际业务没有达到一定程度,真心不要使用多进程。
干货总结
此篇文章根据OOM是什么,了解一些容易混淆的概念,然后熟悉一些OOM的解决方案这个逻辑,再结合实际开发可能遇到的问题,讲述了内存溢出的相关知识。其实,大篇幅都是在讲述Bitmap的处理方案,因为,我们这个Bitmap确实在实际开发当中引发OOM的概率还是相当大的。
希望通过以上的讲述,我们能够对于OOM有一个清晰的了解,从而根据我们实际开发当中自己的业务,进行OOM的优化。
其实有的时候,我们在解决OOM的时候需要有一个权衡,因为如果考虑到了OOM的情况而频繁触发GC,可能会导致UI卡顿的现象,跟严重的可能出现ANR的问题,需要我们在实际开发过程中具体场景具体分析。
好了,内存的泄露的知识就先更新到这了,如果觉得本篇文章对大家有益,请给予一个赞和喜欢,这样我才更有动力一直更新下去,如果想和我一起探讨的,可以关注一波。