首页 » Android程序设计:第2版 » Android程序设计:第2版全文在线阅读

《Android程序设计:第2版》Android中的并发编程

关灯直达底部

正如第2章所介绍的,编写正确的并发程序非常困难。Android库提供了一些方便的工具,使得并发编程更简单和安全。

当讨论并发编程时,开发人员往往认为是编写多线程代码以使得这些线程在同一时间执行——虽然线程确实使得程序运行更快。当然,实际上并没有那么简单。除非有多个处理器执行这些线程,否则需要执行多个不相关的、计算频繁的任务的程序,如果这些任务是在一个线程上执行,其执行速度并不会比以独立的线程方式执行速度快。实际上,在一个处理器上,并发版本可能由于上下文切换开销,导致实际运行速度更慢。

多线程的Java应用已经存在很长一段时间了,但是在之前,大多数人还买不起多处理器的机器来运行这些应用。在Android领域中,多线程是一个基础工具,虽然大多数设备在一年左右的时间内可能还只有一个CPU。如果不能使程序运行更快,并发编程有什么意义呢?

如果你编程已经有了一段时间,你甚至根本不会去思考代码是否必须严格按顺序执行。任何给定语句的执行必须在下一条语句执行之前完成。线程也不会放宽这个限制。即使代码所执行的任务和代码的顺序没有关系,开发人员也会对代码进行抽象,编写有序、富有逻辑且易于阅读的代码。

当线程完全独立时,并发执行独立的线程并不会带来任何内在的复杂性(如一个线程在你的计算机上运行,另一个线程在我的计算机上运行)。然而,当两个并发线程需要协作时,它们需要交汇点。举个例子,通过网络获取的数据可能需要在屏幕上显示,或者用户输入的数据需要保存到数据库。安排这些操作执行的交汇点,尤其对于有代码优化器、流水线处理器和多级缓存这些机制的情况下,会变得异常复杂。其带来的后果是一个程序在单处理器上运行看起来没有什么问题,而当在多处理器的环境下运行时就莫名其妙地失败,而且难以调试,让人感到非常痛苦。

交汇过程(rendezvous process)使得一个线程可以看到另一个线程的数据或状态,通常称为发布(publishing)引用。当一个线程的状态对于另一个线程可见时,就称为发布该状态的引用。正如在第2章所介绍的,唯一一种可以安全发布引用的方式是所有线程在使用时,对相同对象的数据执行同步。其他方式都是不正确和不安全的。

AsyncTask和UI线程

如果你曾经使用过任何现代GUI框架,那么Android UI看起来就会很熟悉。它是事件驱动的,建立在可嵌套组件库上,而且最重要的是,它是单线程的。设计者很多年前就发现由于GUI必须响应多个源的异步事件,如果UI是多线程的,则几乎无法避免死锁。相反,单线程同时控制输入设备(触摸屏、键盘等)和输出设备(显示器等),通常以收到请求的顺序依次执行每个设备的请求。

虽然UI是在单线程上运行,但几乎任何较复杂的Android应用都是多线程的。例如,UI必须响应用户,激活显示器,无论从网络检索数据的代码是否正在处理输入的数据。UI必须响应快速,而且不应该和运行时间很长的进程一起按序执行。运行时间长的程序必须异步运行。

在Android系统中,实现异步任务的一个很方便的工具是AsyncTask。它完全隐藏了运行任务的线程的很多详细信息。

考虑一个非常简单的应用,它初始化游戏引擎,当加载内容时,显示一些插播广告图形。图3-7就是这种应用的一个非常基础的例子。当按下按钮时,它会初始化游戏级别,然后在文本框中显示欢迎消息。

图3-7:简单的游戏应用的初始化

以下是程序的示例代码模板。缺少的是真正初始化游戏和更新文本的代码:


