BitMap高效显示策略(二):在ListView上异步加载网络图片
2014-11-02 12:50
483 查看
在ListView上的item显示网络图片,首先考虑的是2个因素
1.防止内存溢出
2.异步加载网络图片,防止ANR
针对以上2个问题,第一个问题解决方案是1.使用inSampleSize缩小图片,2.且及时回收使用的Bitmap。第二个问题的解决方案是使用AsyncTask,下面讨论这2个方法的具体实现,大部分代码来自于Google的ApiDemo:DisplayingBitmapsSample,本人只是做了少量修改。
使用inSampleSize,参考这篇:BitMap高效显示策略(一):大图的缩放和加载
回收Bitmap:Bitmap的回收使用recycle()方法。为了让代码能更好的复用,不建议在代码中手动调用recycle,因为Imageview通过BitmapDrawable去显示Bitmap,所以可以定义一个BitmapDrawable的子类去实现这个回收逻辑
这个RecyclingBitmapDrawable的setIsDisplayed方法在有ImageView调用setImageDrawable的时被调用,这个过程也不需要手动调用,可以通过实现ImageView的子类去实现。
以上通过实现2个类,可以做到bitmap的recycle方法隐式调用,不用手动管理,只要在布局文件中使用RecyclingImageView显示图片即可。
使用AsyncTask:
为了避免ANR,应该避免在主线程中进行网络下载/读取文件的操作,ListView上Item下载图片应使用AsyncTask。
在Adapter的getView中,为每个ImageView去启动一个AsyncTask用于下载图片。
为ImageView使用WeakReference 确保了 AsyncTask 所引用的资源可以被GC。因为当任务结束时不能确保 ImageView 仍然存在,因此你必须在 onPostExecute() 里面去检查引用。
以上的代码,在ListView不进行上下滑动的时候,是没问题的,但是如果在ListView进行滑动时,会出现问题:
为了内存的优化,ListView/GridView等组件,在Item被滑动到屏幕上看不到的位置时,会被回收,用作下个新的可视Item,如果每个Item上的ImageView都启动一个AsyncTask,则有可能出现当ListView向下划动时,新出现的Item是之前被回收的,那么这个Item加载图片的Url是上一个Item的Url,这样会造成图片显示顺序被打乱的情况。
针对这种情况的解决方法是:
定义一个AsyncDrawable继承BitmapDrawable,作为ImageView的占位Drawable,在加载图片的AsyncTask执行完毕后,在ImageView上显示图片。它保存一个最近使用的AsyncTask引用,加载图片的AsyncTask中也保存ImageVIew的引用。
在加载图片到指定的ImageView的任务开始前,判断和ImageView关联的,当前执行的任务是否之前已经在被执行(下面的cancelPotentialWork方法)。
在加载图片的任务的doInBackgound和onPostExecute中,再次通过AsyncDrawable中引用的 AsyncTask 判断当前的加载任务是否和之前的匹配(下面的getAttachedImageView方法)
代码:
ImageWorker.java:异步加载网络图片的核心类
cancelPotentialWork这个方法的作用是:BitmapWorkerTask开始前,检查当前要下载的图片(Url)是否和之前的一样,如果不一样,取消之前的任务,否则,继续执行之前的任务。
getAttachedImageView:返回当前绑定task的ImageView,如果ImageView中引用的task不是自己,则返回null
它们都调用了 getBitmapWorkerTask 方法。getBitmapWorkerTask获得与这个ImageView绑定的BitmapWorkerTask,用于和当前的BitmapWorkerTask比对
ImageResizer:继承ImageWorker,处理图片缩放。
这样相当于在 BitmapWorkerTask开始前,执行中,执行后都做了检查,所以避免了在这3个阶段的某个阶段中,ListView回收Item导致图片顺序打乱的问题。
ImageGridFragment:
布局:
list.xml:
效果:
Demo下载地址:
http://download.csdn.net/detail/ohehehou/8110965
1.防止内存溢出
2.异步加载网络图片,防止ANR
针对以上2个问题,第一个问题解决方案是1.使用inSampleSize缩小图片,2.且及时回收使用的Bitmap。第二个问题的解决方案是使用AsyncTask,下面讨论这2个方法的具体实现,大部分代码来自于Google的ApiDemo:DisplayingBitmapsSample,本人只是做了少量修改。
使用inSampleSize,参考这篇:BitMap高效显示策略(一):大图的缩放和加载
回收Bitmap:Bitmap的回收使用recycle()方法。为了让代码能更好的复用,不建议在代码中手动调用recycle,因为Imageview通过BitmapDrawable去显示Bitmap,所以可以定义一个BitmapDrawable的子类去实现这个回收逻辑
public class RecyclingBitmapDrawable extends BitmapDrawable { private int mCacheRefCount = 0; private int mDisplayRefCount = 0; private boolean mHasBeenDisplayed; public RecyclingBitmapDrawable(Resources res, Bitmap bitmap) { super(res, bitmap); } public void setIsDisplayed(boolean isDisplayed) { synchronized (this) { if (isDisplayed) { mDisplayRefCount++; mHasBeenDisplayed = true; } else { mDisplayRefCount--; } } checkState(); } public void setIsCached(boolean isCached) { synchronized (this) { if (isCached) { mCacheRefCount++; } else { mCacheRefCount--; } } checkState(); } private synchronized void checkState() { if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed && hasValidBitmap()) { getBitmap().recycle(); } } private synchronized boolean hasValidBitmap() { Bitmap bitmap = getBitmap(); return bitmap != null && !bitmap.isRecycled(); } }RecyclingBitmapDrawable有3个属性:mCacheRefCount记录被内存缓存引用的次数,mDisplayRefCount记录被imageview引用次数,mHasBeenDisplayed记录当前是否是显示状态。setIsDisplayed和setIsCached方法根据布尔值参数设置引用计数,改变计数后,调用checkState方法,在checkState中,如果该Drawable没有被引用或缓存,并且其中的Bitmap对象没被回收的话,则调用其recycle方法。
这个RecyclingBitmapDrawable的setIsDisplayed方法在有ImageView调用setImageDrawable的时被调用,这个过程也不需要手动调用,可以通过实现ImageView的子类去实现。
public class RecyclingImageView extends ImageView { public RecyclingImageView(Context context) { super(context); } public RecyclingImageView(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onDetachedFromWindow() { setImageDrawable(null); super.onDetachedFromWindow(); } @Override public void setImageDrawable(Drawable drawable) { final Drawable previousDrawable = getDrawable(); super.setImageDrawable(drawable); notifyDrawable(drawable, true); notifyDrawable(previousDrawable, false); } private static void notifyDrawable(Drawable drawable, final boolean isDisplayed) { if (drawable instanceof RecyclingBitmapDrawable) { ((RecyclingBitmapDrawable) drawable).setIsDisplayed(isDisplayed); } else if (drawable instanceof LayerDrawable) { LayerDrawable layerDrawable = (LayerDrawable) drawable; for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) { notifyDrawable(layerDrawable.getDrawable(i), isDisplayed); } } } }注意在重写的setImageDrawable方法中,调用 notifyDrawable, 将上一个被显示的drawable的引用计数-1,新的drawable引用+1。在Imageview被移除,onDetachedFromWindow被回调时,也对计数进行更新。
以上通过实现2个类,可以做到bitmap的recycle方法隐式调用,不用手动管理,只要在布局文件中使用RecyclingImageView显示图片即可。
使用AsyncTask:
为了避免ANR,应该避免在主线程中进行网络下载/读取文件的操作,ListView上Item下载图片应使用AsyncTask。
在Adapter的getView中,为每个ImageView去启动一个AsyncTask用于下载图片。
class BitmapWorkerTask extends AsyncTask<Void, Void, BitmapDrawable> { private final WeakReference imageViewReference; private int mUrl = 0; public BitmapWorkerTask(ImageView imageView,String url) { imageViewReference = new WeakReference(imageView); mUrl = url; } @Override protected Bitmap doInBackground(Void... params) { bitmap = processBitmap(mUrl); //子类实现 return bitmap; } @Override protected void onPostExecute(Bitmap bitmap) { if (imageViewReference != null && bitmap != null) { final ImageView imageView = imageViewReference.get(); if (imageView != null) { imageView.setImageBitmap(bitmap); } } } }
为ImageView使用WeakReference 确保了 AsyncTask 所引用的资源可以被GC。因为当任务结束时不能确保 ImageView 仍然存在,因此你必须在 onPostExecute() 里面去检查引用。
以上的代码,在ListView不进行上下滑动的时候,是没问题的,但是如果在ListView进行滑动时,会出现问题:
为了内存的优化,ListView/GridView等组件,在Item被滑动到屏幕上看不到的位置时,会被回收,用作下个新的可视Item,如果每个Item上的ImageView都启动一个AsyncTask,则有可能出现当ListView向下划动时,新出现的Item是之前被回收的,那么这个Item加载图片的Url是上一个Item的Url,这样会造成图片显示顺序被打乱的情况。
针对这种情况的解决方法是:
定义一个AsyncDrawable继承BitmapDrawable,作为ImageView的占位Drawable,在加载图片的AsyncTask执行完毕后,在ImageView上显示图片。它保存一个最近使用的AsyncTask引用,加载图片的AsyncTask中也保存ImageVIew的引用。
在加载图片到指定的ImageView的任务开始前,判断和ImageView关联的,当前执行的任务是否之前已经在被执行(下面的cancelPotentialWork方法)。
在加载图片的任务的doInBackgound和onPostExecute中,再次通过AsyncDrawable中引用的 AsyncTask 判断当前的加载任务是否和之前的匹配(下面的getAttachedImageView方法)
代码:
ImageWorker.java:异步加载网络图片的核心类
/*** * * ListView上每个item启动一个AsyncTask去下载图片,AsyncTask在容量为2的线程池上运行。 * 每个AsyncTask和item上的下载url和imageview绑定 * */ public abstract class ImageWorker { private boolean mExitTasksEarly = false; // 是否提前退出的标志 protected boolean mPauseWork = false; private final Object mPauseWorkLock = new Object(); protected Resources mResources; protected ImageWorker(Context context) { mResources = context.getResources(); } public void loadImage(String url, ImageView imageView) { if (url == null) { return; } if (cancelPotentialWork(url, imageView)) { final BitmapWorkerTask task = new BitmapWorkerTask(url, imageView); final AsyncDrawable asyncDrawable = new AsyncDrawable(mResources, task); imageView.setImageDrawable(asyncDrawable); task.executeOnExecutor(ImageAsyncTask.DUAL_THREAD_EXECUTOR); } } /** * * * 当item进行图片下载时,这个item有可能是以前回收的item,此时item上的imageView还和一个task关联着, * 检查当前的task和imageview绑定的以前的task是不是在下载同一个图片, 不匹配则终止以前运行的task,否则继续之前的下载 * * @param data * @param imageView * @return true表示需要重新下载 */ public static boolean cancelPotentialWork(String currentUrl, ImageView imageView) { BitmapWorkerTask oldBitmapWorkerTask = getBitmapWorkerTask(imageView); if (oldBitmapWorkerTask != null) { String oldUrl = oldBitmapWorkerTask.mUrl; if (oldUrl == null || !oldUrl.equals(currentUrl)) { oldBitmapWorkerTask.cancel(true); } else { return false; } } return true; } /*** * 下载图片的task。 ListView上Item的ImageView被回收重用后,绑定的下载图片task可能和之前的不一样, * Item回收可能发生在:task执行中,task执行后。这2种情况都需要校验,通过{@link #getAttachedImageView()} * 方法 * */ private class BitmapWorkerTask extends ImageAsyncTask<Void, Void, BitmapDrawable> { private String mUrl; private final WeakReference<ImageView> imageViewReference; public BitmapWorkerTask(String url, ImageView imageView) { mUrl = url; imageViewReference = new WeakReference<ImageView>(imageView); } @Override protected BitmapDrawable doInBackground(Void... params) { Bitmap bitmap = null; BitmapDrawable drawable = null; synchronized (mPauseWorkLock) { while (mPauseWork && !isCancelled()) { try { mPauseWorkLock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } if (bitmap == null && !isCancelled() && getAttachedImageView() != null && !mExitTasksEarly) { bitmap = processBitmap(mUrl); } if (bitmap != null) { drawable = new BitmapDrawable(mResources, bitmap); } return drawable; } @Override protected void onPostExecute(BitmapDrawable result) { if (isCancelled() || mExitTasksEarly) { result = null; } ImageView imageView = getAttachedImageView(); if (result != null && imageView != null) { setImageDrawable(imageView, result); } } @Override protected void onCancelled(BitmapDrawable value) { super.onCancelled(value); synchronized (mPauseWorkLock) { mPauseWorkLock.notifyAll(); } } /*** * 返回当前绑定task的ImageView,如果ImageView绑定的task不是自己,则返回null * * @return */ private ImageView getAttachedImageView() { ImageView imageView = imageViewReference.get(); BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (this == bitmapWorkerTask) { return imageView; } return null; } } /*** * 图片下载完毕后设置ImageView * * @param imageView * @param drawable */ private void setImageDrawable(ImageView imageView, Drawable drawable) { imageView.setImageDrawable(drawable); } /*** * 返回当前ImageView绑定的task * * @param imageView * @return */ private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { if (imageView != null) { Drawable drawable = imageView.getDrawable(); if (drawable instanceof AsyncDrawable) { final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; return asyncDrawable.getBitmapWorkerTask(); } } return null; } private static class AsyncDrawable extends BitmapDrawable { private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; public AsyncDrawable(Resources res, BitmapWorkerTask bitmapWorkerTask) { super(res); bitmapWorkerTaskReference = new WeakReference<ImageWorker.BitmapWorkerTask>( bitmapWorkerTask); } public BitmapWorkerTask getBitmapWorkerTask() { return bitmapWorkerTaskReference.get(); } } public void setPauseWork(boolean pauseWork) { synchronized (mPauseWorkLock) { mPauseWork = pauseWork; if (!mPauseWork) { mPauseWorkLock.notifyAll(); } } } public void setExitTasksEarly(boolean exitTasksEarly) { mExitTasksEarly = exitTasksEarly; setPauseWork(false); } /*** * 下载图片的代码由子类实现 * * @param url * @return */ protected abstract Bitmap processBitmap(String url); }
cancelPotentialWork这个方法的作用是:BitmapWorkerTask开始前,检查当前要下载的图片(Url)是否和之前的一样,如果不一样,取消之前的任务,否则,继续执行之前的任务。
getAttachedImageView:返回当前绑定task的ImageView,如果ImageView中引用的task不是自己,则返回null
它们都调用了 getBitmapWorkerTask 方法。getBitmapWorkerTask获得与这个ImageView绑定的BitmapWorkerTask,用于和当前的BitmapWorkerTask比对
ImageResizer:继承ImageWorker,处理图片缩放。
这样相当于在 BitmapWorkerTask开始前,执行中,执行后都做了检查,所以避免了在这3个阶段的某个阶段中,ListView回收Item导致图片顺序打乱的问题。
public class ImageResizer extends ImageWorker{ private static final String TAG = "ImageResizer"; private int mImageWidth; private int mImageHeight; public ImageResizer(Context context, int imageWidth, int imageHeight) { super(context); setImageSize(imageWidth, imageHeight); } public ImageResizer(Context context, int imageSize) { super(context); setImageSize(imageSize, imageSize); } protected ImageResizer(Context context) { super(context); } public void setImageSize(int width, int height) { mImageWidth = width; mImageHeight = height; } public int getmImageWidth() { return mImageWidth; } public int getmImageHeight() { return mImageHeight; } @Override protected Bitmap processBitmap(String url) { return null; } public static BitmapFactory.Options getSampledBitmapOptionsFromStream( InputStream is, int reqWidth, int reqHeight) { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeStream(is, null, options); // 计算缩放比例 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); options.inJustDecodeBounds = false; return options; } public static Bitmap decodeSampledBitmapFromStream( InputStream is, BitmapFactory.Options options) { return BitmapFactory.decodeStream(is, null, options); } /** * @Description: 计算图片缩放比例 * @param @param options * @param @param reqWidth * @param @param reqHeight * @param @return * @return int 缩小的比例 * @throws */ public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { final int halfHeight = height / 2; final int halfWidth = width / 2; while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { inSampleSize *= 2; } } return inSampleSize; } }ImageDownloader:继承ImageResizer,实现父类的processBitmap
public class ImageDownloader extends ImageResizer { public ImageDownloader(Context context) { super(context); } public ImageDownloader(Context context, int imageSize) { super(context, imageSize); } @Override protected Bitmap processBitmap(String url) { return downLoadByUrl(url); } public Bitmap downLoadByUrl(String urlString) { disableConnectionReuseIfNecessary(); HttpURLConnection urlConnection = null; InputStream in = null; try { final URL url = new URL(urlString); urlConnection = (HttpURLConnection) url.openConnection(); in = urlConnection.getInputStream(); final BitmapFactory.Options options = getSampledBitmapOptionsFromStream(in, getmImageWidth(), getmImageHeight()); in.close(); urlConnection.disconnect(); //重新获得输入流 urlConnection = (HttpURLConnection) url.openConnection(); in = urlConnection.getInputStream(); Bitmap bitmap = decodeSampledBitmapFromStream(in, options); return bitmap; } catch (final IOException e) { Log.e("text", "Error in downloadBitmap - " + e); } finally { if (urlConnection != null) { urlConnection.disconnect(); } try { if (in != null) { in.close(); } } catch (final IOException e) {} } return null; } public static void disableConnectionReuseIfNecessary() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) { System.setProperty("http.keepAlive", "false"); } } }
ImageGridFragment:
public class ImageGridFragment extends Fragment { private static final String TAG = "ImageGridFragment"; private ImageWorker mImageWorker; private ImageAdapter mAdapter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mImageWorker = new ImageFetcher(getActivity(), 50); } @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View v = inflater.inflate(R.layout.list, container, false); ListView listView = (ListView) v.findViewById(R.id.image_list); mAdapter = new ImageAdapter(getActivity()); listView.setAdapter(mAdapter); listView.setOnScrollListener(new AbsListView.OnScrollListener() { @Override public void onScrollStateChanged(AbsListView absListView, int scrollState) { if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) { if (Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { mImageWorker.setPauseWork(true); } } else { mImageWorker.setPauseWork(false); } } @Override public void onScroll(AbsListView absListView, int firstVisibleItem, int visibleItemCount, int totalItemCount) { } }); return v; } @Override public void onResume() { super.onResume(); mImageWorker.setExitTasksEarly(false); mAdapter.notifyDataSetChanged(); } @Override public void onPause() { super.onPause(); mImageWorker.setPauseWork(false); mImageWorker.setExitTasksEarly(true); } private class ImageAdapter extends BaseAdapter { private final Context mContext; private LayoutInflater mInflater; public ImageAdapter(Context context) { this.mContext = context; this.mInflater = LayoutInflater.from(mContext); } @Override public int getCount() { return Images.imageThumbUrls.length; } @Override public Object getItem(int position) { return null; } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { ImageView imageView; if (convertView == null) { convertView = mInflater.inflate(R.layout.list_item, parent, false); } imageView = (RecyclingImageView) convertView.findViewById(R.id.img); //参数:url,imageview mImageWorker.loadImage(Images.imageThumbUrls[position], imageView); return convertView; } } }MainActivity:
public class MainActivity extends ActionBarActivity { private static final String TAG = "ImageGridActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getSupportFragmentManager().findFragmentByTag(TAG) == null) { final FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); ft.add(android.R.id.content, new ImageGridFragment(), TAG); ft.commit(); } } }
布局:
list.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" > <ListView android:id="@+id/image_list" android:layout_width="match_parent" android:layout_height="match_parent" ></ListView> </LinearLayout>list_item.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" > <com.example.displayingbitamapdemov1.image.RecyclingImageView android:id="@+id/img" android:layout_width="60dp" android:layout_height="60dp" android:background="@drawable/ic_launcher" android:scaleType="centerCrop" /> </LinearLayout>
效果:
Demo下载地址:
http://download.csdn.net/detail/ohehehou/8110965
相关文章推荐
- ListView的常见优化:获取网络图片异步加载,分批加载,分页显示,图片缓存等优化方式
- Listview 设置两列异步加载网络图片,结果出现图片显示错位
- Android实习04:ListView网络异步加载图片的优化显示(2)
- ListView优化,获取网络图片异步加载,分批加载,分页显示,图片缓存等优化方式
- ListView的常见优化:获取网络图片异步加载,分批加载,分页显示,图片缓存等优化方式
- Android:ListView异步加载图片(实现网络下载、存储本地、缓存内存、压缩显示)
- Android实习03:ListView网络异步加载图片的优化显示(1)
- ListView的常见优化:获取网络图片异步加载,分批加载,分页显示,图片缓存等优化方式
- android ListView利用SimpleAdapter显示特定布局并且异步加载网络图片
- Android之ListView异步加载网络图片(优化缓存机制)
- Android 如何实现ListView异步加载网络图片
- ListView图片异步加载与缓存策略
- android开发--ListView+Json+异步网络图片加载+滚动翻页的例子(图片能缓存,图片不错乱)
- 解决ListView异步加载网络图片的各种问题(二)
- C# Winform DataGridView中利用WebClient异步加载显示网络图片
- Android中ListView使用- 网络图片的异步加载
- 高效地显示Bitmap图片 1 - 有效率地加载大尺寸的位图
- 关于ListView异步加载图片导致图片显示混乱以及ListView效率问题探讨
- Android之ListView异步加载网络图片(优化缓存机制)
- 解决ListView异步加载网络图片的各种问题(二)