问题: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可用内存
这里面有一个小小小注意点,我本来使用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点,原来我每一点都是不知道的~
希望对看到这里的你有帮助,没有帮助的话就当我是自己记录啦~