我们在使用GridView或者ListView时,通常会遇到两个棘手的问题:
1.每个Item获取的数据所用的时间太长会导致程序长时间黑屏,更甚会导致程序ANR,也就是Application No Responding
2.当每个Item中有图片存在时,少量图片不会出现问题,当有大量图片存在时,就会出现Out Of Memory的错误,导致这个错误的原因是Android系统中读取位图Bitmap时.默认分给虚拟机中图片的堆栈大小只有8M。
一、解决第一个问题,这里我们采用异步加载图片的方法,也就是先让每个Item加载一张默认的drawable,在后台处理获取图片的任务,等后台处理完以后,提示前台更新图片。这 里我们会遇到一个问题,就是在gridview或则listview等容器中,当用户随意滑动的时候,将会产生N个线程去加载图片,这个是我们不想看到 的。我们希望的是一个图片只有一个线程去下载就行了。为了解决这个问题,我们应该做的是让这个Item中imageview记住它是否正在加载(或者说是 下载)图片资源。如果正在加载,或者加载完成,那么我就不应该再建立一个任务去加载图片了。
二、第二个问题我们采用图片缓存的方式,将已经加载完成的图片保存到缓存中,然后通过监控gridview的滑动事件去释放图片,即调用bitmap.recycle()方法,从而保证不会出现Out Of Memory错误 。
MainActivity类:
public class MainActivity extends Activity implements OnScrollListener{ private static final String TAG = "MainActivity"; private TextView textview_show_prompt = null; private GridView gridview_test = null; private List<String> mList = null; //图片缓存用来保存GridView中每个Item的图片,以便释放 public static Map<String,Bitmap> gridviewBitmapCaches = new HashMap<String,Bitmap>(); private MyGridViewAdapter adapter = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViews(); initData(); setAdapter(); } private void findViews(){ textview_show_prompt = (TextView)findViewById(R.id.textview_show_prompt); gridview_test = (GridView)findViewById(R.id.gridview_test); } private void initData(){ mList = new ArrayList<String>(); String url = "/mnt/sdcard/testGridView/jay.png";//为sd卡下面创建testGridView文件夹,将图片放入其中 //为了方便测试,我们这里只存入一张图片,将其路径后面添加数字进行区分,到后面要获取图片时候再处理该路径。 for(int i=0;i<1000;i++){ mList.add(url+"/"+i);//区分路径 } } private void setAdapter(){ adapter = new MyGridViewAdapter(this, mList); gridview_test.setAdapter(adapter); gridview_test.setOnScrollListener(this); } //释放图片的函数 private void recycleBitmapCaches(int fromPosition,int toPosition){ Bitmap delBitmap = null; for(int del=fromPosition;del<toPosition;del++){ delBitmap = gridviewBitmapCaches.get(mList.get(del)); if(delBitmap != null){ //如果非空则表示有缓存的bitmap,需要清理 Log.d(TAG, "release position:"+ del); //从缓存中移除该del->bitmap的映射 gridviewBitmapCaches.remove(mList.get(del)); delBitmap.recycle(); delBitmap = null; } } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { // TODO Auto-generated method stub //注释:firstVisibleItem为第一个可见的Item的position,从0开始,随着拖动会改变 //visibleItemCount为当前页面总共可见的Item的项数 //totalItemCount为当前总共已经出现的Item的项数 recycleBitmapCaches(0,firstVisibleItem); recycleBitmapCaches(firstVisibleItem+visibleItemCount, totalItemCount); } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { // TODO Auto-generated method stub } }
MyGridViewAdapter类:
public class MyGridViewAdapter extends BaseAdapter{ private Context mContext = null; private LayoutInflater mLayoutInflater = null; private List<String> mList = null; private int width = 120;//每个Item的宽度,可以根据实际情况修改 private int height = 150;//每个Item的高度,可以根据实际情况修改 public static class MyGridViewHolder{ public ImageView imageview_thumbnail; public TextView textview_test; } public MyGridViewAdapter(Context context,List<String> list) { // TODO Auto-generated constructor stub this.mContext = context; this.mList = list; mLayoutInflater = LayoutInflater.from(context); } @Override public int getCount() { // TODO Auto-generated method stub return mList.size(); } @Override public Object getItem(int arg0) { // TODO Auto-generated method stub return null; } @Override public long getItemId(int position) { // TODO Auto-generated method stub return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { // TODO Auto-generated method stub MyGridViewHolder viewHolder = null; if(convertView == null){ viewHolder = new MyGridViewHolder(); convertView = mLayoutInflater.inflate(R.layout.layout_my_gridview_item, null); viewHolder.imageview_thumbnail = (ImageView)convertView.findViewById(R.id.imageview_thumbnail); viewHolder.textview_test = (TextView)convertView.findViewById(R.id.textview_test); convertView.setTag(viewHolder); }else{ viewHolder = (MyGridViewHolder)convertView.getTag(); } String url = mList.get(position); //首先我们先通过cancelPotentialLoad方法去判断imageview是否有线程正在为它加载图片资源, //如果有现在正在加载,那么判断加载的这个图片资源(url)是否和现在的图片资源一样,不一样则取消之前的线程(之前的下载线程作废)。 //见下面cancelPotentialLoad方法代码 if (cancelPotentialLoad(url, viewHolder.imageview_thumbnail)) { AsyncLoadImageTask task = new AsyncLoadImageTask(viewHolder.imageview_thumbnail); LoadedDrawable loadedDrawable = new LoadedDrawable(task); viewHolder.imageview_thumbnail.setImageDrawable(loadedDrawable); task.execute(position); } viewHolder.textview_test.setText((position+1)+""); return convertView; } private Bitmap getBitmapFromUrl(String url){ Bitmap bitmap = null; bitmap = MainActivity.gridviewBitmapCaches.get(url); if(bitmap != null){ System.out.println(url); return bitmap; } url = url.substring(0, url.lastIndexOf("/"));//处理之前的路径区分,否则路径不对,获取不到图片 //bitmap = BitmapFactory.decodeFile(url); //这里我们不用BitmapFactory.decodeFile(url)这个方法 //用decodeFileDescriptor()方法来生成bitmap会节省内存 //查看源码对比一下我们会发现decodeFile()方法最终是以流的方式生成bitmap //而decodeFileDescriptor()方法是通过Native方法decodeFileDescriptor生成bitmap的 try { FileInputStream is = new FileInputStream(url); bitmap = BitmapFactory.decodeFileDescriptor(is.getFD()); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } bitmap = BitmapUtilities.getBitmapThumbnail(bitmap,width,height); return bitmap; } //加载图片的异步任务 private class AsyncLoadImageTask extends AsyncTask<Integer, Void, Bitmap>{ private String url = null; private final WeakReference<ImageView> imageViewReference; public AsyncLoadImageTask(ImageView imageview) { super(); // TODO Auto-generated constructor stub imageViewReference = new WeakReference<ImageView>(imageview); } @Override protected Bitmap doInBackground(Integer... params) { // TODO Auto-generated method stub Bitmap bitmap = null; this.url = mList.get(params[0]); bitmap = getBitmapFromUrl(url); MainActivity.gridviewBitmapCaches.put(mList.get(params[0]), bitmap); return bitmap; } @Override protected void onPostExecute(Bitmap resultBitmap) { // TODO Auto-generated method stub if(isCancelled()){ resultBitmap = null; } if(imageViewReference != null){ ImageView imageview = imageViewReference.get(); AsyncLoadImageTask loadImageTask = getAsyncLoadImageTask(imageview); if (this == loadImageTask) { imageview.setImageBitmap(resultBitmap); imageview.setScaleType(ImageView.ScaleType.CENTER_INSIDE); } } super.onPostExecute(resultBitmap); } } private boolean cancelPotentialLoad(String url,ImageView imageview){ AsyncLoadImageTask loadImageTask = getAsyncLoadImageTask(imageview); if (loadImageTask != null) { String bitmapUrl = loadImageTask.url; if ((bitmapUrl == null) || (!bitmapUrl.equals(url))) { loadImageTask.cancel(true); } else { // 相同的url已经在加载中. return false; } } return true; } //当 loadImageTask.cancel(true)被执行的时候,则AsyncLoadImageTask 就会被取消, //当AsyncLoadImageTask 任务执行到onPostExecute的时候,如果这个任务加载到了图片, //它也会把这个bitmap设为null了。 //getAsyncLoadImageTask代码如下: private AsyncLoadImageTask getAsyncLoadImageTask(ImageView imageview){ if (imageview != null) { Drawable drawable = imageview.getDrawable(); if (drawable instanceof LoadedDrawable) { LoadedDrawable loadedDrawable = (LoadedDrawable)drawable; return loadedDrawable.getLoadImageTask(); } } return null; } //该类功能为:记录imageview加载任务并且为imageview设置默认的drawable public static class LoadedDrawable extends ColorDrawable{ private final WeakReference<AsyncLoadImageTask> loadImageTaskReference; public LoadedDrawable(AsyncLoadImageTask loadImageTask) { super(Color.TRANSPARENT); loadImageTaskReference = new WeakReference<AsyncLoadImageTask>(loadImageTask); } public AsyncLoadImageTask getLoadImageTask() { return loadImageTaskReference.get(); } } }
BitmapUtilities类处理图片,这里我们在imageview显示图片之前就将图片缩小,更好的节省内存:
public class BitmapUtilities { public BitmapUtilities() { // TODO Auto-generated constructor stub } public static Bitmap getBitmapThumbnail(String path,int width,int height){ Bitmap bitmap = null; //这里可以按比例缩小图片: /*BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inSampleSize = 4;//宽和高都是原来的1/4 bitmap = BitmapFactory.decodeFile(path, opts); */ /*进一步的, 如何设置恰当的inSampleSize是解决该问题的关键之一。BitmapFactory.Options提供了另一个成员inJustDecodeBounds。 设置inJustDecodeBounds为true后,decodeFile并不分配空间,但可计算出原始图片的长度和宽度,即opts.width和opts.height。 有了这两个参数,再通过一定的算法,即可得到一个恰当的inSampleSize。*/ BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inJustDecodeBounds = true; BitmapFactory.decodeFile(path, opts); opts.inSampleSize = Math.max((int)(opts.outHeight / (float) height), (int)(opts.outWidth / (float) width)); opts.inJustDecodeBounds = false; bitmap = BitmapFactory.decodeFile(path, opts); return bitmap; } public static Bitmap getBitmapThumbnail(Bitmap bmp,int width,int height){ Bitmap bitmap = null; if(bmp != null ){ int bmpWidth = bmp.getWidth(); int bmpHeight = bmp.getHeight(); if(width != 0 && height !=0){ Matrix matrix = new Matrix(); float scaleWidth = ((float) width / bmpWidth); float scaleHeight = ((float) height / bmpHeight); matrix.postScale(scaleWidth, scaleHeight); bitmap = Bitmap.createBitmap(bmp, 0, 0, bmpWidth, bmpHeight, matrix, true); }else{ bitmap = bmp; } } return bitmap; } }
activity_main.xml:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <TextView android:id="@+id/textview_show_prompt" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/textview_show_prompt" android:textSize="24dp" tools:context=".MainActivity" /> <GridView android:layout_below="@id/textview_show_prompt" android:id="@+id/gridview_test" android:layout_width="fill_parent" android:layout_height="fill_parent" android:horizontalSpacing="10dp" android:verticalSpacing="20dp" android:numColumns="4" android:gravity="center" android:padding="10dp" android:background="#FFFFFFFF"> </GridView> </RelativeLayout>
layout_my_gridview_item.xml:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#88000000" android:gravity="center" > <ImageView android:id="@+id/imageview_thumbnail" android:layout_width="120dp" android:layout_height="150dp" android:padding="5dp" android:scaleType="centerInside" /> <TextView android:id="@+id/textview_test" android:layout_width="120dp" android:layout_height="wrap_content" android:gravity="center" android:textSize="24dp" android:textColor="#FFFF0000" /> </RelativeLayout>