Multithreading For Performance 使用异步线程优化界面效果

第二篇 Fragment For All感觉没翻译必要,现在翻译第三篇。


Multithreading For Performance

多线程优化(注:从字面理解意思应该是使用多线程优化功能)

[This post is by Gilles Debunne, an engineer in the Android group who loves to get multitasked. — Tim Bray]

{本文由Gilles Debunne 发表,他是Android group(安卓项目组)中以为能够同时完成多个工作的人—— Time Bray]

A good practice in creating responsive applications is to make sure your main UI thread does the minimum amount of work. Any potentially long task that may hang your application should be handled in a different thread. Typical examples of such tasks are network operations, which involve unpredictable delays. Users will tolerate some pauses, especially if you provide feedback that something is in progress, but a frozen application gives them no clue.

对于创建一个响应app而言,一个很好的经验是让你的UI线程做最少的工作;任何潜在的可能消耗较长时间的任务都应该在另外一个线程被执行。最典型的例子是网络操作,它会带来不确定的延时。用户可以容忍稍许暂停,特别是app提供了进度条的时候;但一个没有任何响应的App让用户不知道他是在运行,还是已经崩溃了。(注:现在Android开发已经不允许在UI线程执行网络事件,会直接崩溃)。

In this article, we will create a simple image downloader that illustrates this pattern. We will populate a ListView with thumbnail images downloaded from the internet. Creating an asynchronous task that downloads in the background will keep our application fast.

在本文中,我们会使用一个简单的图片下载器来阐释这种模式。我们会使用一个带有缩略图的ListView;缩略图存放在网络上,使用异步线程从网上下载以加快App的响应速度。(注:这种方式可以让界面不需要等待网络真实的图片就可以显示,即图片的现实会有延迟,但界面很流畅;github上有个Ion项目可以很好的处理这个问题)。

An Image downloader

图片下载器

Downloading an image from the web is fairly simple, using the HTTP-related classes provided by the framework. Here is a possible implementation:

从网络上下载图片是很简单的,使用系统框架提供的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;
}

A client and an HTTP request are created. If the request succeeds, the response entity stream containing the image is decoded to create the resulting Bitmap. Your applications' manifest must ask for the INTERNET to make this possible.

(代码中)创建了一个Http客户端,并发送一个Http请求;如果请求成功了,返回值的实体里面就是我们要的图片,可以把他转换成Bitmao。注意,需要在app的Manifest中添加请求网络权限(注:请求网络权限:android.permission.INTERNET)。

Note: a bug in the previous versions of BitmapFactory.decodeStream may prevent this code from working over a slow connection. Decode a new FlushedInputStream(inputStream) instead to fix the problem. Here is the implementation of this helper class:

注意:BitmapFactory.decodeStream()的之前版本中有一个bug,在网络较差时代码可能无法正常执行。使用心得FlushInputStream()可以解决这个问题。下面是解决这个问题的一个辅助类,

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

This ensures that skip() actually skips the provided number of bytes, unless we reach the end of file.

上面的代码保证了skip()函数真的跳过了对应的字节数,直到读到文件结尾。

If you were to directly use this method in your ListAdapter's getView method, the resulting scrolling would be unpleasantly jaggy. Each display of a new view has to wait for an image download, which prevents smooth scrolling.

如果你直接在ListAdapter的getView()函数直接调用上面的downloadBitmap()函数,会让你的界面在滑动的时候卡得让人难受,因为没展示一个view都得等到图片被下载后才行。

Indeed, this is such a bad idea that the AndroidHttpClient does not allow itself to be started from the main thread. The above code will display "This thread forbids HTTP requests" error messages instead. Use the DefaultHttpClient instead if you really want to shoot yourself in the foot.

实际上,AndroidHttpClientt类根本不可以在主线程中被执行,上面的代码会抛出“This thread forbids HTTP requests”错误。如果想一脚中的,建议使用DefaultHttpClient。(注:shoot sb. in the foot应该是固定短语,没有查什么意思,突然想到一脚中的这个词,觉得应该贴切)。

