讀古今文學網 > Android程序設計:第2版 > Android中的並發編程 >

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 class
int 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是靈活、可擴展的,能夠高速執行並發處理,它可以滿足絕大部分應用的需求,除了那些對時間高度敏感的應用。