【Android】使用glide加载未知尺寸图片导致OOM问题的解决方案

问题:app中有一个Activity用于查看大图,最近出现了一些超大图(内存占用超100M),导致app出现OOM导致的crash

背景:大图的来源中只给出了图片的url,除此之外再无任何信息。图片url提供方对于提供图片其他信息(如宽/高),成本较高,讨论之后有客户端自己进行处理。

 

1⃣️OOM产生的原因

内存占用量超过了vm能分配的最大内存量,或者一下子申请了一块非常大的内存(比如100M),虽然内存还有很大空间,但是无法一下腾出连续的一片内存进行分配,可能会导致内存溢出。

之前试了一下try-catch,发现是catch不住这种Error的,知乎上一篇回答安卓编程:可否用try-catch捕获Out Of Memory Error以避免其发生?说,try-catch只能catch住在try里面申请内存的变量抛出的Error,而且这次catch住了,下次还是会抛,所以用try-catch解决OOM的问题是不太可行的。

关于vm能分配的内存,android dalvik heap 浅析 这篇文章介绍的非常形象

(1) dalvik.vm.heapstartsize        初始分配的堆size

(2) dalvik.vm.heapgrowthlimit    受控app的极限堆size

(3) dalvik.vm.heapsize               不受控app的极限堆size
 

这三个值在不同手机上会有所不同,在build.prop文件里,可以查看 Android系统移植与调试之——->build.prop文件详细赏析

dalvik.vm.heapstartsize=5m	#单个应用程序分配的初始内存
dalvik.vm.heapgrowthlimit=48m	#单个应用程序最大内存限制,超过将被Kill,这或许是某些大体积程序闪退的原因
dalvik.vm.heapsize=256m  #dalvik的虚拟内存大小

我们可以通过在manifest中配置android:largeheap=true,可以让app申请的内存达到heapsize,这样app可用内存可以变多。但是这样相应的后果是每次gc的时间也会变长,导致性能变差,我们尽量要从“减少使用的内存”方向优化,避免占用更大的内存。

 

2⃣️图片占用内存的计算方式

图片占用的字节数 = 图片宽度(像素)* 图片高度(像素)* 单位像素占用字节数

其中图片的宽/高,glide在RequestListener的onSourceReady回调的GlideDrawable中,可以通过GlideDrawable.getIntrinsicWidth()和GlideDrawable.getIntrinsicHeight()获取,单位像素占用字节数根据ARGB方式进行计算,如config为Bitmap.Config.RGB_565表示:每个像素占16位,没有透明度,R-5,G-6,B-5,共5+6+5=16bit = 2bytes。

这样 一张1080宽,1920高的图像占用的内存大小为 1080*1920*2 = 4147200bytes = 3.955M

更具体的计算方式可以移步Android 一张图片BitMap占用内存的计算

 

3⃣️图片压缩方式 

可以采用public static Bitmap decodeFile (String pathName, BitmapFactory.Options opts) 方法来对bitmap的进行压缩,其中path是文件路径、opts是压缩配置(如opts.inSampleSize为采样率)。

可以先将BitmapFactory.Options的inJustDecodeBounds参数设为true,并decodeBitmap,这样并不会真的加载图片,而只是解析图片的宽高。可以options.outWidth和options.outHeight查看图片的宽高,通过目前的宽高,与可加载的宽高进行除法运算,得到采样率,比如宽高分别为4000、3000,可能导致内存溢出,将宽高压缩为2000、1500是安全的,则设置inSampleSize = 2,之后再设置inJustDecodeBounds参数为false,此时再次decodeBitmap可以真正加载图片。更详细的资料,可以参考Android之Bitmap总结(加载、尺寸压缩、优化)

值得注意的是inSampleSize需要设置为2的指数,Developers BitmapFactory.Options 当不为2的指数时,向下取为2的指数,如5会被取为4,而我们需要设置为8才能真正保证安全,因此我们先计算出需要压缩的比例shrinkSize,再根据shrinkSize向上取到2的指数,使采样后的宽高符合设置的目标值。

//inSampleSize要设置为2的指数,不是2的指数的数字会被向下取整,所以要用shrinkSize计算sampleSize
int sampleSize = (int) Math.pow(2, (int) Math.ceil(Math.log(shrinkSize) / Math.log(2)));

 

4⃣️Glide存储路径的获取

decodeBitmap的一个必要参数是path,网上查了很多资料,只有调用downloadOnly(xxx).get()时,Glide会直接返回文件存储的路径,其余的都不会返回缓存路径,但是这个方法确实只执行下载逻辑,Glide那一套缓存机制就全部失效了,每次加载图片都要重新download,一方面浪费流量,一方面响应变慢。所以想的是仍旧保留Glide缓存的那一套,自己拼接一套glide的缓存路径。

首先使用了一下downloadOnly方法打印了一下文件的存储路径,Glide的缓存路径由Glide缓存路径+一串加密字符串组成。其中Glide 缓存路径可以自己设置,也可以使用默认路径,可以通过Glide.getPhotoCacheDir(context)获取,一串加密字符串事实证明是由图片的url进行SHA256算法进行加密之后得到的,代码如下。SHA256算法来源 Java数据加密(MD5,sha1,sha256)

