性能之多线程

创建一个反应敏捷的应用时,一个重要的技巧就是保证你的UI线程做少量的工作。任何潜在的长耗时任务应该在其他线程中处理。经典的例子就是受不可预料延迟影响的网络操作。用户可以容忍一些打断,特别是当你提供一些反馈说明正在处理一些东西,但是一个冻结的应用对用户来说没有任何提示。

在这篇文章中,我们将创建一个简单的图片下载器来加以演示。我们将从网络下载一些缩略图来填充ListView。创建一个异步任务用来在后台下载将使我们的应用更流畅。

An Image downloader

从网络下载图片很简单,试用framwork提供的HTTP相关类。以下是一种实现:

static Bitmap downloadBitmap(String url) {
        final AndroidHttpClient client = AndroidHttpClient.newInstance("Android");
        final HttpGet getRequest = new HttpGet(url);

        try {
            HttpResponse response = client.execute(getRequest);
            final int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode != HttpStatus.SC_OK) {
                Log.w("ImageDownloader", "Error " + statusCode + " while retrieving bitmap from " + url);
                return null;
            }

            final HttpEntity entity = response.getEntity();
            if (entity != null) {
                InputStream inputStream = null;
                try {
                    inputStream = entity.getContent();
                    final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
                } finally {
                    if (inputStream != null) {
                        inputStream.close();
                    }
                    entity.consumeContent();
                }
            }


        } catch (IOException e) {
            getRequest.abort();
            Log.w("ImageDownloader", "Error while retrieving bitmap from " + url, e);
        } finally {
            if (client != null) {
                client.close();
            }
        }
        return null;
    }
如果你直接在ListAdapter的getView方法直接调用这个方法,将导致滚动效果很不好。每一个新view的显示必须等待图片下载,使得滚动不流畅。

实际上,这是不中糟糕的做法由于AndroidHttpClient不允许在主线程启动。上面的代码将显示“This thread forbids HTTP requsets"错误提示。

异步任务介绍

AsyncTask类提供一种最简单的方式。让我们创建一个ImageDownloader类来负责创建这些任务。它将提供一个download方法来给一个Im设置从指定URL下载的图片

这个BitmapDownloaderTask就是下载图片的AsyncTask。它调用execute来启动,直接返回因此是这个方法非常执行很快。以下是这个类的实现:

class BitmapDownloaderTask extends AsyncTask<String, Void, Bitmap> {
        private String url;
        private final WeakReference<ImageView> imageViewReference;

        public BitmapDownloaderTask(ImageView imageView) {
            imageViewReference = new WeakReference<ImageView>(imageView);
        }

        // Actual download method, run in the task thread
        @Override
        protected Bitmap doInBackground(String... strings) {
            // params comes from the execute() call: params[0] is the url
            return downloadBitmap(strings[0]);
        }

        // Once the image is download, associates it to the imageView
        @Override
        protected void onPostExecute(Bitmap bitmap) {
            if (isCancelled()) {
                bitmap = null;
            }

            if (imageViewReference != null) {
                ImageView imageView = imageViewReference.get();
                if (imageView != null) {
                    imageView.setImageBitmap(bitmap);
                }
            }
        }
    }
doInBackground方法是实际在工作线程中运行的方法。它直接调用我们在文章开头实现的方法。

onPostExecute方法当任务完成是运行在UI线程。它将返回的Bitmap作为参数。注意ImageView使用了弱引用,以使在下载过程中当activity被杀时不会阻止ImageView被垃圾回收。这解释了我们为什么在onPostExecute方法中必须检查弱引用和imageview非空(例如未被回收)。

这个简化的例子介绍了AsyncTask的使用,如果你运行后会发现这几行代码明显提升了ListView的性能使得滑动很流畅。

但是,我们当前的实现ListView的行为会导致一个问题。实际上,由于效率原因,ListView会回收利用当用户滑动时显示的视图。如果fling列表,一个ImageView对象将被使用多次。每次显示相应ImageView时触发一个图片下载任务,最终改变它的图片。所以问题在哪呢?关键问题在于顺序。在我们的例子中,没有任何保证下载任务会按照开启的顺序来完成。这就导致图片最终在列表中显示在上一个item。让我们来解决这个问题。

处理并发

为了解决这个问题,我们应该记住下载的顺序,使得上一个启动下载的将被有效显示。对每个ImageView来说记住它的最近下载很容易。我们在ImageView使用Drawable子类来加入这些信息。当正在下载时,这个Drawable子类将绑定到ImageView。以下是DownloaderDrawable的代码:

static class DownloadedDrawable extends ColorDrawable {
    private final WeakReference<BitmapDownloaderTask> bitmapDownloaderTaskReference;

    public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) {
        super(Color.BLACK);
        bitmapDownloaderTaskReference =
            new WeakReference<BitmapDownloaderTask>(bitmapDownloaderTask);
    }

    public BitmapDownloaderTask getBitmapDownloaderTask() {
        return bitmapDownloaderTaskReference.get();
    }
}
ColorDrable使得ImageView在下载时显示一个黑色背景。当然我们可以使用替代图片显示“正在加载”。再次注意这里我们使用了WeakReference来限制对象依赖性。

让我们修改代码来使用这个新类。首先,在download方法中创建一个实例并与ImageView关联。

public void download(String url, ImageView imageView) {
     if (cancelPotentialDownload(url, imageView)) {
         BitmapDownloaderTask task = new BitmapDownloaderTask(imageView);
         DownloadedDrawable downloadedDrawable = new DownloadedDrawable(task);
         imageView.setImageDrawable(downloadedDrawable);
         task.execute(url, cookie);
     }
}

cancelPotentialDownload方法将停止ImageView可能正在进行的下载,因为将启动一个新任务。

private static boolean cancelPotentialDownload(String url, ImageView imageView) {
    BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);

    if (bitmapDownloaderTask != null) {
        String bitmapUrl = bitmapDownloaderTask.url;
        if ((bitmapUrl == null) || (!bitmapUrl.equals(url))) {
            bitmapDownloaderTask.cancel(true);
        } else {
            // The same URL is already being downloaded.
            return false;
        }
    }
    return true;
}

cancelPotentialDownload调用AsyncTask的cancel方法停止正在进行的系在。它通常将返回ture。特例就是当同一个URL下载正在进行时。此方法调用了一个helper方法。

private static BitmapDownloaderTask getBitmapDownloaderTask(ImageView imageView) {
    if (imageView != null) {
        Drawable drawable = imageView.getDrawable();
        if (drawable instanceof DownloadedDrawable) {
            DownloadedDrawable downloadedDrawable = (DownloadedDrawable)drawable;
            return downloadedDrawable.getBitmapDownloaderTask();
        }
    }
    return null;
}
最后,修改onPostExecute方法,使得ImageView只绑定与它相关的下载回来的Bitmap。

if (imageViewReference != null) {
    ImageView imageView = imageViewReference.get();
    BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);
    // Change bitmap only if this process is still associated with it
    if (this == bitmapDownloaderTask) {
        imageView.setImageBitmap(bitmap);
    }
}

做了这些修改之后,我们的ImageDownloader类完成了我们期待的效果。

源码下载,请点击这里

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值