讀古今文學網 > Android程序設計:第2版 > 擴展Android >

擴展Android

現在,你有了Android框架的基本路線圖,很顯然會問:「我該如何使用該框架來構建自己的應用呢?」如何對這複雜的框架進行擴展,把它變成優秀的應用呢?

正如你所想到的,該問題有不同的答案。Android庫在組織上支持在不同層次上訪問框架。本節所描述的概念已經深入Android的內幕。瞭解這些概念將有助於提升如何輕鬆通過Android API進行交互的「本能」。

Android應用模板

20年前,應用多是通過命令行運行,其大部分代碼是唯一的程序邏輯。但是,現在應用需要支持非常複雜的交互式用戶界面、網絡管理和調用處理等。支持邏輯對於所有的應用都是相同的。隨著應用環境變得越來越複雜,Android框架處理這些邏輯的方式已經得到相當普遍的應用:骨架應用(skeleton application),或稱應用模板(application template)。

當創建簡單的演示程序來驗證Android SDK的安裝是否成功時,即第1章介紹的,就已經創建了一個完整的可運行應用。它可以發送網絡請求,顯示並處理屏幕輸入。雖然沒有實際使用它,但它可以處理來電,也能檢查地理位置。該應用還不能做任何事情。這就是骨架應用。

在Android框架中,開發人員的任務更多的不是要構建一個完整的程序,而是實現特定行為,然後在正確的擴展點把這些行為插入到骨架應用中。MacApp作為最早的骨架應用框架之一,其箴言就是:「Don』t call us,we』ll call you.」如果創建Android應用的主要工作是理解如何擴展框架,那麼考慮一些通用的最佳實踐來實現這些擴展是有意義的。

重寫(override)和回調

最簡單且最容易實現的(程序員在框架中添加新的行為的首選)應該是回調。回調是Android庫中非常普遍的模式,其基本思想已經在第2章做了詳細描述。為了創建一個回調擴展點,類必須給出兩個定義。首先,定義Java接口(通常是以Handler、Callback或Listener結尾的名字)來描述回調行為,但是並不實現該行為。其次,定義setter方法,用一個實現該接口的對象作為其參數。

假定有個應用需要接收用戶的文本輸入。文本輸入、編輯和顯示當然需要一套龐大複雜的用戶接口類集合。但是,大部分接口應用不需要關心。相反,它只是在佈局(layout)中添加一個小組件庫,例如EditText(layout和widget在P167「組裝圖形界面」一節中描述)。Android框架對小組件進行實例化,在屏幕上顯示它,當用戶輸入時更新其內容等。實際上,它執行了除了程序所實現的功能之外的所有事情:內容文本由應用代碼進行處理。這是通過回調實現的。

Android文檔顯示EditText對像定義了方法addTextChangedListener,該方法作為參數接收TextWatcher對象。TextWatcher定義了方法,當EditText小組件的內容文本發生變化時,會調用這個方法。示例應用代碼大體如下所示:


public class MyModel {
      public MyModel(TextView textBox) {
            textBox.addTextChangedListener(
                new TextWatcher {
                     public void afterTextChanged(Editable s) {
                         handleTextChange(s);
                     }
                     public void beforeTextChanged(
                          CharSequence s,
                          int start,
                          int count,
                          int after)
                    { }
                    public void onTextChanged(
                          CharSequence s,
                          int start,
                          int count,
                          int after)
                     { }
                  });
      }
      void handleTextChange(Editable s) {
            // do something with s, the changed text.
      }
}
  

MyModel是應用的核心。它會處理用戶輸入的文本,執行一些相關的處理。當創建MyModel對像時,會傳遞TextBox對像給它,通過這個TextBox對象可以獲得用戶輸入的文本。現在,你對於閱讀這樣的代碼可能已經非常熟練了:MyModel構造函數會創建TextWatcher接口的一個新的匿名實現。它實現了該接口需要的3個方法,其中有兩個方法什麼也不做:onTextChanged和beforeTextChanged。但是,第3個方法afterTextChanged,調用MyModel的handleTextChange方法。

