Android完美解决GridView异步加载图片和加载大量图片时出现Out Of Memory问题

来源:互联网 发布:知乎ios7版本 编辑:程序博客网 时间:2024/05/17 08:52

众所周知,我们在使用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;
         }

 }

这里我顺便把用到的两个XML文件代码贴出来下:
 这个是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>


好了,到此为止我们已经完成了任务,在这里我想说的一些心得体会:
     本来三天前就打算写这篇文章了,准备先写好demo,然后再来发表,结果在写demo的过程中发现了很多问题,比如AsyncTask这个类的用法,Bitmap的recycle方法的调用,以及面对嵌入式开发,怎么样去节省内存空间的使用,从而确保不会出现Out Of Memory。大家可以去查看资料看看AsyncTask这个类的用法,至于怎么样去节省内存使用,仁者见仁,智者见智吧。我在这里就单独说下Bitmap的recycle方法的调用的注意事项吧。


   首先,Bitmap是否有调用recycle方法的必要性,这个怎么说呢,每个人都有不同的答案。嵌入式系统总是格外注重空间的问题,不小心的话就会有OOM。但是应用层使用java的android平台有其天然的优势(java语言有自己的垃圾回收,android平台上各个application有自己的process自己的空间)。
   无需调用bitmap的理由有:
     1. 垃圾回收会处理的;
     2.当application关闭,process被杀掉,所有这个process占用的空间自然回归系统;

 

   bitmap的recycle函数的调用还是有必要的,理由有:
     1.垃圾回收虽然好使,但是有可能的话,我们还是让它少干点活吧。垃圾回收有很大的未来不确定性,会加重未来未知时间点的loading,若有大量bitmap需要垃圾回收处理,那必然垃圾回收需要做的次数就更多也发生地更频繁,小心会造成ANR。但是,若是自己recycle,就可以可控制地分散处理了这些回收任务了。
     2. 若是launcher那样一直运行的application,它的process一直存在,memory问题还是多多注意下比较好

 再来说下假如要调用bitmap的recycle方法,何时调用是个很重要的问题,早了就大事不好了,会有这样的Exception:    java.lang.RuntimeException,Canvas: trying to use a recycled bitmap

     所以,我们 怎样才可以保证不会早了呢?
     关于图片显示,重要的时间点:
     step1: 设置进去的时间点;
     Step2: 画面画出来的时间点;
     最保险最笨的做法,在新的图片设置进去以后再recycle掉老的图片,这样做的坏处在于,在某个时间段,你需要的空间是double的(新旧两套都在);
     如果你不偏向于那么做,可以考虑后面一个时间点,除了setImageBitmap以及其它代码中显示调用那个bitmap的时候我们会检查bitmap,在acticvity变为visible的时候系统还是会去找之前设置进去的bitmap。所以,在UI线程里面,在一个不可能被打断的方法里面,是先设置新的bitmap还是先recycle旧的图片是没有影响的。
     譬如说     mBitmap.recycle();
                    mBitmap = .....   //设置
                   mImageView.setImageBitmap(mBitmap);
     这样的代码是完全可以的。

     后面这样的做法,最重要的就是确保:在UI线程(因为设置UI显示只能在UI主线程里)里面一个不可能被打断的方法里面。这个是为了确保在两者之间UI主线程不可能被打断,不可能刚好从invisible变成visible。
     所以,特别小心两种东西:
     1. 多线程(最好不要在多线程里面调用);
     2. 非即时发生的方法:譬如,发intent,发notify去通知UI主线程去做UI重新刷新并不能替代mImageView.setImageBitmap(mBitmap)。完全有可能出现这种情况:你确实发了intent出去了,但是目标activity之一还没有做UI重新设置,这个时候这个acitivity变成visible了,系统仍然试图找旧的图片,找不到了就会报exception了。

 就说这么多吧,这里也参照一些网络上的代码,另外大家下载demo代码运行的时候需要把压缩包里面的jay.png图片放到sd中testGridView文件夹 ,有些机器sd卡路径可能会不同,大家只要到代码中修改String url = "/mnt/sdcard/testGridView/jay.png"这行代码就行。

 

 

原文:http://www.apkbus.com/android-85388-1-1.html

原创粉丝点击