`
sacredlove
  • 浏览: 12188 次
  • 性别: Icon_minigender_2
  • 来自: 北京
社区版块
存档分类
最新评论

利用多线程提高程序性能

阅读更多
要想搞出一个反应迅速的Android应用程序,一个很好的做法就是确保在主UI线程里执行尽量少的代码。任何有可能花费较长时间来执行的代码如果在主UI线程执行,则会让程序挂起无法响应用户的操作,所以应该放到一个单独的线程里执行。典型的例子就是与网络通信相关的操作了,因为通过网络收发信息的快慢我们无法预测,有可能“biu”地一下就搞定了,也有可能磨磨唧唧半天。用户心情好的话可能会容忍一点点迟延,而且前提是你给出了必要的提示,但是一个看上去根本不动貌似嗝儿屁的程序……(译注:就好比Ajax技术出现之前的网页,用户可以习惯短时间的载入,但是一个载入了半天都是空白的浏览器窗口就常常让那个拨号时代的我们感到困惑和抓狂。)
在这篇文章中,我们将创建一个简单的图片下载程序来演示一下多线程模式。我们将从网上下载一坨图片,然后用这些图片生成一个缩略图列表。创建一个异步工作的任务,让它在后台下载图片,会让我们的程序看上去更快。(译注:这里我加上“看上去”,因为我认为所谓多线程让程序更快,更多的意义在于“提高对用户操作的响应”。包括本文题目,所谓的“高性能”,主要指的还是避免UI的硬直(格斗游戏术语,请自行google)、挂起。毕竟多线程无法避免代码固有的主要资源开销。)

一个图片下载器

从web下载图片很简单,使用SDK提供的HTTP相关的类即可实现。下面是一个简单的实现。
(译注:下面用到的AndroidHttpClient等类从2.2版,也就是API Level 8才开始提供。请2.1以下各位从代码领会精神即可。直接用HttpClient应该亦可实现。)
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客户端和HTTP请求。如果请求成功,就把响应中包含的图片内容解码成位图格式并返回,以备后续使用。另外补充一句,为了让程序可以访问网络,必须在程序的manifest文件中声明使用INTERNET。
注意:旧版的BitmapFactory.decodeStream有个bug,可能使得在网络较慢的时候无法正常工作。可以使用 FlushedInputStream(inputStream)代替原始的inputStream来解决这个问题。下面是这个helper class的实现:
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方法中直接使用上面的downloadBitmap方法,结果可以想象的出,随着我们滚动屏幕,一定是一顿一顿很不爽的。因为每显示一个新的view,都必须等待一张图片完成下载,势必会影响滚屏的流畅度。

正是因为这想都想得出来的糟糕体验,AndroidHttpClient根本就不允许在主线程里启动!上面的代码在主线程里将会提示“本线程无法进行HTTP请求”。如果你不见棺材不落泪,说啥也要亲手试试这糟糕的用户体验的话,可以用DefaultHttpClient代替 AndroidHttpClient,给自己一个交代。

异步任务

AsyncTask类提供了一个从主线程生成新任务的方法。让我们创建一个ImageDownloader类来负责生成任务。这个类将提供一个download方法,从指定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方法启动,该方法是立即返回的,从而使得调用它的主线程代码可以迅速执行完毕。这正是我们使用AsyncTask的意义所在。下面是BitmapDownloaderTask的实现:

class BitmapDownloaderTask extends AsyncTask {
private String url;
private final WeakReference imageViewReference;

public BitmapDownloaderTask(ImageView imageView) {
imageViewReference = new WeakReference(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方法是真正在单独进程中执行异步任务的代码。它调用前面介绍的downloadBitmap方法,完成下载,取得位图。
onPostExecute在任务结束后由主线程调用。它通过传入的参数得到下载回来的位图,并设置到ImageView显示(该ImageView在实例化BitmapDownloaderTask时传入)。需要注意的是这里对ImageView的引用是以WeakReference的形式保存在 BitmapDownloaderTask实例里,所以在下载过程中如果activity被关掉,无法阻止activity里的ImageView被回收。因此我们必须在使用前检查imageViewReference和imageview是否为空。
这个简单的小例子演示了如何使用AsyncTask。如果你亲自动手实验一下,应该会发现这短短几行代码显著地改善了ListView的滚屏体验。推荐阅读developer.android.com的文章《Painless threading》来学习AsyncTasks的更多细节。
但是,这个基于ListView的例子暴露出一个问题。出于对内存的利用效率考虑,ListView会在用户滚屏的时候对view进行循环再利用。如果用户快速猛烈发飙般地滚屏,一个ImageView对象将会被反复使用多次。每一次它被显示出来,都会触发生成一个下载图片的任务,从而改变这个 ImageView的显示内容。那么问题在哪呢?跟大部分并行程序一样,关键问题在于顺序。在我们这个例子中,没有采取任何措施保证所有下载任务按顺序完成,换句话说,无法保证先启动的任务先完成,后启动的任务后完成。这样就导致显示在list中的图片可能来自之前的任务,该任务因为花费的时间更长,所以最后结束,最终导致预期外的结果。如果你要下载的图片们是一次性绑定到一坨ImageView的,那么就不存在问题,但我们还是从大局出发,为了通用的情况,修正一下吧。

并发处理

要想解决上面提到的问题,我们需要知道并保存下载任务的顺序,以保证最后启动的任务最后结束,并完成对ImageView的更新。要达到这个目的,让每个ImageView记住自己的最后一个下载任务就可以了。我们使用一个专用的Drawable类给ImageView添加这份信息。这个 Drawable类将在下载过程中临时绑定到ImageView。下面是这个DownloadedDrawable类的代码:

static class DownloadedDrawable extends ColorDrawable {
private final WeakReference bitmapDownloaderTaskReference;

public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) {
super(Color.BLACK);
bitmapDownloaderTaskReference =
new WeakReference(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方法将在一个新的下载开始前取消尚在进行中的下载任务。注意,这并不足以保证新开始的下载任务得到的图片一定能够被显示,因为之前的任务可能已经完成了,处于等待onPostExecute方法执行的时间点,而这个onPostExecute方法还是有可能在新任务的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,所以调用它的download方法中可以开始新的下载。唯一的例外情况是如果进行中的下载任务与新任务请求的是同一个URL,我们就不取消旧任务了,让它继续下载。注意在我们这个实现方法中,如果ImageView被回收了,与其关联的下载不会停止(可以借助RecyclerListener实现)。
这个方法还调用了一个helper函数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尚与下载进程关联的情况下绑定位图到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);
}
}

嗯,做了这些修改之后,我们的ImageDownloader类基本可以提供预期的服务了。你可以在自己的项目中灵活运用这些代码或者它演示的异步思想,改善用户体验。

Demo

本文的源代码可以从Google Code获取。你可以在本文提到的三种实现方式(非异步、无并发处理以及最终版本)中切换、比较。注意,缓存大小已经被限制到10张图片以便更好地演示可能出现的问题。

进一步的工作

文中代码为了集中讨论并行问题而做了简化,因此缺少很多功能。首先ImageDownloader类应该利用缓存,特别是与ListView结合使用的时候。因为ListView在用户上下往返滚屏的时候会多次显示相同图片,而缓存可以大大降低开销。通过使用一个基于LinkedHashMap(该 hashmap提供从URL到Bitmap SoftReference的映射)的LRU缓存可以很容易地实现这一点。更加复杂的缓存机制还可以依赖于本地存储。缩略图的创建、图片缩放等功能也可以考虑加进来。
本文代码已经考虑到了下载错误和超时的情况。这些情况下将会返回一个空位图。你也可以显示一张带有提示信息的图片。
本文示例的HTTP请求很简单。根据实际情况的不同(大都依赖于服务器端),可以在HTTP请求中加入各种参数或者cookie等等。
本文使用的AsyncTask类是一个把任务从主线程分离出来很简单方便的途径。你可能会用到Handler类来实现对任务流程更好的控制,比如控制并行的下载线程数,等等。

分享到:
评论

相关推荐

    pb真正的多线程,用createthread创建的多线程.rar

    标题中的“pb真正的多线程,用createthread创建的多线程.rar”指的是PowerBuilder(PB)编程环境中实现的多...学习这个例子,开发者可以深入理解PowerBuilder中的多线程编程,以及如何有效地利用多线程提高程序性能。

    堪称精品的VB多线程控制台源程序代码.rar_vb 多线程_vb6_vb6多线程_vb多线程_多线程

    总之,VB6的多线程编程虽然复杂,但通过学习和实践,开发者可以利用多线程提高程序性能,实现更复杂的并发操作。这个压缩包中的源代码是一个宝贵的教育资源,可以帮助你掌握在VB6环境中实现多线程的关键技术和最佳...

    delphi多线程实例

    总之,这个"delphi多线程实例"是一个很好的学习资源,可以帮助开发者理解多线程的概念,以及如何在实际项目中有效利用多线程提高程序性能。通过分析和实践这个实例,我们可以加深对线程编程的理解,提升Delphi开发...

    C#多线程实时显示系统时间【VS2008源码】

    通过这个项目,开发者可以学习到如何在C#中有效地利用多线程提高程序性能,并理解如何安全地更新UI以避免线程冲突。对于任何希望深入理解和应用多线程技术的C#开发者来说,这是一个很好的起点。

    多线程编程实例

    通过研究这些示例,我们可以学习如何创建、管理和同步线程,以及如何利用多线程提高程序性能。例如,可能会有线程池的实现,线程同步的案例,以及不同线程安全的数据结构的使用等。 综上所述,多线程编程是提升程序...

    两种多线程的使用源码+文档

    通过阅读源码和文档,开发者可以学习到如何有效地利用多线程提高程序性能,避免常见的并发问题,以及如何设计和管理线程安全的代码。 总之,这个压缩包提供的资源是学习和提升多线程编程能力的宝贵材料,无论是初学...

    多线程小实验

    通过对这个“多线程小实验”的源代码学习,你将能够掌握如何创建、管理和控制线程,如何解决并发编程中的问题,以及如何利用多线程提高程序性能。此外,这也能帮助你在实际项目中更好地应用多线程技术,提升软件的...

    一个多线程下载程序

    标题 "一个多线程下载程序" 描述了一个使用VC++编程语言实现的软件,该软件能够通过多线程技术加速...通过分析和理解源码,我们可以了解到实际开发中如何有效地利用多线程来优化性能,以及如何构建可靠的网络应用程序。

    多线程聊天程序多线程聊天程序多线程聊天程序

    多线程意味着在一个程序中同时执行多个任务,可以显著提高CPU资源的利用率。 2. **线程同步与互斥**:在多线程环境中,数据共享可能会导致竞态条件,即多个线程同时访问和修改同一数据,可能引发错误。为此,我们...

    多线程应用程序设计

    多线程技术是现代软件开发中的重要组成部分,尤其是在嵌入式系统中,由于资源有限,合理利用多线程能够显著提升系统的性能和响应速度。本实验旨在通过具体的实践操作,帮助学生深入理解多线程编程的基本原理,并熟悉...

    win32多线程程序设计 pdf

    多线程程序设计是现代操作系统和应用程序设计中的一个重要概念,它允许程序中的不同部分同时运行,以充分利用多核CPU资源,提高软件性能,尤其是在服务器和并发处理方面。在Windows操作系统中,Win32 API提供了创建...

    C#多线程编程.Net Winform

    在.NET框架中,C#语言提供了强大的多线程编程能力,使得...通过深入理解和实践以上知识点,你将能够熟练掌握C#中的多线程编程,并在Winform应用中有效利用多线程提高程序性能。在学习过程中,结合源码详解,效果更佳。

    操作系统 多线程演示程序

    在多线程程序中,线程间的同步是非常关键的,以避免数据竞争和死锁等问题。这就是信号量(Semaphore)的作用所在。信号量是一个同步原语,用于控制对共享资源的访问,它可以看作是资源计数器,当计数值大于0时,线程...

    C++多线程网络聊天程序 .zip

    在单线程程序中,任务是顺序执行的,而在多线程程序中,多个线程可以同时执行不同的任务,从而提高了程序的执行效率。C++11及更高版本开始内置了对多线程的支持,通过`&lt;thread&gt;`库,我们可以方便地创建和管理线程。 ...

    2.2多线程应用程序设计

    在编程领域,多线程应用程序设计是至关重要的一个主题,特别是在现代计算机系统中,多核处理器的普及使得并发执行成为提升程序性能的有效手段。本文将深入探讨多线程应用程序设计的基础概念、优缺点以及实现方法,...

    简单的多线程程序

    总之,“简单的多线程程序”是一个很好的起点,可以帮助初学者理解如何在实际项目中应用多线程技术,提高程序性能。结合MFC和OpenCV,我们可以构建复杂的图像处理系统,利用多线程优化性能,提高用户体验。而"more_...

    Win32多线程程序设计全部代码

    总之,“Win32多线程程序设计全部代码”这个资源将帮助开发者深入了解和实践Win32 API中的多线程编程技术,包括线程的创建、同步、通信、资源管理和性能优化等方面。通过这些代码示例,开发者可以更好地应对多线程...

    Win32多线程程序设计.pdf(带目录)

    通过合理地利用多线程技术,可以显著提高程序的执行效率和响应速度,优化用户界面的交互体验,同时还能实现更复杂的并行算法。 ### Win32 API与多线程 Win32 API提供了丰富的函数集,用于创建和管理线程。其中,`...

    C++多线程聊天程序

    10. **测试与调试**: 对于多线程程序,测试和调试是一项挑战。可以使用各种工具,如GDB、Valgrind等,来检测线程错误,如死锁、数据竞争等问题。 综上所述,“C++多线程聊天程序”涵盖了C++11及更高版本的多线程...

Global site tag (gtag.js) - Google Analytics