本帖最后由 Andy.Ma 于 2013-3-26 14:23 编辑
在Android App中,任何耗费时间的操作都可能堵塞UI线程,你必须将这些耗费时间的操作放在额外的线程中。例如与网络交互的一些操作,就会包括不确定的延迟。
接下来,我们将用异步线程做一个下载图片的例子:界面上会呈现图片列表,图片将从网络上下载下来。首先我们将新建一个异步Task,将下载图片的操作放到后台中执行,确保App的UI线程不会被阻塞。
public class ImgLoadTask extends AsyncTask<String, Void, Bitmap> {
private String url;
private WeakReference<ImageView> mImgViewReference;
public ImgLoadTask(ImageView view){
mImgViewReference = new WeakReference<ImageView>(view);
}
@Override
protected Bitmap doInBackground(String... params) {
url = params[0];
ImgLoader loader = new ImgLoader();
return loader.downloadBitmap(url);
}
@Override
protected void onPostExecute(Bitmap result) {
if(result!=null && mImgViewReference!=null){
ImageView imgView = mImgViewReference.get();
if(imgView!=null){
imgView.setImageBitmap(result);
}
}
}
}
注意,以上代码用ImageView用了一个虚引用(WeakReference),目的是当task正在执行下载的时候,如果当前的Activity要被Kill或销毁掉时,GC可以回收这个ImageView.所以在onPostExecute方法中要检测mImgViewReference和imgView是否为null的情况。
当你把Task部署上去,并进行execute后,你发现图片显示出来了,但是当你滑动list的时候,图片的显示会错乱。这是因为Android框架考虑到使用内存效率的原因。当滑动列表的时候,ListView会重新使用以显示过的View。
所以如果你快速滑动(就是一个Fling的手势)列表,ImageView会被重复用到很多次。在每次正确的显示ImageView的时,都会触发一个下载图片的ImgLoadTask,ImgLoadTask会最终改变它显示的图像。
到底最终问题出现在什么地方呢?答案是下载顺序的问题引起的。
因为ImgLoadTask在下载图片是异步的,它无法保证先执行execute的task就先执行完,也就是说不能保证有序的执行。所以最终显示的图片可能是很早就被execute的task(这个Task可能花费了很多时间来下载)。
知道了问题出现的原因,接下来就解决这个问题。
解决这个问题,就要记住下载的顺序,来保证最后执行execute的task下载的图片被正确显示出来。在这里使用Drawable的子类,在Task执行过程中,他会临时绑定ImageView。代码如下:
static class DownloaderDrawable extends BitmapDrawable {
private WeakReference<ImgLoadTask> mTaskReference;
public DownloaderDrawable (Bitmap defaultImg,ImgLoadTask task){
super(defaultImg);
mTaskReference = new WeakReference<ImgLoadTask>(task);
}
public ImgLoadTask getDrawableTask(){
return mTaskReference.get();
}
}
注意:Drawable为什么会持有ImageView的引用的原因,请参看之前分享的文章“[Android分享]Context引起的内存泄露分析”
参数defaultImg的作用是:当图片还没下载完或下载失败时显示的默认图片。当然你也可以根据不同情况继承不同的Drawable。
现在可以编写下载的入口代码了,如下:
public static void loadBitmap(Bitmap defaultImg,String url,ImageView imgView){
if(cancelPotentialDownload(url,imgView)){
ImgLoadTask task = new ImgLoadTask(imgView);
DownloaderDrawable drawable=new DownloaderDrawable(defaultImg, task);
imgView.setImageDrawable(drawable);
task.execute(url);
}
}
在一个ImageView将要启动一下下载图片Task时,cancelPotentialDownload方法将会停止该ImageView上正在下载图片的Task。需要注意的是,这种做法并不能保证最新的下载的图片总会被显示,应为之前的Task可能已经Finish了。解决这个问题的方法将在Task的onPostExecute方法中解决,之后会进行介绍。下面是cancelPotentialDownload的实现:
public static boolean cancelPotentialDownload(String url,ImageView imgView){
ImgLoadTask task = getImgLoadTask(imgView);
if(task !=null){
String taskUrl = task.url;
if(url == null || !url.equals(taskUrl)){
task.cancel(true);
}else{
// The same URL is already being downloaded.
return false;
}
}
return true;
}
上面代码用cancel方法停止正在下载的Task。如果一个正在下载的Task的URL跟当前的URL相同,当前就不用启动新的Task。用这种实现方式需要注意的是,如果某一个ImageView被GC回收,它相关的Task并不会终止。你可以用RecyclerListener接口来控制。
getImgLoadTask方法代码如下:
public static ImgLoadTask getImgLoadTask(ImageView imgView){
if(imgView!=null){
Drawable imgDrawable = imgView.getDrawable();
if(imgDrawable!=null){
if(imgDrawable instanceof DownloaderDrawable){
DownloaderDrawable downDrawable = (DownloaderDrawable)imgDrawable;
return downDrawable.getDrawableTask();
}
}
}
return null;
}
最后修改一下Task中的onPostExecute方法实现:
@Override
protected void onPostExecute(Bitmap result) {
if (result != null && mImgViewReference != null) {
ImageView imgView = mImgViewReference.get();
if (imgView != null) {
ImgLoadTask task = ImgLoader.getImgLoadTask(imgView);
if (task != null && this == task) {
imgView.setImageBitmap(result);
}
}
}
}
Ok,利用后台多线程来加载数据的介绍就到这里。但是这个代码还需要进一步优化处理,比如需要缓存图片。。。之后我会再分享个大家。
有需要源码的可直接留言给我或者私信。THX
|