Android性能调优篇之内存溢出

开篇废话

上一篇我们了解了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的问题,需要我们在实际开发过程中具体场景具体分析。

好了,内存的泄露的知识就先更新到这了,如果觉得本篇文章对大家有益,请给予一个赞和喜欢,这样我才更有动力一直更新下去,如果想和我一起探讨的,可以关注一波。

你可能感兴趣的:(Android性能调优篇之内存溢出)