/** AsyncTaskDemo */public class AsyncTaskDemo extends Activity {      int mInFlight;      /** @see android.app.Activity#onCreate(android.os.Bundle) */      @Override      public void onCreate(Bundle state) {            super.onCreate(state);            setContentView(R.layout.asyncdemo);            final View root = findViewById(R.id.root);            final Drawable bg = root.getBackground;            final TextView msg = ((TextView) findViewById(R.id.msg));            final Game game = Game.newGame;            ((Button) findViewById(R.id.start)).setOnClickListener(                  new View.OnClickListener {                      @Override public void onClick(View v) {                        // !!! initialize the game here!                  } });}  

现在,假设我们希望在用户等待游戏启动时,显示一个动画背景(如图3-7所示的缓慢显示的点)。以下是必需代码的框架:


/** * Synchronous request to remote service * DO NOT USE!! */void initGame(      View root,      Drawable bg,      Game game,      TextView resp,      String level){      // if the animation hasn't been started yet,      // do so now      if (0 >= mInFlight++ ) {            root.setBackgroundResource(R.anim.dots);            ((AnimationDrawable) root.getBackground).start;      }      // initialize the game and get the welcome message      String msg = game.initialize(level);      // if this is the last running initialization      // remove and clean up the animation      if (0 >= --mInFlight) {            ((AnimationDrawable) root.getBackground).stop;            root.setBackgroundDrawable(bg);      }      resp.setText(msg);}  

以上代码相当简单明了。用户可能想要单击启动按钮,因此可能会执行多个初始化。如果插播广告背景尚未显示,则显示该背景并准备启动游戏。然后,调用游戏引擎初始化程序。一旦游戏完成启动,就执行清理工作。如果这是要启动的最后一个游戏,则清除插播广告动画。最后,在文本框中显示欢迎消息。

虽然以上代码很接近于使指定的示例应用工作所需要的,但它在某种条件下它会失败。它使得UI线程在整个过程中都无法调用game.initialize方法。这会带来很多不愉快的后果。

其中最明显的是背景动画将无法正常工作。虽然设置和运行动画的逻辑几乎是正确的,但是代码非常清楚地表明除了完成远程服务调用,不能执行任何其他操作。

糟糕的是,Android框架实际上会监测应用的UI线程,避免程序挂起或恶意程序导致设备不可用。如果应用响应输入的时间过长,Android框架会暂停该应用,向用户发出警告信息,使用户可以强制关掉该应用。如果构建和运行该示例应用,initGame的实现如上例所示(试试运行它,它实际上很有启发性),则第一次单击Send Request按钮后,该用户界面就会冻结。如果你多单击几次,就会看到如图3-8所示的警告信息。

图3-8:没有响应的应用

使用AsyncTask就可以解决这个问题!Android提供了一个相对安全、强大且易于使用的方式,其可以正确地在后台执行任务。以下是通过AsyncTask重新实现的initGame类:


private static final class AsyncInitGame      extends AsyncTask<String, Void, String>{      private final View root;      private final Game game;      private final TextView message;      private final Drawable bg;      public AsyncInitGame(            View root,            Drawable bg,            Game game,            TextView msg)      {            this.root = root;            this.bg = bg;            this.game = game;            this.message = msg;      }      // runs on the UI thread      @Override protected void onPreExecute {            if (0 >= mInFlight++) {                root.setBackgroundResource(R.anim.dots);                ((AnimationDrawable) root.getBackground).start;            }      }      // runs on the UI thread      @Override protected void onPostExecute(String msg) {            if (0 >= --mInFlight) {                ((AnimationDrawable) root.getBackground).stop;                root.setBackgroundDrawable(bg);            }            message.setText(msg);      }      // runs on a background thread      @Override protected String doInBackground(String... args) {            return ((1 != args.length) || (null == args[0]))                ? null                : game.initialize(args[0]);      }}  

这段代码和第一个例子几乎完全相同。AsyncInitGame类包含的3个方法和initGame类执行的代码也几乎相同,执行顺序也相同。

AsyncTask是在UI线程上创建的。当UI线程调用任务的execute方法时,该方法会首先调用onPreExecute方法。这使得该任务能够对其本身和环境执行初始化——在这个例子中,即安装背景动画。下一步,AsyncTask创建新的背景线程,并发执行doInBackground方法。当最终doInBackground方法完成时,就删除背景线程,再在UI线程中调用onPostExecute方法。

假设该AsyncTask的实现是正确的,单击监听器只需要创建一个实例并调用它,如下所示:


((Button) findViewById(R.id.start)).setOnClickListener(      new View.OnClickListener {            @Override public void onClick(View v) {                  new AsyncInitGame(                      root,                      bg,                      game,                      msg)                  .execute("basic");            } });  

实际上,AsyncInitGame类的实现是完整、准确和可靠的。我们一起来看看更多的细节。

首先,要注意基类AsyncTask是抽象类。使用该类的唯一方式是创建一个子类,用于专门执行某些特定的任务(是is-a关系,而不是has-a关系)。通常情况下,子类是简单、匿名类,并且只定义几个方法。和第2章所提到的良好的编码作风及代码分离类似,建议子类要小,并委托给负责UI和异步任务的类去实现方法。例如,在这个例子中,doInBackground只不过是Game类的代理(proxy)。

一般来说,AsyncTask需要一组参数并返回一个结果。因为需要在线程之间传递该参数并返回结果,所以需要一些握手机制以确保线程安全性。通过参数传递调用execute方法来调用AsyncTask。当线程在后台执行时,这些参数最终是通过AsyncTask机制传递给doInBackground方法的,doInBackground返回结果。AsyncTask把该结果作为参数传递给doPostExecute方法,doPostExecute方法和最初的execute方法在同一个线程中运行。图3-9显示说明了其中的数据流。

图3-9:AsyncTask中的数据流

AsyncTask不但会确保数据流的安全,也会确保类型安全。AsyncTask是典型的类型安全模板模式。该抽象基类(AsyncTask)使用Java泛型,使得实现能够指定任务参数和结果的类型。

当定义具体的AsyncTask子类时,可以为Params、Progress和Result提供确切的类型,这些类型变量在AsyncTask中定义。第一个类型变量(Params)和最后一个类型变量(Result)分别是任务参数类型和结果类型。我们将很快介绍中间类型变量。

Params所绑定的具体类型是execute参数类型,因此也是doInBackground的参数类型。同样,绑定到Result的具体类型是doInBackground的返回值类型,即onPostExecute方法的参数类型。

这有点难以理解。在第一个例子中的AsyncInitGame类对于这部分的理解没有什么帮助,因为其输入参数和输出参数都是相同的类型String。接下来给出几个例子,其参数和返回类型不同。它们能够更好地说明如何使用泛型类型变量:


public class AsyncDBReq      extends AsyncTask<PreparedStatement, Void, ResultSet>{      @Override      protected ResultSet doInBackground(PreparedStatement... q) {            // implementation...      }      @Override      protected void onPostExecute(ResultSet result) {            // implementation...      }}public class AsyncHttpReq      extends AsyncTask<HttpRequest, Void, HttpResponse>{      @Override      protected HttpResponse doInBackground(HttpRequest... req) {            // implementation...      }      @Override      protected void onPostExecute(HttpResponse result) {            // implementation...      }}  

在第一个例子中,AsyncDBReq实例的execute方法参数是一个或多个PreparedStatement变量。AsyncDBReq实例的doInBackground方法会把这些PreparedStatement参数作为其参数,返回结果是ResultSet。onPostExecute方法会把该ResultSet作为其参数使用。

类似地,在第二个例子中,AsyncHttpReq实例的execute方法调用会接收一个或多个HttpRequest变量。doInBackground方法把这些HttpRequest作为其参数,返回结果是HttpResponse。onPostExecute处理该返回的HttpResponse。

警告:AsyncTask的一个实例只能运行一次。第二次执行execute方法会抛出IllegalStateException异常。每个任务调用都需要一个新的实例。

虽然AsyncTask简化了并行处理,但它有很强的限制约束条件且无法自动验证这些条件。注意不要违反这些约束条件是非常必要的。如果违反这些约束条件,会触发本节最开始所描述的bug:间歇性故障,并且很难查找。

对于这些约束,最明显的是doInBackground方法,因为它是在另一个线程上执行的,只能引用作用域内的变量,这样才是线程安全的。例如,下面这段代码中就有一个很容易犯的错误:


// ... some classint mCount;public void initButton1( Button button) {      mCount = 0;      button.setOnClickListener(            new View.OnClickListener {                @SuppressWarnings("unchecked")                @Override public void onClick(View v) {                     new AsyncTask<Void, Void, Void> {                         @Override                         protected Void doInBackground(Void... args) {                          mCount++; // !!! NOT THREAD SAFE!                          return null;                         }                     }.execute;                  } });}  

虽然关于这个问题不会有任何提醒,没有编译错误,没有运行警告,可能甚至在bug被触发时也不会立即失败,但该代码绝对是错误的。有两个不同的线程访问变量mCount,而这两个线程之间却没有执行同步。

鉴于这种情况,当你看到在AsyncTaskDemo中没有对mInFlight的访问执行同步时,可能会感到很奇怪。实际上它是正确的。AsyncTask约束会确保onPreExecute方法和onPostExecute方法在同一个线程中执行,即execute方法被调用的线程。和mCount不同,mInFlight只有一个线程访问,不需要执行同步。

可能会导致最致命的并发问题是在用完某个参数变量后,没有释放其引用。例如以下代码是不正确的,你能看出为什么吗?


public void initButton(      Button button,      final Map<String, String> vals){      button.setOnClickListener(            new View.OnClickListener {                @Override public void onClick(View v) {                     new AsyncTask<Map<String, String>, Void, Void> {                         @Override                         protected Void doInBackground(                           Map<String, String>... params)                         {                           // implementation, uses the params Map                         }                     }.execute(vals);                     vals.clear; // !!! THIS IS NOT THREAD SAFE !!!                  } });}  

这个问题非常微妙且难以发现。如果你注意到initButton的参数vals被并发引用,却没有执行同步,你就对了!当调用AsyncTask时,它作为参数传递给execute方法。syncTask框架可以确保当调用doInBackground方法时,该引用会正确地传递给后台线程。但是,对于在initButton方法中所保存并使用的vals引用,它却没有办法处理。调用vals.clear修改了在另一个线程上正在使用的状态,但没有执行同步。因此,不是线程安全的。

这个问题的最佳解决办法是确保AsyncTask的参数是不可改变的。如果这些参数不会发生变化,类似String、Integer或只包含final变量的POJO对象,那么它们都是线程安全的,不需要更多的操作。要保证传递给AsyncTask的可变对象是线程安全的唯一办法是确保只有AsyncTask持有引用。在前面给出的例子中(如图3-7所示),参数vals是传递给initButton方法,我们完全无法保证它不存在悬空的引用(dangling references)。即使删掉代码vals.clear,也无法保证该代码正确,因为调用initButton方法的实例可能会保存其参数map的引用,它最终传递的是参数vals。使该代码正确的唯一方式是完全复制(深复制)map及其包含的对象!

熟悉Java集合包的开发人员可能会指出也可以不完全深复制map参数,而是把它封装成不可修改的unmodifiableMap,如下所示:


public void initButton(      Button button,      final Map<String, String> vals){      button.setOnClickListener(            new View.OnClickListener {                @Override public void onClick(View v) {                     new AsyncTask<Map<String, String>, Void, Void> {                         @Override                         protected Void doInBackground(                             Map<String, String>... params)                         {                           // implementation, uses the params Map                         }                     }.execute(Collections.unmodifiableMap(vals));                     vals.clear; // !!! STILL NOT THREAD SAFE !!!                  } });}  

遗憾的是,这种方式还是错误的。Collections.unmodifiableMap提供了对其所封装的map的一个不可修改的视图。但是,它并没有阻止进程对原始、可变对象的引用的访问和修改。在前面这个例子中,虽然AsyncTask不能改变传递给execute方法的map值,但当后台线程使用onClickListener方法时,还是会通过vals参数修改map引用,而没有执行同步!真是糟糕!

对于AsyncTask,还有一方面比较差强人意。由于生成任务的活动有生命周期。如果有个任务持有创建该任务的活动的指针,用户在其运行时接了个电话,该任务可能会发现其持有的活动指针已经被销毁。匿名子类可以很容易做到这一点:持有创建它的类的一个隐式指针。AsyncTask最适合运行历时非常短的任务,比如几秒钟。它不适合于历时数分钟或者更长的进程。

最后,值得一提的是,AsyncTask还有一个方法没有使用到:onProgressUpdate。它的作用是使长时间运行的任务可以周期性安全地把状态返回给UI线程。以下例子说明了如何使用onProgressUpdate方法实现进度条,向用户显示游戏初始化进程还需要多长时间:


public class AsyncTaskDemoWithProgress extends Activity {      private final class AsyncInit            extends AsyncTask<String, Integer, String>            implements Game.InitProgressListener      {            private final View root;            private final Game game;            private final TextView message;            private final Drawable bg;            public AsyncInit(                View root,                Drawable bg,                Game game,                TextView msg)            {                this.root = root;                this.bg = bg;                this.game = game;                this.message = msg;            }            // runs on the UI thread            @Override protected void onPreExecute {                if (0 >= mInFlight++) {                     root.setBackgroundResource(R.anim.dots);                     ((AnimationDrawable) root.getBackground).start;                }            }            // runs on the UI thread            @Override protected void onPostExecute(String msg) {                if (0 >= --mInFlight) {                     ((AnimationDrawable) root.getBackground).stop;                     root.setBackgroundDrawable(bg);                }                message.setText(msg);            }            // runs on its own thread            @Override protected String doInBackground(String... args) {                return ((1 != args.length) || (null == args[0]))                     ? null                     : game.initialize(args[0], this);            }            // runs on the UI thread            @Override protected void onProgressUpdate(Integer... vals) {                updateProgressBar(vals[0].intValue);            }            // runs on the UI thread            @Override public void onInitProgress(int pctComplete) {                publishProgress(Integer.valueOf(pctComplete));            }      }      int mInFlight;      int mComplete;      /** @see android.app.Activity#onCreate(android.os.Bundle) */      @Override      public void onCreate(Bundle state) {            super.onCreate(state);            setContentView(R.layout.asyncdemoprogress);            final View root = findViewById(R.id.root);            final Drawable bg = root.getBackground;            final TextView msg = ((TextView) findViewById(R.id.msg));            final Game game = Game.newGame;            ((Button) findViewById(R.id.start)).setOnClickListener(                new View.OnClickListener {                  @Override public void onClick(View v) {                      mComplete = 0;                      new AsyncInit(                          root,                          bg,                          game,                          msg)                     .execute("basic");                  } });}void updateProgressBar(int progress) {      int p = progress;      if (mComplete < p) {            mComplete = p;            ((ProgressBar) findViewById(R.id.progress))            .setprogress(p);            }      }} 

在这个例子中,假定该游戏初始化程序接收Game.Init ProgressListener作为其参数。初始化进程周期性调用监听器的onInitProgress方法,通知已经完成了多少工作。然后,在调用树的doInBackground会调用onInitProgress方法,在后台线程执行该方法。如果onInitProgress要直接调用AsyncTaskDemoWith Progress.updateProgressBar,则在后台线程也会执行后续的bar.setStatus调用,这破坏了只有UI线程可以修改视图对象这一规则。它会带来如下的异常:


11-30 02:42:37.471: ERROR/AndroidRuntime(162): android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.  

为了正确地给UI线程发布进程状态,onInitProgress调用的是AsyncTask的publishProgress方法。AsyncTask处理UI线程的publishProgress调度细节,从而onProgressUpdate可以安全地使用View方法。

我们暂且不再这样详细探讨AsyncTask,先总结一下其关键点:

·Android UI是单线程的。为了熟练使用Android UI,开发人员必须对任务队列概念很熟悉。

·为了保证UI的及时响应,需要运行的任务的执行时间超过几毫秒,或者需要好几百条指令,都不应该在UI线程中执行。

·并发编程很棘手,很容易犯错误,并且很难查找这些错误。

·AsyncTask是运行简单、异步任务的很便捷的工具。要记住的是doInBackground运行在另一个线程上。它不能写任何其他线程可见的状态,也不能读任何其他线程可写的状态。这也包括其参数。

·不可改变的对象是在并发线程之间传递信息的重要工具。

Android进程中的线程

AsyncTask和ContentProvider相结合功能会很强大,可以适应各种常见的应用架构。几乎在所有的MVC模式中,视图(View)对模型(Model)的轮询,都可以通过这种方式实现。在需要模型把变化推送给视图或者模型是长期持续运行的架构的应用中,采用AsyncTask可能是不够的。

回顾一下P70“同步和线程安全”一节中所提到的线程间数据共享的基础规则。一般而言,该规则相当烦琐。但是,上一节对AsyncTask的探讨表明了简化Android中并发任务的正确协调的一个基本规则:把一个线程的状态发布给另一个线程的操作完全由模板类的实现隐藏起来。同时,也重点强调了并发编程的一些陷阱,不够细心的编码者会很容易掉入这些陷阱。还有一些安全的方式可以简化并发问题的一些具体的类。其中一个(在Java编程中常见的一个规则)是和Android框架结合起来。它有时又被称为线程约束(thread confinement)。

假设线程DBMinder创建一个对象,并且在一段时间后修改该对象。当DBMinder完成其工作后,需要把该对象传递给另一个线程DBViewer作进一步处理。为了实现这一目的,可以使用线程约束的方式,DBMinder和DBViewer之间必须存在共享区(drop point)和关联锁。其执行过程如下:

1.DBMinder获得锁,在共享区保存对对象的引用。

2.DBMinder删除其所有该对象的引用!

3.DBMinder释放该锁。

4.DBViewer获得锁,注意到在共享区存在一个对象引用。

5.DBViewer从共享区恢复该引用,然后清空共享区。

6.DBViewer释放该锁。

这个过程适用于任何对象,无论该对象本身是否是线程安全的。因为在多个线程之间共享的唯一状态是这个共享区。两个线程在访问之前都正确地获取了锁。当DBMinder处理完一个对象时,它把该对象传递给DBViewer,不再保存引用。因此所传递的对象的状态永远不会在多个线程之间共享。

“线程约束”是非常强大的手段。在实现“线程约束”时,通常通过有序的任务队列实现共享区。可能会有多个线程竞争该锁,但是每个线程只保留把任务插入队列的时间。一个或多个工作线程从队列中删除任务用于执行。这种模式有时称为“生产者/消费者模型”。只要一个工作单元可以完全在其所在的工作线程上下文中处理,就不需要进一步执行同步。如果你进一步查看AsyncTask的实现,会发现其工作模式就是如此。

线程约束非常有用,因而Android把它包含进了框架中,放在Looper类中。当执行Looper初始化时,Java线程会把它转换成任务队列。它的整个生命周期就是从本地队列中删除对象,执行它们。其他线程执行队列插入工作,正如之前所描述的,交给初始化线程执行。只要执行插入队列的线程删除其入队列的对象的所有引用,那么两个线程就都可以继续执行代码,不需要进一步考虑并发问题。这除了极大简化了如何正确编程,还消除了由于广泛的同步可能带来的效率低下的问题。

由对该任务队列的描述是否使你想起了在本章前面所提到的一种结构呢?Android的单线程、事件驱动的UI实质上就是一个Looper。当启动Context时,系统执行一些记录工作,然后把初始化线程作为Looper启动执行。该线程会成为服务的主线程,即activity的UI线程。在activity中,UI框架保存该线程的引用,其任务队列变成UI事件队列。所有的外部驱动器、屏幕、键盘及呼叫处理器等操作都插入到该队列。Looper的另一部分工作是作为Handler。在Looper线程上创建的Handler,提供到Looper队列的入口。当Looper线程允许其他入队线程访问其任务队列时,它就创建一个新的Handler,把它传递给其他线程。Handler提供了一些快捷的使用方式使得使用其变得更加简单:View.post(Runnable)、View.postDelayed(Runnable,long)和Activity.runOnUiThread(Runnable)。

在Android工具包中,还有一个便捷强大的规范用于进程间通信和工作区共享:ContentProvider。第12章将详细讨论它。在构建自己的架构时,首先考虑ContentProvider能否满足本节所探讨的底层组件的需求。ContentProvider是灵活、可扩展的,能够高速执行并发处理,它可以满足绝大部分应用的需求,除了那些对时间高度敏感的应用。