您的位置:首页 > 理论基础 > 计算机网络

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的子类去实现这个回收逻辑

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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