加载大图和多图开发是Android应用时会经常遇到的,如果处理不当就会出现OOM(OutOfMemory)异常,这是由于加载图片时所需的内存超过Android设备的内存所造成的。如何解决呢?
加载大图片之前我们可以查看APP能使用的最大内存:
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
Log.d("TAG", "Max memory is " + maxMemory + "KB");
在加载高分辨率图片的时候我们最好先进行压缩处理,压缩后的图片大小应该与展示它的控件大小差不多.
首先我们需要知道BitmapFactory这个类,BitmapFactory这个类提供了多个解析方法(decodeByteArray, decodeFile, decodeResource等)用于创建Bitmap对象,我们应该根据图片的来源选择合适的方法。比如SD卡中的图片可以使用decodeFile方法,网络上的图片可以使用decodeStream方法,资源文件中的图片可以使用decodeResource方法。这些方法会尝试为已经构建的bitmap分配内存,这时就会很容易导致OOM出现。为此每一种解析方法都提供了一个可选的BitmapFactory.Options参数,将这个参数的inJustDecodeBounds属性设置为true就可以让解析方法禁止为bitmap分配内存,返回值也不再是一个Bitmap对象,而是null。虽然Bitmap是null了,但是BitmapFactory.Options的outWidth、outHeight和outMimeType属性都会被赋值。这个技巧让我们可以在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩。如下代码所示:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;//true表示先不加载图片到内存,只是获取它的宽高,图片类型等信息 这个属性默认是false
BitmapFactory.decodeResource(getResources(), R.id.test, options);
int imageHeight = options.outHeight; //获取量得的高
int imageWidth = options.outWidth; //获取量得的宽
String imageType = options.outMimeType; //获取图片类型
获取图片由三种方式:
1.从资源文件中获取:
public Bitmap FromRecourse(){ Bitmap rawBitmap = null; try{ rawBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.test); }catch (OutOfMemoryError e){ Toast.makeText(BitmapActivity.this,"内存溢出",Toast.LENGTH_LONG).show(); int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024/1024); Toast.makeText(this,"最大内存"+maxMemory+"M",Toast.LENGTH_LONG).show(); } return rawBitmap; }
2.从SD卡获取
public Bitmap FromSDcard1(){ String SDcardPath = Environment.getExternalStorageDirectory().toString(); String bitmapFile = SDcardPath+"/"+"test.jpg"; Bitmap rawBitmap2 = null; try{ rawBitmap2 = BitmapFactory.decodeFile(bitmapFile, null); }catch (OutOfMemoryError e){ Toast.makeText(BitmapActivity.this,"内存溢出",Toast.LENGTH_LONG).show(); } return rawBitmap2; }
3.从SD卡获取
public Bitmap FromSDcard2(){ InputStream stream = getBitmapInputStreamFromSDCard("test.jpg"); Bitmap rawBitmap3 = BitmapFactory.decodeStream(stream); return rawBitmap3; }
//从SD卡获取Bitmap图片 public InputStream getBitmapInputStreamFromSDCard(String fileName){ if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){ String SDcardPath = Environment.getExternalStorageDirectory().toString(); String filePath = SDcardPath+"/"+fileName; File file = new File(filePath); try { FileInputStream fileInputStream = new FileInputStream(file); return fileInputStream; } catch (FileNotFoundException e) { e.printStackTrace(); } } return null; }
我们可以先将原始图片压缩到指定宽高的比例后再加载到内存
/** * 计算需要压缩尺寸的图片的压缩比 * @param options * @param reqWidth 需要将图片的宽压缩到指定的值 * @param reqHeight 需要将图片的高压缩到指定的值 * @return 压缩比 */ public static int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight){ //源图片的宽 高 int width = options.outWidth; int height = options.outHeight; int inSamleSize = 1; if(width > reqWidth || height > reqHeight){ int scaleHeirht = Math.round((float)height/reqHeight); int scaleWidth = Math.round((float)width/reqWidth); inSamleSize = scaleHeirht<scaleWidth?scaleHeirht:scaleWidth; } return inSamleSize; }
大概就是这么个使用过程吧
private ImageView imageView ; private Bitmap copyBitmap; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_bitmap); imageView = (ImageView) findViewById(R.id.image_test); copyBitmap = FromRecourse(); if(copyBitmap!=null){ //不为空说明直接加载(没有经过压缩)没用发送内存溢出 compressBitmap(copyBitmap); }else{ //先获取内存溢出图片的尺寸 BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;//只加载高宽图片类型等信息 BitmapFactory.decodeResource(getResources(), R.mipmap.test, options); int inSampleSize = calculateInSampleSize(options, 1080/2, 1920/2);//将图片压缩到宽高为1080/2 * 1920/2时的缩放因子 options.inSampleSize = inSampleSize;//赋给options options.inJustDecodeBounds = false;//必须置为false,下载加载才会加载图片到内存 copyBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.test, options); } if(copyBitmap!=null){ Drawable drawable = new BitmapDrawable(copyBitmap); imageView.setImageDrawable(drawable); } }
下面是一个使用 LruCache 来缓存图片的例子:
private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
// 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。
// LruCache通过构造函数传入缓存值,以KB为单位。
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 使用最大可用内存值的1/8作为缓存的大小。
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// 重写此方法来衡量每张图片的大小,默认返回图片数量。
return bitmap.getByteCount() / 1024;
}
};
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
在这个例子当中,使用了系统分配给应用程序的八分之一内存来作为缓存大小。在中高配置的手机当中,这大概会有4兆(32/8)的缓存空间。一个全屏幕的 GridView 使用4张 800x480分辨率的图片来填充,则大概会占用1.5兆的空间(800*480*4)。因此,这个缓存大小可以存储2.5页的图片。
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
} else {
imageView.setImageResource(R.drawable.image_placeholder); //加载未完成之前先用默认图片
BitmapWorkerTask task = new BitmapWorkerTask(imageView); //AsyncTask
task.execute(resId);
}
}
通过这两种方式,可以有效避免OOM,以及提高ListView等流畅度