我们在平常使用Bitmap的过程中经常会遇到OOM异常,为此困扰了我两三天时间,接下来把我自己的解决方法讲述一下:
首先来看看为什么使用Bitmap会导致OOM异常的,假如采用Bitmap来加载一个596KB(1920*1152)大小的图片,那么实际上在加载到内存中的时候占用空间的大小将不只是596KB那么大,具体多大呢?计算方法是:
图片的长度 * 图片的宽度 * 单位像素占用的字节数
对于单位像素点占用的字节数,官方支持的图片解码方式有四种:
ALPHA_8:只存储透明度信息,没有颜色值;
ARGB_4444:在API 13之后用ARGB_8888来代替;
ARGB_8888:把每个像素点的透明度、R、G、B值都采用一个byte来表示,即需要4个字节来存储像素点的颜色信息;
RGB_565:仅仅需要两个字节来存储像素点的颜色信息,他不会存储像素点的透明度,而且采用5比特来存放R颜色值,采用6比特来存放G颜色值,采用5比特来存放B颜色值;
就以上面596KB的图片如果采用ARGB_8888来进行图片解码的话,实际占用内存的大小是:1920*1152*4 B = 8640KB,为了印证这个的结论,先来个小Demo看看具体怎么获得图片变成Bitmap占用内存的大小:
布局文件很简单,就只有ImageView
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/meimei"/> </LinearLayout>接下来是BitmapActivity
public class BitmapActivity extends Activity { public ImageView imageView = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.bitmap_activity); imageView = (ImageView) findViewById(R.id.imageView); Drawable drawable = imageView.getDrawable(); if(drawable instanceof BitmapDrawable) { BitmapDrawable bitmapDrawable = (BitmapDrawable)drawable; Bitmap bitmap = bitmapDrawable.getBitmap(); int rowBytes = bitmap.getRowBytes(); int height = bitmap.getHeight(); long memorySize = rowBytes*height; System.out.println("width: "+bitmap.getWidth()); System.out.println("height: "+height); System.out.println("size: "+(float)memorySize/1024+"KB"); } } }
这里的getRowBytes用于返回Bitmap中每一行像素点占用的字节数,getHeight获得高度,相当于行数,二者相乘正好就是图片解码之后将要占用的内存空间大小;
查看Logcat输出:
看到没有呢?相对于596KB来说,足足是他的14.5倍,这还只是像素不是太大的图片,如果像素更大呢?一张图片就占用这么大内存,如果是很多张图片呢?不OOM才怪呢
那么为什么我们平常的应用程序在加载很多图片的时候也没见过有什么异常啊,他们是怎么实现的呢?这里讲解一种最直观,最简单的实现,那就是压缩图片,因为在一个很小的布局中显示高分辨率的图片对于你界面来说除了占用很大内存外是没有任何意义的,完全可以显示其压缩后的小图呀,那么我们该怎么做呢?别急,下面就是了:
定义图片加载布局bitmap_activity.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <ImageView android:id="@+id/imageView1" android:layout_width="50dp" android:layout_height="50dp"/> </LinearLayout>很简单,只有一个ImageView
定义BitmapActivity
public class BitmapActivity extends Activity { public ImageView imageView = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.bitmap_activity); imageView = (ImageView) findViewById(R.id.imageView1); ImageAsyncTask task = new ImageAsyncTask(imageView,50,50); task.execute("http://image.baidu.com/search/down?tn=download&word=download&ie=utf8&fr=detail&url=http%3A%2F%2Fimg1.pconline.com.cn%2Fpiclib%2F200904%2F28%2Fbatch%2F1%2F32910%2F12408824046039mk21hbi75.jpg&thumburl=http%3A%2F%2Fimg2.imgtn.bdimg.com%2Fit%2Fu%3D3414739956%2C4196877666%26fm%3D21%26gp%3D0.jpg"); } /** * 计算出压缩比 * @param options * @param reqWith * @param reqHeight * @return */ public int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight) { //通过参数options来获取真实图片的宽、高 int width = options.outWidth; int height = options.outHeight; int inSampleSize = 1;//初始值是没有压缩的 if(width > reqWidth || height > reqHeight) { //计算出原始宽与现有宽,原始高与现有高的比率 int widthRatio = Math.round((float)width/(float)reqWidth); int heightRatio = Math.round((float)height/(float)reqHeight); //选出两个比率中的较小值,这样的话能够保证图片显示完全 inSampleSize = widthRatio < heightRatio ? widthRatio:heightRatio; } System.out.println("压缩比: "+inSampleSize); return inSampleSize; } /** * 将InputStream转换为Byte数组 * @param in * @return */ public static byte[] inputStreamToByteArray(InputStream in) { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len; try { while((len = in.read(buffer)) != -1) { outputStream.write(buffer, 0, len); } } catch (IOException e) { e.printStackTrace(); }finally{ try { in.close(); outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } return outputStream.toByteArray(); } class ImageAsyncTask extends AsyncTask<String, Void, Bitmap> { public ImageView iv; public int reqWidth; public int reqHeight; public ImageAsyncTask(ImageView imageView,int reqWidth,int reqHeight) { this.iv = imageView; this.reqWidth = reqWidth; this.reqHeight = reqHeight; } @Override protected Bitmap doInBackground(String... params) { URL url; HttpURLConnection connection = null; InputStream in = null; Bitmap beforeBitmap = null; Bitmap afterBitmap = null; try { url = new URL(params[0]); connection = (HttpURLConnection) url.openConnection(); in = connection.getInputStream(); BitmapFactory.Options options = new BitmapFactory.Options(); //设置BitmapFactory.Options的inJustDecodeBounds属性为true表示禁止为bitmap分配内存 options.inJustDecodeBounds = true; byte[] data = inputStreamToByteArray(in); beforeBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);//这次调用的目的是获取到原始图片的宽、高,但是这次操作是没有写内存操作的 options.inSampleSize = calculateInSampleSize(options,reqWidth, reqHeight); //设置这次加载图片需要加载到内存中 options.inJustDecodeBounds = false; afterBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options); float afterSize = (float)(afterBitmap.getRowBytes()*afterBitmap.getHeight()); System.out.println("压缩之后的图片大小: "+(float)afterSize/1024+"KB"); } catch (Exception e) { System.out.println(e.toString()); } return afterBitmap; } @Override protected void onPostExecute(Bitmap result) { if(result != null) imageView.setImageBitmap(result); } } }
不要忘记添加访问网络的权限:
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
其中第8行会开启开启线程来加载图片同时会将ImageView通过构造函数传递给线程,第9行execute的参数就是我们要加载图片的地址,调用之后会执行ImageAsyncTask的doInBackground方法,在这个方法中会通过HttpURLConnection来获得对应地址图片的输入流,接着第85行获得Bitmap的Options对象,这个对象的inJustDecodeBounds属性用来设置在解码图片的时候是否将其加入到内存中,因为一般Bitmap图片都是按照像素点来存储的,这样的话会很占用内存,将inJustDecodeBounds设置为true的话表示在解码图片的时候不会将其加入到内存中,第88行我们调用inputStreamToByteArray将in输入流转换成了byte数组,原因在于将流进行固化,因为后面我们可能会多次使用流中的数据,但是输入流只能使用一次,用过之后是不能回滚的,第89行通过BitmapFactory的decodeByteArray方法对图片数据byte数组进行了解码,但是不会加入内存,第四个参数options就是我们之前创建的Options对象,这个方法执行结束后就会获得图片的头部信息,他会存储到options中,如果你仔细点的话会发现beforeBitmap的值是等于null的,就是因为他设置inJustDecodeBounds为true没有使用内存的原因啦!
随后我们调用calculateInSampleSize计算出压缩比,计算方法也很简单,就是通过options的outWidth和outHeight获得图片的实际宽度和高度,分别与所要求图片的宽度和高度相除,取两者结果的较小值作为压缩比;
第90行设置了options的压缩比,91行设置inJustDecodeBounds为false表示解码图片的时候需要将其加入到内存中,当然这里占用内存的大小将远远实际图片需要占用的大小,达到了真正意义上对图片进行压缩减少使用内存的操作;
最后在onPostExecute中将doInBackground返回的bitmap设置为ImageView需要显示的图片,这里需要注意的是设置ImageView的图片操作必须是在onPostExecute中,因为onPostExecute是属于主线程的,而doInBackground是属于子线程的,只有主线程可以更新UI操作的,具体分析可以参见:android-----AsyncTask源码分析