通过多线程技术提高Android应用性能
来源:互联网 发布:简述js闭包使用场景 编辑:程序博客网 时间:2024/06/05 21:06
有一个好方法可以让你的应用保持快速响应,那就是让主UI线程尽量少做事情,如果在UI线程中做一个耗时过长的处理,会导致UI僵死,因此对于有可能耗时过长的任务应该另起一个线程处理。这种典型的应用场景就是做网络相关的操作,因为网络传输过程中可能有意料不到的延迟。通常来说,用户可以忍受反馈时的一小段等待,但界面僵死就是另外一回事了。
本文就根据这种设计模式实现一个简单的图片下载应用,我们将实现一个带有图片预览功能的列表,这些图片都是从互联网上下载的,下载操作是在后台异步下载的。
图片下载应用
从网上下载图片非常简单,使用Android framework中提供的HTTP相关类就很容易实现,下面提供了一段样例代码:
staticBitmap downloadBitmap(String url) { finalAndroidHttpClient client = AndroidHttpClient.newInstance("Android"); finalHttpGet getRequest = newHttpGet(url); try{ HttpResponse response = client.execute(getRequest); finalint statusCode = response.getStatusLine().getStatusCode(); if(statusCode != HttpStatus.SC_OK) { Log.w("ImageDownloader","Error " + statusCode + " while retrieving bitmap from " + url); returnnull; } finalHttpEntity entity = response.getEntity(); if(entity != null) { InputStream inputStream = null; try{ inputStream = entity.getContent(); finalBitmap bitmap = BitmapFactory.decodeStream(inputStream); returnbitmap; }finally{ if(inputStream != null) { inputStream.close(); } entity.consumeContent(); } } }catch(Exception e) { // 可以在这里提供更多更详细的关于IOException和IllegalStateException的错误信息 getRequest.abort(); Log.w("ImageDownloader","Error while retrieving bitmap from " + url + e.toString()); }finally{ if(client != null) { client.close(); } } returnnull;}
上面的代码创建了一个发送HTTP请求的客户端,如果HTTP请求正确返回,将会返回一张图片的二进制编码流,通过它可以解码创建一个Bitmap对象。当然,如果想要上面这段代码正常运行,必须保证网络环境正常。(【译者注】:网络环境包括一个可达的web服务器,你的Android应用要有android.permission.INTERNET权限,另外有一点,自从Android3.0以后,Android强制不允许在Android主线程中做网络相关操作,否则会抛出NetworkOnMainThreadException异常,想尝试上面的代码的读者要注意这几点。)
注意:上面这个版本中使用的BitmapFactory.decodeStream 在网络连接速度慢的情况下可能会导致图片编码失败,这个问题可以使用FlushedInputStream(inputStream)替代BitmapFactory.decodeStream方法来解决。下面是一个实现:(【译者注】:这里原作者说BitmapFactory.decodeStream当网速慢的时候可能会失败,这个现象是正确的,原因是因为InputStream中的skip方法有问题,这个问题google一把,有很多相关内容,下面这个实现就是解决这个问题的。)
staticclass FlushedInputStream extendsFilterInputStream { publicFlushedInputStream(InputStream inputStream) { super(inputStream); } @Override publiclong skip(longn) throwsIOException { longtotalBytesSkipped = 0L; while(totalBytesSkipped < n) { longbytesSkipped = in.skip(n - totalBytesSkipped); if(bytesSkipped == 0L) { intbytes = read(); if(bytes < 0) { break; //读到文件结束 }else{ bytesSkipped = 1;// 读一个字节 } } totalBytesSkipped += bytesSkipped; } returntotalBytesSkipped; }}
上面代码中的skip()方法能略过指定的字节数,除非已经读到了文件尽头。如果直接在ListAdapter的getView方法中使用上面的下载图片的代码的话,会导致界面一顿一顿的,用户体验会很差,每张图片的显示都需要等待图片下载完成,使得界面无法顺畅的上下滚动。(【译者注】:如上所注,如果用API level 11以上的(包括11),那会直接抛异常,不会执行。现在的Android强制网络操作必须在UI线程之外做。)
这样看来这的确不是一个好主意,所以AndroidHttpClient不允许在主线程中启动,上面的代码会报”This thread forbids HTTP requests” 的错误,所以如果你真想尝试的话,请使用DefaultHttpClient代替。(【译者注】:事实上,我在Android4.0.3上使用DefaultHttpClient也不行。)
使用异步任务
AsyncTask类提供了一种最为简单的方法从UI启动一个新的任务,我们现在创建一个ImageDownloader类负责创建这些任务,这个类中的download方法可以从给定URL下载图片并且将该图片赋给一个ImageView控件。
publicclass ImageDownloader { publicvoid download(String url, ImageView imageView) { BitmapDownloaderTask task = newBitmapDownloaderTask(imageView); task.execute(url); }}
classBitmapDownloaderTask extendsAsyncTask<String, Void, Bitmap> { privateString url; privatefinal WeakReference<ImageView> imageViewReference; publicBitmapDownloaderTask(ImageView imageView) { imageViewReference = newWeakReference<ImageView>(imageView); } @Override // 这里是在下载线程中真正要执行的代码 protectedBitmap doInBackground(String... params) { // 参数params是从execute方法传递过来的,params[0]就是要下载的图片url returndownloadBitmap(params[0]); } @Override // 一旦图片下载完成,就将它在ImageView控件上显示出来 protectedvoid onPostExecute(Bitmap bitmap) { if(isCancelled()) { bitmap = null; } if(imageViewReference != null) { ImageView imageView = imageViewReference.get(); if(imageView != null) { imageView.setImageBitmap(bitmap); } } }}
实际上真正在后台异步运行的是doInBackground方法,在这个方法中只是简单的调用本文开头时提供的downloadBitmap方法。
当异步任务执行完成以后,也就是doInBackground方法执行完成之后,onPostExecute方法将被调用,Android会自动将doInBackground方法的返回结果作为参数传递给onPostExecute方法,在这段代码中,onPostExecute就是简单的将doInBackground方法下载的图片与ImageView关联起来。值得注意的一点是,在这里的ImageView是通过一个WeakReference关联的,所以在下载过程没有结束前,ImageView所在的Activity有可能被结束,ImageView对象被垃圾回收,所以这里在使用ImageView之前必须要做两次是否为null的验证,一次是WeakReference对象的验证,一次是ImageView对象的验证。
这个简单的例子介绍了AsyncTask的使用,尝试一下,你就会发现只需要添加很少的代码就可以让你的ListView性能有很大的提高,现在它可以平滑的滚动了。这里有篇文章“轻松的多线程”,介绍了更多关于AsyncTask的细节。
然而,由于ListView的一个特性使我们当前实现还是有问题。事实上,为了更高效的使用内存,当用户滚动ListView的时候,它会重复利用显示控件。当手指在屏幕上猛的一划,让ListView滚动幅度很大的时候,一个ImageView对象会被多次利用(【译者注】:这个应用有点像走马灯一样,用有限的几个ImageView显示很多图片,因此可能一个ImageView会用来显示多个图片),每次显示该对象时都会触发下载图片任务,然后更新自己显示的图片。那么问题出在哪里呢?正如大多数多线程应用一样,最关键的问题就是顺序问题。在我们现在这个应用场景中,根本没有保证先开始的下载任务会先结束,导致的结果当前显示的图片有可能是上一次下载任务的图片。这种情况一般在下载时间较长的时候会发生。实际上,如果你下载的图片只显示一次,并且都在ListView中的ImageView对象上显示的话,这就不算什么问题(【译者注】:也不一定,比如看连环画的时候,图片顺序非常重要),不过我们为了适应更普遍的需求还是把这个问题给修好。(【译者注】:实际上,如果你是依次新建AsyncTask同时下载图片,而且都是用execute()方法触发的话,如果你用的是Android1.6~Android3.0之间的版本的话那会出现原作者说的这个问题,否则你会发现他还真的是先开始的任务先结束。而且只有当第一个任务结束后,下一个任务才会开始。因为在Android1.6之前,Android的所有AsyncTask都会在除了UI线程之外的一个线程中运行,Android1.6开始,会为每个AsyncTask新建一个线程,后来可能是为了防止无止境的滥用线程或者一些其他的问题,从Android3.0开始,所有的AsyncTask又只在一个线程中运行,如果想要多个AsyncTask同时运行,需要自己建一个Excutor,然后用AsyncTask的executeOnExecutor方法执行。)
处理并发问题
为了解决这个问题,我们需要记下每个下载任务的顺序,确保最后一个开始下载的图片是最终显示的图片。这样的话就需要每一个ImageView对象都记得它最后一次下载的是哪张图片。我们现在就用一个包装过的Drawable子类来保存这一信息,然后在下载过程中将这个Drawable子类对象与ImageView控件绑定,下面给出DowloadedDrawable类的实现代码:
staticclass DownloadedDrawable extendsColorDrawable { privatefinal WeakReference<BitmapDownloaderTask> bitmapDownloaderTaskReference; publicDownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) { super(Color.BLACK); bitmapDownloaderTaskReference = newWeakReference<BitmapDownloaderTask>( bitmapDownloaderTask); } publicBitmapDownloaderTask getBitmapDownloaderTask() { returnbitmapDownloaderTaskReference.get(); }}
此方法是通过继承ColorDrawable类实现的,效果是在下载过程中,ImageView控件会显示一个黑色背景,当然你也可以使用一个进度条替代单纯的黑色背景从而给用户下载进度的反馈信息。另外,注意要使用WeakRefernece来减弱对象之间的依赖关系。
下面我们改进之前的ImageDownloader类的download方法,首先先创建一个DownloadedDrawable的实例,并且赋给ImageView,代码如下:
publicvoid download(String url, ImageView imageView) { if(cancelPotentialDownload(url, imageView)) { BitmapDownloaderTask task = newBitmapDownloaderTask(imageView); DownloadedDrawable downloadedDrawable = newDownloadedDrawable(task); imageView.setImageDrawable(downloadedDrawable); task.execute(url, cookie); }}
privatestatic 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{ // 相同的URL已经在下载过程中了,因此不取消,因为不管是先下载还是后下载,反正下载的都是同一个文件 returnfalse; } } returntrue;}
cancelPotentialDownload使用AsyncTask类的cancel方法来终止一个还没有结束的下载任务。大多数时候它会返回true,因此在download方法中可以新启动一个下载任务。但是如果相同的URL已经在下载过程中,那我们就没有必要取消该任务再新建一个任务来下载该URL的图片了。还需要注意一点,如果下载任务对应的ImageView对象已经被垃圾回收了,而该任务还没有结束,这种情况该怎么办?这时可以使用RecyclerListener来处理这种情况。
如果要使用RecyclerListener的话,我们先要使用一个辅助函数,getBitMapDownloaderTask,代码如下:
privatestatic BitmapDownloaderTask getBitmapDownloaderTask(ImageView imageView) { if(imageView != null) { Drawable drawable = imageView.getDrawable(); if(drawable instanceofDownloadedDrawable) { DownloadedDrawable downloadedDrawable = (DownloadedDrawable)drawable; returndownloadedDrawable.getBitmapDownloaderTask(); } } returnnull;}
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上下载,点击这里。你可以比较一下本文中描述的三种实现(不使用异步任务的,不将下载任务和ImageView绑定的,还有最终的版本)。请注意为了更好的演示效果,我们只缓存10张图片。(【译者注】:这里原作者说的缓存在前面的文章中没有提到,在他的代码中每次下载图片前都先尝试从缓存中获取该URL对应的图片,如果这张图片已经被下载过了就不再新建下载任务而直接使用缓存中的图片,其实用适当的缓存来减少消耗大的操作也是个相当好的经验)。
下一步工作
本文的代码只注重了并行方面的需求,为此简化了不少,从而很多有用的功能都没有包含在内。ImageDownloader类首先应该使用缓存,特别当它与ListView配合使用的时候,因为这种情况下屏幕上下来回滚动,一张图片可能会被显示多次。这时可以使用LinkedHashMap来实现一个缓存,用来存储最近使用过的图片,缓存SoftRefernces中以URL为Key,以BitMap为value。缓存的大小可以根据本地存储的大小来决定(【译者注】:不一定都要存在内存中,从本地sdcard中读取图片速度也会被从网上下载快得多,因此缓存在sdcard上也是个不错的选择)。如果需要的话,还可以添加图片预览和图片缩放的功能。
在我们的实现中,当下载错误或者超时的时候,会返回null,在这里可以考虑使用一张提示错误的图片替代null。
我们发送HTTP请求的代码非常简单,我们可以考虑为了某些特定的网站添加一些必须的参数或者cookie。(【译者注】:比如说有些网站禁止外来访问,可以添加HTTP协议中的referer或者某些网站需要验证才能下载的等等)。
本文使用的AsyncTask类是可以非常简单方便的从UI线程中触发异步任务。你也可以考虑使用Handler类来对你想做的事情做更好的控制,比如说控制下载线程数的总量等等。( 【译者注】:现在的AsyncTask配合Executor使用完全可以做到控制总线程数的功能,并且如果在非UI线程中更新UI会导致“android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views”这样的异常,因此个人建议尽量使用AsyncTask比较好。)
我们发送HTTP请求的代码非常简单,我们可以考虑为了某些特定的网站添加一些必须的参数或者cookie。(【译者注】:比如说有些网站禁止外来访问,可以添加HTTP协议中的referer或者某些网站需要验证才能下载的等等)。
本文使用的AsyncTask类是可以非常简单方便的从UI线程中触发异步任务。你也可以考虑使用Handler类来对你想做的事情做更好的控制,比如说控制下载线程数的总量等等。( 【译者注】:现在的AsyncTask配合Executor使用完全可以做到控制总线程数的功能,并且如果在非UI线程中更新UI会导致“android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views”这样的异常,因此个人建议尽量使用AsyncTask比较好。)
英文原文:Gilles Debunne,编译:ImportNew - 赵荣
译文地址: http://www.importnew.com/895.html
【如需转载,请在正文中标注并保留原文链接、译文链接和译者等信息,谢谢合作!】
点击打开链接
0 0
- 通过多线程技术提高Android应用性能
- 【译】通过多线程技术提高Android应用性能
- 应用:性能提高技术
- Android利用多线程提高程序性能
- android应用开发之性能提高
- 用缓冲技术提高JSP应用的性能和稳定性
- 用缓冲技术提高JSP应用的性能和稳定性
- 用缓冲技术提高JSP应用的性能和稳定性
- 用缓冲技术提高JSP应用的性能和稳定性
- 用缓冲技术提高JSP应用的性能和稳定性
- 用缓冲技术提高JSP应用的性能和稳定性
- 用缓冲技术提高JSP应用的性能和稳定性
- 用缓冲技术提高JSP应用的性能和稳定性
- 性能提高技术
- 通过硬件层提高Android动画的性能
- 通过硬件层提高Android动画的性能
- 利用多线程提高程序性能(for Android)
- 利用多线程提高程序性能(for Android)
- C#控件:DataGridView合并单元格
- IOS自学第一天
- java实现冒泡排序算法
- 改善C#编程的50个建议(21-25)
- Mac OS X中正确添加环境变量的方法
- 通过多线程技术提高Android应用性能
- jquery源码学习
- 双向循环链表
- 网站需要做好二次营销的几个方案才能将流量最大化
- C++基础
- C++ 指针的偏移 The offset of a pointer in C++
- SSH知识总结
- linux内核可以接受的参数 | Linux kernel启动参数 | 通过grub给内核传递参数
- 二维最大熵阈值分割原理与opencv实现