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

《Android程序设计:第2版》扩展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 warningpublic 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调用实现,否则这个规定其实也可以接受。