android 用多线程提升性能

在创建响应式app中,一个非常好的做法就是确保你的主线程尽可能做最小量的工作。任何有可能导致你的app阻塞的长期任务都应该在不同的线程处理。这种任务的典型例子是网络操作,这种操作可能包含未知的延迟。用户可以容忍一些停顿、尤其是你如果在停顿时提供了正在进行的信息反馈。但如果直接卡死并且没有任何回馈信息,用户将不知所措。

在这篇文章中,我们将会创建一个简单的图片下载器来说明这种模式。接下来我们将会用ListView来展示从网上下载的缩略图,并且在后台创建一个异步的下载任务来保持app快速运行。

图片下载器

从网上下载图片是比较简单的,使用框架提供的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);
                return bitmap;
            } finally {
                if (inputStream != null) {
                    inputStream.close();  
                }
                entity.consumeContent();
            }
        }
    } catch (Exception e) {
        // Could provide a more explicit error message for IOException or IllegalStateException
        getRequest.abort();
        Log.w("ImageDownloader", "Error while retrieving bitmap from " + url, e.toString());
    } finally {
        if (client != null) {
            client.close();
        }
    }
    return null;
}

创建了客服端和http并请求。如果请求成功,服务器将会相应一个包含图片字节流的实体,字节流将会被解码成Bitmap并且返回给用户。注意,你必须需要在manifest添加网络权限。

注意:在BitmapFactory.decodeStream以前的版本中存在一个bug,可能阻止这代码在一个慢连接上执行。使用FlushedInputStream(inputStream) 代替解决这个问题。
下面是这个帮助类的实现:

static class FlushedInputStream extends FilterInputStream {
    public FlushedInputStream(InputStream inputStream) {
        super(inputStream);
    }

    @Override
    public long skip(long n) throws IOException {
        long totalBytesSkipped = 0L;
        while (totalBytesSkipped < n) {
            long bytesSkipped = in.skip(n - totalBytesSkipped);
            if (bytesSkipped == 0L) {
                  int byte = read();
                  if (byte < 0) {
                      break;  // we reached EOF
                  } else {
                      bytesSkipped = 1; // we read one byte
                  }
           }
            totalBytesSkipped += bytesSkipped;
        }
        return totalBytesSkipped;
    }
}

这可以确保skip()实际跳过提供的字节数,除非到达了文件的结尾。

如果你在ListAdapter中的getView方法中直接使用这个方法,将会导致滚动卡顿。每一个新视图的显示都需要等待图片的下载,这样将会阻止平滑的滚动。

实际上,这是一个非常糟糕的想法,AndroidClient不允许自己在主线程运行。这上面的代码将会显示”该线程禁止Http请求”错误信息。如果你真的想要向你自己的脚设计,可以使用DefaultHttpClient.

引入异步任务

AsyncTask 类提供了从UI线程启动一个新任务的最简单方法之一。我们来创建一个ImageDownloader类,它将会负责创建这些线程。它将会提供一个下载方法,将从URL下载图片分配给ImageView:

public class ImageDownloader {

    public void download(String url, ImageView imageView) {
            BitmapDownloaderTask task = new BitmapDownloaderTask(imageView);
            task.execute(url);
        }
    }

    /* class BitmapDownloaderTask, see below */
}

BitmapDownloaderTask是实际上下载图片的AsyncTask.它用execute启动,立即返回,因此使得这个方法非常快,这是最终目的,因为它将从UI线程中调用。这里有它的实现类:

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

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

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

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

        if (imageViewReference != null) {
            ImageView imageView = imageViewReference.get();
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

doInBackground 方法实际上是运行在task自己的进程中,它只是简单的使用我们在文章开始实现的dwonloadBitmap方法。

当任务完成后,onPostExecute在调用UI线程中运行。它将返回的Bitmap做为参数,它只和提供下载和存储在BitmapDownloaderTask中的ImageView简单的关联。请注意,这个ImageView是以WeakReference形式存储,所以,在进行下载的时候并不会阻止GC回收销毁的activity’s ImageView 。这就解释了为什么在onPostExecute中使用它们之前必须检查弱引用和ImageView不为空。

这个简化的例子演示了AsyncTask的使用,如果你去使用它,你将会看到这几行代码实际上极大的提高ListView的性能,并且滚动流畅。阅读Read Painless获取AsyncTasks的更多信息。

然而,一个ListView特定行为揭示了我们当前实现的一个问题。
实际上,出于内存效率的理由,当用户滑动时ListView将会循环的展示这些视图。如果滑动列表,给定的imageView对象将会被使用多次。每次显示ImageView都会正确的触发图片下载任务,最终都会改变ImageView的图片。所以,问题在哪?和多数平行的应用程序一样,关键的问题在于排序。在我们的例子中,不能保证下载任务按启动顺序完成。在列表最终显示的图片可能来自上一个项,而恰好该项需要更长的时间来下载。如果你为所有的ImageViews绑定下载图片,那这就没有问题了,但让我们来解决它在列表使用的常见问题。

处理并发

为了解决这个问题,我们应该记住下载的顺序,以便于上次开始的能有效的展示。每个ImageView记住最后的下载确实足够了。我们将使用Drawable子类在ImageVeiw中添加这些额外的信息,它将在下载的时候临时绑定到这个ImageView。这是我们的DownloadedDrawable代码:

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();
    }
}

该实现由ColorDrawable支持,这将会导致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上可能进行的下载。注意,这并不能足够保证最新的下载被展示,因为这个任务可能已经被完成了,它可能在onPostExecute方法中等待,它可以在一个新任务开启后依然可以执行。

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方法来停止这个下载。大多数的时间它都是返回true,以便于可以在下载中启动一个下载,我们不想这种情况发生的唯一理由是相同的URL已经在下载了,在这种情况下,我们只是让它继续下载。注意在这种实现下,如果ImageView被GC,它关联的下载不会被停止。RecyclerListener也许可以在这种情况下使用。

这个方法使用一个getBitmapDownloaderTask的帮助方法,它真是非常精确。

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类提供了我们期待的基本的服务。你可以自由的使用它,或者在你的应用程序中演示这个异步模板来确定他们的响应。

Demo

文章的源代码可以在Google Code上在线获取。你可以在文章中讨论的三种不同的实现(没有异步任务、没有Bitmap关联任务和最终正确的版本)之间切换比较。注意,它的缓存大小已经被限制在10个图像,以便于更好的演示这个问题。

进一步优化

这个代码只是简单关注了平行方面,还有许多有用的特性在我们的实现中缺失。ImageDownloader类将首先获取缓存是明显有益的,特别是如果结合listView使用的时候,因为可以能出现当用户上下滚动时,相同的图片会出现多次。这个能被简单的实现,使用最少的记录使用缓存机制,它可以使用以Url为key的LinkedHashMap来存储Bitmap的SoftReferrences。更多的有关缓存机制也可以在硬盘上存储图片。如果你需要,也可以创建缩略图和调整图片大小。

我们的下载能正确处理下载错误和超时,在这种情况下它会返回一个null Bitmap.这个时候你可能想用一张错误的图片来代替。

我们的Http请求是非常简单的。你可能需要根据某些网站要求在请求中添加参数或者cookies。

在文章中,AsyncTask使用是真的很便利,可以轻易的从UI线程推迟工作。你也许想要使用Handler来对你的操作做更好的控制。列如在当前例子中控制并行的下载线程的总数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值