這一切都工作良好。這裡該應用並沒有使用這兩個必需的方法:beforeTextChanged和onTextChanged,這顯得有些混亂。除此之外,代碼分離很好。MyModel不知道TextView如何顯示屏幕上的文本,或者如何獲取其包含的文本。一個很小的類(TextWatcher的匿名實例)傳遞視圖和MyModel之間變化的文本。MyModel模型實現只關心當文本有變化時會發生什麼。

把用戶界面和執行行為結合起來的這個過程,通常稱為連接(wiring up)。連接雖然很強大,但也相當嚴格。客戶端代碼(註冊回調功能的代碼)不能改變調用者的行為。客戶端除了調用時傳遞的參數之外,沒有接收任何狀態信息。接口類型(在這個例子中即TextWatcher)是回調提供者和客戶端之間的一個顯式協議。

實際上,回調客戶端有一個操作會影響調用服務:它可以拒絕返回。客戶端代碼應該只把回調作為通知,不要嘗試執行任何冗長的內部處理。如果有一些重要的任務要執行(好幾百條指令或任何會使服務變慢的操作,如文件或網絡操作,這些操作應該排隊等待執行),很可能是通過另一個線程執行。P103「AsyncTask和UI線程」一節將深入探討如何實現這一點。

在令牌機制中,一個服務持有一個令牌,當它嘗試支持多個回調客戶端時,會由於CPU資源競爭而停止,即使所有的客戶端都表現很好。雖然addTextChangedListener支持多個客戶端訂閱,但Android客戶端中的很多回調函數只支持一個客戶端。通過這些回調函數(如setOnKeyListener),在特定對像上為特定的回調函數設置新的客戶端會取代之前的客戶端。之前註冊的客戶端不再接收任何回調通知。實際上,甚至不會再給該客戶端發送任何通知,因為它已經不再是個客戶端了。因此,新註冊的客戶端會接收到所有的通知。這也是回調這種方式中實際存在的局限性,即實際可支持的客戶端數量有限。如果必須給多個接收者發送通知,則需要以在應用程序的上下文中是安全的方式來實現它。

回調模式在整個Android庫中都存在。因為所有Android開發人員都熟悉回調模式術語,且以該模式設計應用也很有意義。一旦某個類需要接收另一個類的通知,尤其是當關聯在運行時發生很大變化時,可以考慮通過回調實現兩個類之間的關聯。如果兩個類之間的關聯不是動態的,則考慮使用依賴注入方式(dependency injection)(構造函數參數和final類型的成員變量)使得兩個類之間持久關聯。

多態和組合

在Android開發中,正如在任何其他面向對象的環境中,多態(polymorphism)和組合(composition)是擴展環境的強大工具。上面給出的例子本身已經證明了多態和組合的價值。我們先暫停一下,重新強化一下概念,重申其作為設計目標的價值。

TextWatcher的匿名實例作為回調對像傳遞給addTextChangedListener,它使用組合來實現這種行為。該實例本身並沒有實現任何行為。相反,addTextChangedListener委託(delegate)給MyModel的handleTextChange方法,採用的是has-a的實現方式,而不是is-a。這使得代碼看起來清晰獨立。舉個例子,如果對MyModel進行擴展,那麼使用另一個源中的文本,新的源也會使用handleTextChange方法。不需要跟蹤追查後面匿名類的代碼。

這個例子也說明了多態的用途。傳遞給addTextChangedListener方法的實例是強靜態類型,它是TextWatcher的匿名子類。其特定實現,在這個例子中,即委託給MyModel的handleTextChange方法,幾乎可以確定它和該接口的任何其他實現都不相同。雖然它實現了TextWatcher接口,無論其如何工作,它都是靜態類型。編譯器確保只給EditText的addTextChangedListener方法傳遞要執行操作的對象。該實現可能會有bug,但至少肯定不會給addTextChangedListener傳遞響應網絡事件的對象。這就是多態。

這個例子中的錯誤方法(anti-pattern)也值得一提,因為它很普遍。很多開發人員發現匿名類實質上是給函數傳遞指針,這種方式冗余笨拙。為了避免使用匿名類,他們一併忽略了消息對象,如下:


