ListView,Gallery,GridView等控件,在载入大量图片时,很容易会产生OutOfMemoryError异常,即内存溢出.因为每个应用可用内存是有限的,但是图片却很占内存,JPG,PNG本身就是压缩格式,如果分辨率高,很可能保存到磁盘中只有几百K,但是读到内存中会占用十几M内存.图片一多,自然就OutOfMemoryError了.
解决这个问题,需要考虑两个方面.
一是按需载入图片,即读图片的缩略图.比如,每个ImageView在屏幕上的面积是120dp*120dp,但是这个图片是高清的,分辨率2560*1600,如果直接载入整个图片,占用的内存是2560*1600*4/1024/1024=15.625M,如果一屏显示5个Item,占用的内存就是78.125M!而Galaxy Nexus给每个应用分配的内存只有64M,低配机型更少.所以,这里必须根据需要的分辨率载入图片.BitmapFactory解析图片时可以传入一个BitmapFactory.Options参数.
BitmapFactory.Options options = new BitmapFactory.Options();options.inJustDecodeBounds = false;options.inSampleSize = inSampleSize;Bitmap bitmap = BitmapFactory.decodeFile(filePath, options);
这里最重要的是inSampleSize,这是一个int值,等于1时,返回原图,大于1时,返回宽高为原图1/inSampleSize的图片,
所以,我们需要先根据需要的大小,计算inSampleSize.
BitmapFactory.Options options = new BitmapFactory.Options(); //设为true,不会去解析图片,只解析边界,即宽高 options.inJustDecodeBounds = true; BitmapFactory.decodeFile(jpgPath, options); //读出图片真实的宽高 int[] size = new int[2]; size[0] = options.outWidth; size[1] = options.outHeight; float realWidth = size[0]; float realHeight = size[1]; // 如果图片尺寸比最大值小,直接返回 if (maxWidth > realWidth && maxHeight > realHeight) { return 1; } // 计算宽高比 float target_ratio = (float) maxWidth / maxHeight; float real_ratio = realWidth / realHeight; int inSampleSize = 1; if (real_ratio > target_ratio) { // 如果width太大,height太小,以width为基准,把realWidth设为maxWidth,realHeight缩放 inSampleSize = (int) realWidth / maxWidth; } else { inSampleSize = (int) realHeight / maxHeight; }
虽然经过上面的处理,图片占用的内存已经大幅减少,放个百来个Item也不会OutOfMemoryError了,但很多应用都是下拉刷新,增加Item,Item的数量不可控,如果所有的图片都放内存里,溢出也是早晚的事.这里就需要做个图片缓存,内存里只保留需要用到的,没用到的,释放.我这里做了二级缓存,第一级放内存,第二级放SD卡.
说到图片内存缓存,网上大部分文章推荐软引用(SoftReference) 和弱引用(WeakReference),但是从Android 2.3开始,所有软引用和弱引用的对象在GC时都会被回收,根本起不到缓存的作用,Google推荐使用LruCache(http://developer.android.com/reference/android/util/LruCache.html),该类可以分配指定大小的缓存空间,当达到临界值时,最先保存的对象会被挤出,释放内存.
下面是一个使用LruCache的适配器:
package com.pocketdigi.googleimage; import java.util.List; import android.app.ActivityManager;import android.content.Context;import android.graphics.Bitmap;import android.support.v4.util.LruCache;import android.view.LayoutInflater;import android.view.View;import android.view.View.OnClickListener;import android.view.ViewGroup;import android.widget.BaseAdapter;import android.widget.ImageView;import android.widget.TextView; import com.pocketdigi.googleimage.mode.ApiResult.ImageData;import com.pocketdigi.utils.ImageUtils;import com.pocketdigi.utils.ImageUtils.DownBitmapListener;import com.pocketdigi.views.ImageViewer; public class GoogleImageAdapter extends BaseAdapter{ Context mContext; List<ImageData> mImageList; LayoutInflater mInflater; boolean mBusy = false; // 图片缓存,Key是url,Value就是Bitmap LruCache<String, Bitmap> mImageCache; public GoogleImageAdapter(Context context, List<ImageData> imageList) { mContext = context; mImageList = imageList; mInflater = LayoutInflater.from(mContext); //读取应用可用内存大小,这里读出来,单位是兆,Galaxy Nexus是64M final int memClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass(); //取1/8作为图片缓存 final int maxSize = 1024 * 1024 * memClass / 8; mImageCache = new LruCache<String, Bitmap>(maxSize) { // 必须覆写sizeOf方法,因为默认返回的是1,即统计的是对象的数量,而不是占用的内存 @Override protected int sizeOf(String key, Bitmap value) { // TODO 自动生成的方法存根 return value.getByteCount(); } }; } @Override public int getCount() { // TODO 自动生成的方法存根 return mImageList.size(); } @Override public Object getItem(int position) { // TODO 自动生成的方法存根 return mImageList.get(position); } @Override public long getItemId(int position) { // TODO 自动生成的方法存根 return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { // TODO 自动生成的方法存根 final ImageData imageData = mImageList.get(position); ViewHolder holder = null; if (convertView == null) { convertView = mInflater.inflate(R.layout.item_listview, null); holder = new ViewHolder(); holder.tv_title = (TextView) convertView.findViewById(R.id.tv_title); holder.iv_thumbnail = (ImageView) convertView.findViewById(R.id.iv_thumbnail); holder.thumbnail_url = imageData.getThumbnail_url(); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); if (!holder.thumbnail_url.equals(imageData.getThumbnail_url())) { holder.iv_thumbnail.setImageResource(R.drawable.loading); } } holder.tv_title.setText(imageData.getAbs()); if (!isBusy()) { final String imgUrl = imageData.getThumbnail_url(); Bitmap bmp = mImageCache.get(imgUrl); if (bmp != null) { holder.iv_thumbnail.setImageBitmap(bmp); } else { loadBmpFromNetWork(imgUrl); } } holder.iv_thumbnail.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { //点击ImageView显示大图 ImageViewer imageViewer = new ImageViewer(mContext); imageViewer.setSourceView((ImageView) v); imageViewer.setJpgUrl(imageData.getImage_url()); imageViewer.show(); } }); return convertView; } /** * 从网络下载图片 * @param imgUrl */ private void loadBmpFromNetWork(final String imgUrl) { //异步从网络下载图片,这个异步下载方法,是有作磁盘缓存的 //缓存原理,根据传入的url,计算md5,作缓存文件名,下载前先判断该文件存不存在,如果存在,从SD卡读,不存在,再去下载 //参数120,120,是返回120*120的bitmap,避免在载入大图时浪费内存 ImageUtils.asynchronousDownloadBitmap(imgUrl, 120, 120, new DownBitmapListener() { @Override public void onComplete(Bitmap bmp) { // TODO 自动生成的方法存根 if(imgUrl!=null&&bmp!=null) { mImageCache.put(imgUrl, bmp); notifyDataSetChanged(); } } @Override public void onProgressChanged(long total, long downloaded) { // TODO 自动生成的方法存根 } }); } @Override public int getItemViewType(int position) { // TODO 自动生成的方法存根 return super.getItemViewType(position); } @Override public int getViewTypeCount() { // TODO 自动生成的方法存根 return super.getViewTypeCount(); } //用来保存各个控件的引用 static class ViewHolder { TextView tv_title; ImageView iv_thumbnail; String thumbnail_url; } public boolean isBusy() { return mBusy; } public void setBusy(boolean busy) { this.mBusy = busy; } }