Android性能调优篇之内存溢出

作者: 进击的欧阳 | 来源:发表于2017-08-16 23:23 被阅读213次

    开篇废话

    上一篇我们了解了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性能调优篇之内存溢出

        本文链接:https://www.haomeiwen.com/subject/hqgwrxtx.html