// !!! Anti-pattern warning
public class MyModel implements TextWatcher {
      public MyModel(TextView textBox) {
            textBox.addTextChangedListener(this);
      }
      public void afterTextChanged(Editable s) {
            handleTextChange(s);
      }
      public void beforeTextChanged(
            CharSequence s,
            int start,
            int count,
            int after)
      { }
      public void onTextChanged(
            CharSequence s,
            int start,
            int count,
            int after)
      { }
      void handleTextChange(Editable s) {
            // do something with s, the changed text.
      }
}
  

以上這種方式有時是有意義的。如果回調客戶端(在這個例子中,即MyModel)很小,邏輯簡單,只在一兩個場景下使用,該段代碼很清晰明瞭。

另外,如果(正如MyModel所述)該類將得到廣泛使用,且會應用於各種場合,消除消息類會破壞封裝,並限制擴展。顯然,對該實現進行擴展來處理另一個行為不同的TextBox,會顯得很混亂。

這個模式造成的另一個糟糕的影響是所謂的接口污染(interface pollution),常出現在極端應用的情況下。它看起來如下所示:


// !!! Anti-pattern ALERT!
public class MyModel
      implements TextWatcher, OnKeyListener, View.OnTouchListener,
            OnFocusChangeListener, Button.OnClickListener
{
      // ....
}
 

以上代碼看起來非常優雅,而且很常見。遺憾的是,這種用法使得MyModel和其處理的每個事件緊密耦合。

通常情況下,對於「接口污染」沒有硬性規定。正如前面指出的,有大量的工作代碼看起來就是如此。儘管如此,這種接口顯得更脆弱且更易變。當接口包含的方法過多時,可以考慮採用組合的方式對接口進行分解。

擴展Android類

雖然回調提供了對類的行為進行擴展的明確、定義良好的方式,但在某些情況下,回調方式缺乏足夠的靈活性。回調模式的一個明顯問題是,有時對代碼所做的控制是庫的設計人員所未預見的。如果服務沒有定義回調方式,則需要用其他方式把代碼注入到控制流。一種解決方式是創建子類。

Android庫中的一些類在設計上是專門用於繼承的(如android.widgets和AsyncTask的BaseAdapter類)。然而,一般而言,設計者不應該輕易使用子類模式。

子類可以完全重寫其超類的任何非final方法,因而這完全違反了類的層次結構方式。在Java類型系統中,例如TextBox的子類完全可以重寫其addTextChangedListener方法,忽略其參數輸入,對回調客戶端不執行文本內容變化的通知。(例如,可以設想一個實現了「安全」功能的文本框,它不會顯示其內容。)

這種違反常規的方式易於導致兩個類之間出現bug,而且難以發現。當開發人員採用非常規的子類方式以如之前所描述的方式來實現「安全」文本框時,就會出現第一種(也是更明顯的)問題。

假設開發人員構建了一個包含幾個部件的視圖,並對每個部件使用addTextChanged-Listener方法註冊回調函數。但是,在測試時他發現有幾個部件沒有正常工作。他花了好幾個小時的時間查看代碼,才發現有個方法似乎不做任何事情。突然,天亮了,他查看了該部件的源代碼,確定它破壞了類的語義規範。恍然大悟!

比以上情況更糟的是Android框架本身在不同的SDK版本之間可能會發生改變。可能addTextChangedListener方法的實現發生了變化。可能Android框架中的其他部分代碼開始調用addTextChangedListener方法,期望執行正常的操作。但是,由於子類重寫了這個方法,而導致整個應用執行失敗!

可以通過super調用這種實現方式來最大限度減少這類問題的發生,如下所示:


public void addTextChangedListener(TextWatcher watcher) {
      // your code here...
      super.addTextChangedListener(watcher)
      // more of your code here...
}
  

這種調用方式減少了由於子類對方法重寫所帶來的風險,但是它並不會保證執行正確,因為超類的實現可能會發生變化。在一些開發人員社區中,存在編碼規則,稱為「擴展設計」。這個規則要求所有的方法必須是abstract或final形式。雖然這個規定看似過於嚴格,但是想到子類的方法重寫可能會打破語義規範,除非通過super調用實現,否則這個規定其實也可以接受。