Introducing asynchronous tasks

异步线程介绍

The AsyncTask class provides one of the simplest ways to fire off a new task from the UI thread. Let's create an ImageDownloader class which will be in charge of creating these tasks. It will provide a download method which will assign an image downloaded from its URL to an ImageView:

AsyncTask 可以很方便地从UI线程中创建一个新的任务。现在我们来创造一个I使用异步任务的mageDownloader类,它提供了一个下载函数,可以制定下载地址和对应放置图片的ImageView。

public class ImageDownloader {

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

    /* class BitmapDownloaderTask, see below */
}

The BitmapDownloaderTask is the AsyncTask which will actually download the image. It is started using execute, which returns immediately hence making this method really fast which is the whole purpose since it will be called from the UI thread. Here is the implementation of this class:

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

The doInBackground method is the one which is actually run in its own process by the task. It simply uses the downloadBitmap method we implemented at the beginning of this article.

doInBackground函数是具体执行任务的代码。它只是简单地调用了文章开头的downloadBitmap()函数。

onPostExecute is run in the calling UI thread when the task is finished. It takes the resulting Bitmap as a parameter, which is simply associated with the imageView that was provided to download and was stored in the BitmapDownloaderTask. Note that this ImageView is stored as a WeakReference, so that a download in progress does not prevent a killed activity's ImageView from being garbage collected. This explains why we have to check that both the weak reference and the imageView are not null (i.e. were not collected) before using them in onPostExecute.

当任务完成的时候,onPostExecute函数将会在UI线程中被执行,它只是简单地把doInBackground()中下载的图片和对应ImageView关联起来。注意ImageView对象是作为弱引用保存的(WeakReference),所以异步线程的下载不会影响到Activity是否释放ImageView对象。这也是为什么我们在调用onPostExecute()之前要判断ImageView是不是为null(确保没有被回收)。

This simplified example illustrates the use on an AsyncTask, and if you try it, you'll see that these few lines of code actually dramatically improved the performance of the ListView which now scrolls smoothly. Read Painless threading for more details on AsyncTasks.

这个简单的实例阐明了如何使用AsyncTask;如果你尝试这个代码,你将看到这几行代码实际上大大地提高了ListView滑动的流畅性。可以阅读 Painless thread 这篇文章来了解更多关于AsyncTask的内容(注地址:http://android-developers.blogspot.com/2009/05/painless-threading.html)。

However, a ListView-specific behavior reveals a problem with our current implementation. Indeed, for memory efficiency reasons, ListView recycles the views that are displayed when the user scrolls. If one flings the list, a given ImageView object will be used many times. Each time it is displayed the ImageView correctly triggers an image download task, which will eventually change its image. So where is the problem? As with most parallel applications, the key issue is in the ordering. In our case, there's no guarantee that the download tasks will finish in the order in which they were started. The result is that the image finally displayed in the list may come from a previous item, which simply happened to have taken longer to download. This is not an issue if the images you download are bound once and for all to given ImageViews, but let's fix it for the common case where they are used in a list.

然而,ListView有个特别的行为是会影响到我们上面代码的功能。为了更有效地使用内存,ListView在滚动的时候会重复地使用同一个view对象,即滚动的时候,一个ImageView会循环被使用;没显示一次就会触发一次图片下载行为来改变ImageView显示的图片。那么问题在哪里呢?对于大部分的app而言,主要的问题是排序。在我们的例子里,我们并不能保证文件下载结束的顺序和它们被开始的顺序一致;这会导致ImageView上最终显示的图片可能是上一个任务的图片,原因仅仅是因为前面一个任务虽然先开始但更慢结束。如果下载图片都显示在不同的ImageView上面是这完全不是问题,不过我们还是要修复这个对于listView还说普遍存在的问题。

Handling concurrency

并发处理

To solve this issue, we should remember the order of the downloads, so that the last started one is the one that will effectively be displayed. It is indeed sufficient for each ImageView to remember its last download. We will add this extra information in the ImageView using a dedicated Drawable subclass, which will be temporarily bind to the ImageView while the download is in progress. Here is the code of our DownloadedDrawable class:

为了解决这个问题,我们要保持下载的顺序,保证最后开始的任务是最终会被现实出来的。这个记住每个ImageView最后开始的下载任务就可以了。我们专门创建一个Drawable的子类来保存这个额外的信息(注:hich will be temporarily bind to the ImageView while the download is in progress这句没翻译)。下面是这个DownloaedDrawable类的代码:

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

This implementation is backed by a ColorDrawable, which will result in the ImageView displaying a black background while its download is in progress. One could use a “download in progress” image instead, which would provide feedback to the user. Once again, note the use of a WeakReference to limit object dependencies.

DownloadedDrawable类继承于ColorDrawable,它先让ImageView在图片没有下载完成时显示一张黑色的图片。这里也可以使用其他图片让用户知道图片正在正在下载。同样的,这里也使用了弱引用来确保不会影响ImageView的释放。

Let's change our code to take this new class into account. First, the download method will now create an instance of this class and associate it with the imageView:

接下来我们使用新的的类来修改我们的代码:首先,下载函数会生成一个DownloadedDrawable对象并和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);
     }
}

