在日常事务中,我们常常依次做如下操作: 排队取得一个单号; 根据这个单号享有一个操作; 为当前的这个操作买单.
在Android中也一样,为了不阻塞主线程,我们把所有耗时行为都封装为多个线程,有的时候需要先运行线程A,根据得到的结果再运行线程B, 再根据B的结果运行线程C. 时序图如下:
如果我们在ThreadA结束前的代码中插入ThreadB.start, 再ThreadB的结束前的代码中插入ThreadC.start. 那这样的代码很难读,从整体上看也像棉花糖一样无比壮大....
其实在android中有比较优雅的简单的方式处理这个。如下以两个Thread为例子展开演进:
- ConditionVariable方式
conditionVariableForConnection = new ConditionVariable(); app.method01(param1, param2, new callback01() { @Override public void method4Callback01(Object result01) { conditionVariableForRequestSession.open(); ... } };
conditionVariableForRequestSession.block(SESSION_WAIT_TIME_MS); if(getSession()) { app.method02(param01,param02,callback02(){}); } ...
- ExecutorService方式
ExecutorService executorService = Executors.newSingleThreadExecutor(); Callable<String> callback01 = new Callable<String>() { @Override public String call() throws Exception { return HttpHelper.getURLRequestHeadInfo("url01"); } }; Future<String> future01 = executorService.submit(callback01); Callable<String> callback02 = new Callable<String>() { @Override public String call() throws Exception { return HttpHelper.getURLRequestHeadInfo("url02"); } }; Future<String> future02 = executorService.submit(callback02); if(HttpHelper.isSessionAvaiable(future01.get()) && HttpHelper.isSessionAvaiable(future02.get()) ) { ... }
总结:
上述两种方式都是用同步的方式进行着异步的处理,代码的直观性和可移植性成倍增加
ConditionVariable方式主要用来协同多个开发员之间的接口或与老系统的交接。随着交互接口的增多需要多次使用ConditionVariable,但是他的可读性比我们棉花糖式的代码有了直接改观;
ExecutorService方式在上下文数据交互(根据前一线程结果来运行新的线程)方面很优雅,整体效率也更高点
要想搞出一个反应迅速的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类来实现对任务流程更好的控制,比如控制并行的下载线程数,等等。
init方法
在init方法中实例化必要的对象(遵从LazyLoad思想)
init方法中初始化ViewController本身
loadView方法
当view需要被展示而它却是nil时,viewController会调用该方法。不要直接调用该方法。
如果手工维护views,必须重载重写该方法
如果使用IB维护views,必须不能重载重写该方法
loadView和IB构建view
viewDidLoad方法
重载重写该方法以进一步定制view
在iPhone OS 3.0及之后的版本中,还应该重载重写viewDidUnload来释放对view的任何索引
viewDidLoad后调用数据Model
viewDidUnload方法
当系统内存吃紧的时候会调用该方法(注:viewController没有被dealloc)
内存吃紧时,在iPhone OS 3.0之前didReceiveMemoryWarning是释放无用内存的唯一方式,但是OS 3.0及以后viewDidUnload方法是更好的方式
在该方法中将所有IBOutlet(无论是property还是实例变量)置为nil(系统release view时已经将其release掉了)
在该方法中释放其他与view有关的对象、其他在运行时创建(但非系统必须)的对象、在viewDidLoad中被创建的对象、缓存数据等 release对象后,将对象置为nil(IBOutlet只需要将其置为nil,系统release view时已经将其release掉了)
一般认为viewDidUnload是viewDidLoad的镜像,因为当view被重新请求时,viewDidLoad还会重新被执行
viewDidUnload中被release的对象必须是很容易被重新创建的对象(比如在viewDidLoad或其他方法中创建的对象),不要release用户数据或其他很难被重新创建的对象
viewDidLoad总是在loadView之后调用,不管你是不是通过nib文件创建的,这个方法总是会被调用的。
viewDidUnload在收到内存警告的时候调用,在我的理解,这个方法里面应该做几件事情:
1、释放掉一些比较容易创建的对象,或者是一些比较占资源的对象(图片、音频等)
2、如果界面控件自己保持了引用计数,这里也要释放掉。(比如说,这个控件被设成了属性,而且是retain的,这个retain的引用计数就必须释放掉)
3、如果跨类的参数传递机制会在viewDidUnload以后产生不正常的效果,这里也必须处理。
dealloc方法
viewDidUnload和dealloc方法没有关联,dealloc还是继续做它该做的事情
流程:
(loadView/nib文件)来加载view到内存 ——>viewDidLoad函数进一步初始化这些view ——>内存不足时,调用viewDidUnload函数释放views —->当需要使用view时有回到第一步,如此循环。
在iphone里你看到的,摸到的,都是UIView,所以UIView在iphone开发里具有非常重要的作用。 视图和窗口展示了应用的用户界面,同时负责界面的交互。UIKit和其他系统框架提供了很多视图,你可以就地使用而几乎不需要修改。当你需要展示的内容与标准视图允许的有很大的差别时,你也可以定义自己的视图。
不管你是使用系统的视图还是创建自己的视图,你需要理解UIView和UIWindow类所提供的基本结构。这些类提供了复杂的方法来管理视图的布局和展示。理解这些方法的工作非常重要,使你在应用发生改变时可以确认视图有合适的行为。
视图架构
大部分你想要可视化操作都是由视图对象-即UIView类的实例-来进行的。一个视图对象定义了一个屏幕上的一个矩形区域,同时处理该区域的绘制和触屏事件。一个视图也可以作为其他视图的父视图,同时决定着这些子视图的位置和大小。UIView类做了大量的工作去管理这些内部视图的关系,但是需要的时候你也可以定制默认的行为。
视图与层联合起来处理着视图内容的解释和动画过渡。每个UIKit框架里的视图都被一个层对象支持(通常是一个CALayer类的实例),它管理管理着后台的视图存储和处理视图相关的动画。然而,当你需要对视图的解释和动画行为有更多的控制权时,你可以使用层。
为了理解视图和层之间的关系,我们可以借助于一些例子。应用中的视图包括了一个window(同时也是一个视图),一个通用的表现得像一个容器视图的UIView对象,一个图像视图,一个控制显示用的工具条,和一个工具条按钮(它本身不是一个视图但是在内部管理着一个视图)。(注意这个应用包含了一个额外的图像视图,它是用来实现动画的)。为了简化,同时因为这个视图通常是被隐藏的,所以没把它包含在下面的图中。每个视图都有一个相应的层对象,它可以通过视图礶r属性被访问。(因为工具条按钮不是一个视图,你不能直接访问它的层对象。)
图片:视图和层之间的关系.jpg