Android解决GridView异步加载图片和加载大量图片时出现Out Of Memory问题--recycleBitmapCaches
来源:互联网 发布:辅助软件定制 编辑:程序博客网 时间:2024/05/21 13:54
众所周知,我们在使用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错误 。
好了,话不多说,我们一起到代码中去寻找答案。
这个是MyGridViewAdapter类
这里我顺便把用到的两个XML文件代码贴出来下:
这个是activity_main.xml文件:
好了,到此为止我们已经完成了任务,在这里我想说的一些心得体会:
本来三天前就打算写这篇文章了,准备先写好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
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
0 0
- Android解决GridView异步加载图片和加载大量图片时出现Out Of Memory问题--recycleBitmapCaches
- Android完美解决GridView异步加载图片和加载大量图片时出现Out Of Memory问题
- Android完美解决GridView异步加载图片和加载大量图片时出现Out Of Memory问题
- Android解决GridView异步加载大量图片时出现Out Of Memory问题00M
- Android完美解决GridView异步加载图片和加载大量图片时出现OOM
- 关于 android 加载 res 图片 out of memory 问题 解决 同样适用于 sd卡图片
- android加载图片Out Of Memory的解决
- Android GridView加载大量图片时出现OOM情况
- Android内存溢出整理总结 OOM(Out Of Memory) 加载的图片太多或图片过大时经常出现OOM问题
- Android内存溢出整理总结 OOM(Out Of Memory) 加载的图片太多或图片过大时经常出现OOM问题
- GridView加载大量图片
- Android ListView和GridView异步加载图片
- android 加载图片轻松避免OOM(out of memory)
- android 加载图片轻松避免OOM(out of memory)
- android 加载图片轻松避免OOM(out of memory)
- Android加载图片导致内存溢出(Out of Memory异常
- Android GridView 异步加载图片
- Android GridView 异步加载图片
- C++ 编译时期的名字查找
- TextView添加跑马灯效果
- javaweb 中 el表达式问题总结
- lua5.2中对table.insert的区别
- web程序部署到Tomcat服务器报错Deployment is out ofdate due to changes in the underlying project contents...
- Android解决GridView异步加载图片和加载大量图片时出现Out Of Memory问题--recycleBitmapCaches
- UIControl的事件类型
- Linux网络编程:原始套接字的魔力【下】
- 数组越界导致程序崩溃
- 《喜事惠》哈尔滨婚宴宴会预订领导品牌
- C++ 名字冲突和继承
- centos 5.8上nagios服务端安装
- RT-Thread 学习笔记(五)---编辑、下载、调试程序
- C++ 类作用域和函数成员