public class GlideUtil {
    @Volatile
    private static GlideUtil mInstance;

    private Context context;

    public GlideUtil(Context context) {
        this.context = context;
    }

    public String getPath(String url) {
        if (url == null || url.equals("")) return "";
        try {
            MessageDigest messageDigest;
            String encodeStr = "";
            try {
                messageDigest = MessageDigest.getInstance("SHA-256");
                messageDigest.update(url.getBytes("UTF-8"));
                encodeStr = byte2Hex(messageDigest.digest());
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            }

            String path = ;
            if (context != null) {
                path = Glide.getPhotoCacheDir(context) + "/" + encodeStr + ".0";
            } else {
                path = "/data/user/0/com.xxx.xxx(你的包名)/cache/" + DiskLruCacheFactory.DEFAULT_DISK_CACHE_DIR + "/" + encodeStr + ".0";
            }
            return path;

        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }


    /**
     * 将byte转为16进制
     *
     * @param bytes
     * @return
     */
    private static String byte2Hex(byte[] bytes) {
        StringBuffer stringBuffer = new StringBuffer();
        String temp;
        for (int i = 0; i < bytes.length; i++) {
            temp = Integer.toHexString(bytes[i] & 0xFF);
            if (temp.length() == 1) {
                //1得到一位的进行补0操作
                stringBuffer.append("0");
            }
            stringBuffer.append(temp);
        }
        return stringBuffer.toString();
    }

    public static GlideUtil getInstance(Context context) {
        if (mInstance == null) {
            synchronized (GlideUtil.class) {
                if (mInstance == null) {
                    mInstance = new GlideUtil(context);
                    return mInstance;
                }
            }
        }
        return mInstance;
    }
}

 

5⃣️可用内存获取

当我们已经知道图片的原始宽高,如何确定压缩比例?其实可以采用不同的策略,我采用的方法是优先和freeMemory内存进行对比,足够的话不做处理,否则根据freeMemory内存进行压缩。这几个值的区别与联系可以参考Android 获取App可用内存

《【Android】使用glide加载未知尺寸图片导致OOM问题的解决方案》

 

这里面有一个小小小注意点,我本来使用freeMemory的大小去算宽高的压缩比例,但是freeMemory并不是极限,比如freeMemory是2M,你需要3M,申请3M可能也是安全的,因为系统可以再为你分配一些内存,所以没必要完全把图片占用的内存压缩到freeMemory里面,而且调试发现有时候freeMemory的值(long型) == 0L。调整之后,当freeMemory<2M时,取freeMemory为10M,否则可能导致图片压缩的过于模糊。(其中2和10完全是经验值,没有什么具体依据)

    private Drawable getSafeDrawable(GlideDrawable drawable, String url, Context context) {
        Long freeMemory = Runtime.getRuntime().freeMemory();//获取app可用内存

        if (drawable == null) {
            return null;
        }

        //当图片是动图的时候,放宽条件 给10M
        if (drawable instanceof GifDrawable) {
            if (freeMemory < (long) (10 * 1024 * 1024)) {
                freeMemory = (long) (10 * 1024 * 1024);
            }
        }

        if (drawable.getIntrinsicWidth() * drawable.getIntrinsicHeight() * 2 > freeMemory) {
            //当可用内存小于2M,让它去申请10M;可用内存足够的时候,就在可用内存里加载
            if (freeMemory < (long) (2 * 1024 * 1024)) {
                freeMemory = (long) (10 * 1024 * 1024);
            }

            int width = drawable.getIntrinsicWidth();
            int height = drawable.getIntrinsicHeight();

            //width * height * 2 * (1/sample2) <freeMemory,RGB_565 16位 占2字节,所以*2
            int shrinkSize = (int) Math.ceil(Math.sqrt((double) width * height * 2 / freeMemory));

            //inSampleSize要设置为2的指数,不是2的指数的数字会被向下取整,所以要用shrinkSize计算sampleSize
            int sampleSize = (int) Math.pow(2, (int) Math.ceil(Math.log(shrinkSize) / Math.log(2)));

            String filePath = GlideUtil.getInstance(context).getPath(url);
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inPreferredConfig = Bitmap.Config.RGB_565;
            options.inSampleSize = sampleSize;
            Bitmap bitmap = BitmapFactory.decodeFile(filePath, options);

            //当前路径上没有解析到图片
            if (bitmap == null) return null;
            return new BitmapDrawable(bitmap);
        } else {
            return drawable;
        }

    }

最后附上一篇 带你玩转Glide的回调与监听

到这里,我的问题就差不多解决了,收获真的很大,比如以上的5点,原来我每一点都是不知道的~

希望对看到这里的你有帮助,没有帮助的话就当我是自己记录啦~

    原文作者:Crab0314
    原文地址: https://blog.csdn.net/Crab0314/article/details/85102422
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