The cancelPotentialDownload method will stop the possible download in progress on this imageView since a new one is about to start. Note that this is not sufficient to guarantee that the newest download is always displayed, since the task may be finished, waiting in its onPostExecute method, which may still may be executed after the one of this new download.

下面的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 uses the cancel method of the AsyncTask class to stop the download in progress. It returns true most of the time, so that the download can be started in download. The only reason we don't want this to happen is when a download is already in progress on the same URL in which case we let it continue. Note that with this implementation, if an ImageView is garbage collected, its associated download is not stopped. A RecyclerListener might be used for that.

cancelPotentialDownload()函数调用AsyncTask的cancel()函数来取消正在进行的下载。大部分情况它都会返回true,然后我们可以开始新的下载。但当新的下载地址和旧的地一样时,我们并不希望取消这个任务。注意在这个代码中,如果一个ImageView已经被回收,那么它对应的下载任务不会被停止。使用ReycyclerListener可以解决这个问题。

This method uses a helper getBitmapDownloaderTask function, which is pretty straigth forward:

(注:不懂什么意思)。

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

Finally, onPostExecute has to be modified so that it will bind the Bitmap only if this ImageView is still associated with thisdownload process:

最后,onPostExecure()函数也要修改,只有在ImageView依旧和下载任务绑定的时候下显示下载下来的图片。

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

With these modifications, our ImageDownloader class provides the basic services we expect from it. Feel free to use it or the asynchronous pattern it illustrates in your applications to ensure their responsiveness.

Demo

The source code of this article is available online on Google Code. You can switch between and compare the three different implementations that are described in this article (no asynchronous task, no bitmap to task association and the final correct version). Note that the cache size has been limited to 10 images to better demonstrate the issues.

Future work

更多工作

This code was simplified to focus on its parallel aspects and many useful features are missing from our implementation. The ImageDownloader class would first clearly benefit from a cache, especially if it is used in conjuction with a ListView, which will probably display the same image many times as the user scrolls back and forth. This can easily be implemented using a Least Recently Used cache backed by a LinkedHashMap of URL to Bitmap SoftReferences. More involved cache mechanism could also rely on a local disk storage of the image. Thumbnails creation and image resizing could also be added if needed.


Download errors and time-outs are correctly handled by our implementation, which will return a null Bitmap in these case. One may want to display an error image instead.

Our HTTP request is pretty simple. One may want to add parameters or cookies to the request as required by certain web sites.

The AsyncTask class used in this article is a really convenient and easy way to defer some work from the UI thread. You may want to use the Handler class to have a finer control on what you do, such as controlling the total number of download threads which are running in parallel in this case.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值