安卓专家级编程-三-

95 阅读1小时+

安卓专家级编程(三)

原文:Expert Android

协议:CC BY-NC-SA 4.0

六、高级表单处理

Abstract

表单处理是编写计算机应用(包括移动应用)的常见需求。在 Android 中,您可以通过设计活动、对话框或片段来满足这一需求,这些活动、对话框或片段的行为类似于具有大量数据输入字段的表单。

表单处理是编写计算机应用(包括移动应用)的常见需求。在 Android 中,您可以通过设计活动、对话框或片段来满足这一需求,这些活动、对话框或片段的行为类似于具有大量数据输入字段的表单。

开箱即用,Android SDK 具有基于类型控制每个字段行为的基本机制。Android SDK 还允许您在检测到字段出错时,以编程方式在每个字段上设置错误图标和消息。当表单上有一个或两个字段时,这些基本机制非常有用。然而,根据我们的经验,即使只有三四个字段,这种逐个字段的验证也会变得重复和笨拙。

所以我们问了这个问题,“有没有适用于 Android 的表单处理库?”在我们的研究中,我们在开源 Android 社区中发现了一些库。我们会在本章末尾给你一些解决方案的参考。虽然你可以下载并使用其中的一个库,但我们认为有必要在本章解释高级表单处理的一般原则。此外,我们还向您展示了如何开发一个简单而有效的表单处理框架。

解决这个表单处理问题的一般步骤非常简单。了解这些步骤将允许您为您可能遇到的开源库无法解决的许多情况定制解决方案。考虑到这一点,我们将在本章中讲述以下内容:

A simple application that requires form processing in order to address challenges of field-by-field validation   A general-purpose design to deal with form and field validations   Annotated source code that you can use to further customize the presented form processing framework  

当您使用我们在本章中记录的方法时,表单处理变得简单。然后,您可以专注于应用的主要行为,而不会迷失在粒度字段级验证的细节中。

基于规划表单的应用

为了演示字段验证的概念和代码,我们创建了一个简单的表单,允许用户注册服务。这个注册表单有常见的字段,如用户 ID、电子邮件地址和密码,如图 6-1 所示。密码字段是重复的,以确保准确性。

A978-1-4302-4951-1_6_Fig1_HTML.jpg

图 6-1。

A sample signup form

完成注册需要图 6-1 中的所有字段。如果用户单击注册按钮而没有填写某些字段,您需要突出显示这些字段,并告诉用户在提交表单之前填写这些字段。

图 6-2 显示了如果用户点击注册按钮而不填写字段,屏幕会是什么样子。

A978-1-4302-4951-1_6_Fig2_HTML.jpg

图 6-2。

Required field validation on form submit

在图 6-2 中,注意所有必填但未填写的字段都被高亮显示。此外,对于第一个必填字段,如果留空,会出现一条错误消息,指示该字段是必填的。图 6-3 显示了当一些字段被填写并且用户点击注册按钮提交表单时的表单。

A978-1-4302-4951-1_6_Fig3_HTML.jpg

图 6-3。

Partially filled-in form fields

在图 6-3 中,请注意,一旦用户开始在其中一个字段中输入信息,该字段的错误指示器就会消失。图 6-4 显示了当密码字段不匹配时该表单的另一种变体。

A978-1-4302-4951-1_6_Fig4_HTML.jpg

图 6-4。

Multi-field validation

在图 6-4 中,观察错误信息是如何明确错误性质的。当用户成功填写所有字段并点击注册按钮时,应用将向前移动,并可能显示如图 6-5 所示的成功活动。

A978-1-4302-4951-1_6_Fig5_HTML.jpg

图 6-5。

Successfully submitted form

到目前为止,我们已经说明了行为合理的基于表单的应用的要求。我们现在开始设计一个优雅的字段验证框架,它可以满足这些表单处理需求。我们将从一般原则开始。

表单验证的一般步骤

Android 中没有内置的框架来完全实现我们的代表性应用中所指示的行为。然而,Android 确实具有构建一个非常好的表单验证框架所需的基本特性。

构建良好的表单处理框架的一般步骤如下:

When you create the layout file for your form activity, use inputType attribute on the controls (or fields) that make up the form. There are a number of default input types, such as plain text, email, or a decimal number. Specifying the right input type automatically controls the user input that can be entered into that field on every key stroke. This feature is out of the box in the Android SDK.   Then you write validators that can check the behavior that is specific to that field when the form is submitted. These specific validators are attached to each field. They are not in the core framework, so you need to design them. We will show you an implementation for this.   Once you have the validators for each field, you attach the validators through code (as we do in this chapter) to the corresponding fields. You can also attach the validators to their fields through metadata-driven Java annotations. We have provided a URL to a framework that does precisely this; see the References at the end of this chapter.   Once the validators are attached to all the fields, you gather the fields into an abstraction called a Form or a FieldSet, so that you can validate the entire field set just once when the form is submitted. Again, this step is not core Android, but it will be part of the form-validation framework.  

你在 Android 社区中遇到的大多数框架都使用了类似的方法,或者略有不同。在本章的其余部分,我们将向您展示如何调整这些通用过程来实现您的表单验证框架。

设计基础活动

我们的样本表单验证框架使用了一个基于继承的解决方案,包括三个活动级别。

BaseActivity extends Activity

FormActivity extends BaseActivity

SignupFormTestActivity extends FormActivity

在这个活动层次中,SignupFormActivity如图 6-1 所示。FormActivity抽象了字段集并允许字段验证的方法。BaseActivity封装了非常常见的方法。让我们先看看代码,分析一下BaseActivity的设计。该活动的代码如清单 6-1 所示。

清单 6-1。封装常用函数的 BaseActivity

/*

* Provides many utility methods that are used by inherited classes.

* The utility methods include such things as toasts, alerts, log messages

* and navigating to other activities.

* Also includes the ability to turn on/off progress dialogs.

*/

public abstract class BaseActivity extends Activity

{

//Uses the tag from derived classes

private static String tag=null;

//To turn/off progress dialogs

private ProgressDialog pd = null;

//Transfer the tag from derived classes

public BaseActivity(String inTag)    {

tag = inTag;

}

//Just a way to log a message

public void reportBack(String message)    {

reportBack(tag,message);

}

public void reportBack(String tag, String message)    {

Log.d(tag,message);

}

//report a transient message and log it

public void reportTransient(String message)    {

reportTransient(tag,message);

}

//Report it using a toast

public void reportTransient(String tag, String message)

{

String s = tag + ":" + message;

Toast mToast = Toast.makeText(this, s, Toast.LENGTH_SHORT);

mToast.show();

reportBack(tag,message);

Log.d(tag,message);

}

//we often need to do string validations

public boolean invalidString(String s)   {

return StringUtils.invalidString(s);

}

public boolean validString(String s)    {

return StringUtils.validString(s);

}

//we often need to transfer to other activities

public void gotoActivity(Class activityClassReference)

{

Intent i = new Intent(this,activityClassReference);

startActivity(i);

}

//On callbacks turn on/off progress bars

public void turnOnProgressDialog(String title, String message){

pd = ProgressDialog.show(this,title,message);

}

public void turnOffProgressDialog()    {

pd.cancel();

}

//Sometimes you need an explicit alert

public void alert(String title, String message)

{

AlertDialog alertDialog = new AlertDialog.Builder(this).create();

alertDialog.setTitle(title);

alertDialog.setMessage(message);

alertDialog.setButton(DialogInterface.BUTTON_POSITIVE

"OK"

new DialogInterface.OnClickListener() {

public void onClick(DialogInterface dialog, int which) {

}

});

alertDialog.show();

}

}//eof-class

清单 6-1 中的BaseActivity很大程度上是通过代码中的行内注释自我记录的。我们发现这种将最常见的函数封装在基类中的方法对于继承的类非常有用。事实上,如果你注意到了,在同一个清单 6-1 中,我们甚至煞费苦心地从一个名为StringUtils的静态类中重新定义了一些方法,使得最常见的方法更容易调用。为了保证BaseActivity的完整性,下面是StringUtils的代码,如清单 6-2 所示。

清单 6-2。常用的基于字符串的方法

public class StringUtils {

public static boolean invalidString(String s)    {

return !validString(s);

}

public static boolean validString(String s)    {

if (s == null)        {

return false;

}

if (s.trim().equalsIgnoreCase(""))  {

return false;

}

return true;

}

}

Note

请注意,您可以从我们的网站下载整个项目。本章末尾给出了该 URL。

现在我们有了一个BaseActivity,让我们进入FormActivity类,它是表单验证框架的入口。

表单活动的设计与实现

BaseActivity类扩展而来的一个FormActivity提供了它的子类,其他基于表单的活动,一个精简的和不易出错的方法来收集和验证字段。这个基类(FromActivity)的职责是:

Provide an ability for derived form-based classes to add fields that can be validated   Provide a method to run validation on all of the fields when the form is submitted   As part of the validation, make the fields responsible for setting themselves up with appropriate errors  

在我们看了清单 6-3 中的FormActivity, shown的源代码后,我们将讨论每一个责任。

清单 6-3。FormActivity 封装字段验证

public abstract class FormActivity

extends BaseActivity

{

public FormActivity(String inTag) {

super(inTag);

}

//Provide an opportunity to add fields

//to this form. This is called a hook method

protected abstract void initializeFormFields();

//See how the above hook method is called

//whenever the content view is set on this activity

//containing the layout fields.

@Override

public void setContentView(int viewid) {

super.setContentView(viewid);

initializeFormFields();

}

//A set of fields or validators to call validation on

private ArrayList<IValidator> ruleSet = new ArrayList<IValidator>();

//Add a field which is also a validator

public void addValidator(IValidator v)    {

ruleSet.add(v);

}

//Validate the every field in the form

//Call this method when a form is submitted.

public boolean validateForm()

{

boolean finalResult = true;

for(IValidator v: ruleSet)

{

boolean result = v.validate();

if (result == false)

{

finalResult = false;

}

//if true go around

//if all true it should stay true

}

return finalResult;

}

}//eof-class

清单 6-3 的关键部分被突出显示。让我们首先考虑这个FormActivity类如何允许派生类添加字段。FormActivity有一个名为initializeFormFields()的抽象方法。该方法需要由派生类实现,以初始化和添加需要验证的字段。

为了确保方法initializeFormFields()被调用,FormActivity恢复了覆盖Activity类的setContentView()的技巧。方法setContentView()通常由派生类调用来为活动设置布局或主屏幕。因此,这是一个收集视图中需要验证的字段的好地方。认识到这一点,FormActivity自动调用initializeFormFields()作为被覆盖的setContentView()的一部分。

Note

如果出于某种原因,这种覆盖setContentView()的方法不适合您,那么您可以直接从活动创建回调中调用initializeFormFields()函数。这样,这个呼叫就没有魔力了;只需要在活动创建开始时调用它。

强制派生类遵守规定协议的方法被称为template/hook模式。这里的hook方法就是initializeFormField()。在特定时间触发钩子的template方法是setContentView()。在这种模式中,派生类仅仅实现隔离的动作单元(比如initializeFormFields)。template方法将定义协议何时以何种顺序调用这些动作,以及何时调用多少次,等等。

您将很快看到派生类是如何实现initializeFormField()的,在这里它们将调用addValidator()方法。addValidator()方法依赖于下面的类来工作:

IValueValidator //Represents how to validate any string

IValidator //Represents a validatable entity that can self report errors

Field //extends a Validator and also allows

清单 6-4 显示了IValueValidator接口的定义。

清单 6-4。ivalue validator:value validating 类的协定

/*

* An interface for such value validators as RegExValidator

*/

public interface IValueValidator

{

//Given a string to see if it is valid

boolean validateValue(String value);

//what should be the error message when the field is wrong

String getErrorMessage();

}

因此,值验证器负责验证一个字符串值,如果它无效,它会确定它有什么问题。这个接口的目的是让一个像Field这样的对象可以附加许多值验证器。一个验证器可能正在检查该值是否必须是 10 个字符。另一个验证器可以检查所有的字符都是数字。然后,Field可以通过传递它的值并根据一组验证器对它进行评估来检查每个值验证器。

Field实现了一个稍微不同的接口,叫做IValidator。在我们看一下Field的实现之前,让我们先看看这个,如清单 6-5 所示。

清单 6-5。IValidator:自报告实体(如字段)的合同

public interface IValidator {

public boolean validate();

}

一个IValidator类似于一个IValueValidator。然而,与IvalueValidator不同的是,IValidator不仅要验证,还要反映验证实体的含义,比如改变被验证实体的状态。例如,当一个Field被验证并且如果Field是错误的,则Field将显示一条错误信息并且还显示一个图标(参见图 6-1 )。当你检查Field类的实现时,你会看到这种关系,没有延迟,现在如清单 6-6 所示。

清单 6-6。字段:表示控件验证行为的具体类

public class Field

implements IValidator

{

//The underlying control this field is representing

private TextView control;

//Because whether required or not is so essential

//give it a special status.

private boolean required = true;

//A list of value validators to be attached

private ArrayList<IValueValidator> valueValidatorList

= new ArrayList<IValueValidator>();

public Field(TextView tv) {

this(tv, true);

}

public Field(TextView tv, boolean inRequired) {

control = tv;

required = inRequired;

}

//Validate if it is a required field first.

//Also run through all the value validators.

//Stop on the first validator that fails.

//Show the error message from the failed validator.

//Use the android setError to show the errors.

@Override

public boolean validate()

{

String value = getValue();

if (StringUtils.invalidString(value))

{

//invalid string

if (required)

{

warnRequiredField();

return false;

}

}

for(IValueValidator validator: valueValidatorList)

{

boolean result = validator.validateValue(getValue());

if (result == true) continue;

if (result == false)

{

//this validator failed

String errorMessage = validator.getErrorMessage();

setErrorMessage(errorMessage);

return false;

}

}//eof-for

//All validators passed

return true;

}//eof-validate

private void warnRequiredField() {

setErrorMessage("This is a required field");

}

public void setErrorMessage(String message)    {

control.setError(message);

}

public String getValue() {

return this.control.getText().toString();

}

}//eof-class

现在,我们可以讨论表单验证框架的关键组件的实现细节了。Field本身既是一个验证器,也有一组值验证器。一个Field实现了IValidator的契约,因为它不仅想验证自己,还想显示更正该字段所需的任何线索或提示。

一个给定的字段在一个表单中是否是必需的是如此的基本和重要,以至于我们已经将该功能直接硬编码到Field定义中。一个字段上的其余验证可以封装到许多值验证器中。

所以,这里是一个Field如何工作。派生类用它的底层编辑字段初始化一个Field。然后,派生类附加一系列值验证器来进一步验证该字段。然后将Field添加到FormAcvity中,成为验证表单时得到验证的字段集的一部分。下面是如何使用Field对象的伪代码,如清单 6-7 所示。

清单 6-7。创建和注册字段对象的伪代码

//Say emailEditText is a required form field

EditText emailEditText;

//Create a Field object that wraps the emailEditText

//By default the field becomes a required field

Field emailField = new Field(emailEditText);

//Add further validators. Here are some sample validators

emailField.addValidator(new StrictEmailValidator());

emailField.addValidator(new MaxLenghtValidator());

//Add this field to the form field set

addValidator(emailField);

...add other fields similarly if you have them

addValidator(field2);

..etc

清单 6-7 中的伪代码显示了如何处理单个字段。有时候你也要做跨领域的验证。例如,在图 6-1 中,如果两个密码字段必须匹配才能通过表单验证,那么任何一个字段验证都不能满足这个要求。清单 6-8 展示了如何创建一个复合字段来完成这种多字段验证。

清单 6-8。PasswordRule:多字段验证的一个例子

/*

* A class simulating multi-field validation

*/

public class PasswordFieldRule implements IValidator

{

private TextView password1;

private TextView password2;

public PasswordFieldRule(TextView p1, TextView p2)

{

password1 = p1;

password2 = p2;

}

@Override

public boolean validate()

{

String p1 = password1.getText().toString();

String p2 = password2.getText().toString();

if (p1.equals(p2))

{

return true;

}

//They are not the same

password2.setError("Sorry, password values don't match!");

return false;

}

}//eof-class

可以将类PasswordFieldRule添加到表单中,就像它是另一个字段一样。清单 6-9 是一个基于通用正则表达式的值赋值函数的例子。

清单 6-9。正则表达式值验证器

/*

* A general purpose regular expression value validator

*/

public class RegExValueValidator

implements IValueValidator

{

private String regExPattern;

private String error;

private String hint;

RegExValueValidator(String inRegExPattern

String errorMessage, String inHint)

{

regExPattern = inRegExPattern;

error = errorMessage;

hint = inHint;

}

@Override

public boolean validateValue(String value) {

if (value.matches(regExPattern) == true)

{

return true;

}

return false;

}

@Override

public String getErrorMessage() {

return error + ". " + hint;

}

}

到目前为止,我们已经解释了最终实现SignupTestFormActivity所需的所有类,这些类将实现示例应用所需的行为。

实现 SignupActivityTestForm

清单 6-10 显示了源代码,它汇集了到目前为止本章涉及的所有细节,并展示了现在做字段验证是多么简单。

清单 6-10。SignupActivityTestForm:将所有内容放在一起

/*

* A test form to demonstrate field validation

*/

public class SignupActivityTestForm

extends FormActivity

{

private static String tag = "SignupActivity";

//Form Fields

EditText userid;

EditText password1;

EditText password2;

EditText email;

public SignupActivityTestForm()    {

super(tag);

}

/** Called when the activity is first created. */

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.signup);

}

//from FormActivity

@Override

protected void initializeFormFields()

{

this.reportBack("form initialized");

//Keep local variables

userid = (EditText)findViewById(R.id.userid);

password1 = (EditText)findViewById(R.id.password1);

password2 = (EditText)findViewById(R.id.password2);

email = (EditText)findViewById(R.id.email);

//Setup the validators

addValidator(new Field(userid));

addValidator(new Field(password1));

addValidator(new Field(password2));

addValidator(new Field(email));

addValidator(new PasswordFieldRule(password1,password2));

}

public void signupButtonClick(View v)

{

if (validateForm() == false)

{

reportTransient("Make sure all fields have valid values");

return;

}

//everything is good

String userid = getUserId();

String password = getPassword1();

String email = getUserEmail();

reportTransient("Going to sign up now");

signup(userid, email, password);

}

private void signup(String userid, String email, String password)

{

gotoActivity(WelcomeActivity.class);

}

//Utility methods

private String getUserId()    {

return getStringValue(R.id.userid);

}

private String getUserEmail()    {

return getStringValue(R.id.email);

}

private String getPassword1()    {

return getStringValue(R.id.password1);

}

private String getStringValue(int controlId)

{

TextView tv = (TextView)findViewById(controlId);

if (tv == null)

{

throw new RuntimeException("Sorry Can't find the control id");

}

//view available

return tv.getText().toString();

}

}//eof-class

除了注册表单活动的源之外,让我们看看相应的布局文件,如清单 6-11 所示,这样您就可以识别您试图验证的字段。

清单 6-11。支持 SignupActivityTestForm 所需字段的布局文件

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:orientation="vertical" >

<!-- Userid -->

<TextView android:layout_width="fill_parent"

android:layout_height="wrap_content" android:text="Userid" />

<EditText android:id="@+id/userid"

android:layout_width="fill_parent" android:layout_height="wrap_content"/>

<!-- email -->

<TextView android:layout_width="fill_parent"

android:layout_height="wrap_content" android:text="email" />

<EditText android:id="@+id/email" android:layout_width="fill_parent"

android:layout_height="wrap_content"``android:inputType="textEmailAddress"

<!-- password1 -->

<TextView android:layout_width="fill_parent" android:layout_height="wrap_content"

android:text="password" />

<EditText android:id="@+id/password1" android:layout_width="fill_parent"

android:layout_height="wrap_content"``android:inputType="textPassword"

<!-- password2 -->

<TextView android:layout_width="fill_parent" android:layout_height="wrap_content"

android:text="Reenter Password" />

<EditText android:id="@+id/password2" android:layout_width="fill_parent"

android:layout_height="wrap_content" android:inputType="textPassword"

/>

<!-- form submit button -->

<Button android:id="@+id/SignupButton" android:layout_width="fill_parent"

android:layout_height="wrap_content" android:onClick="signupButtonClick"

android:text="SignUp" />

</LinearLayout>

现在让我们一节一节地分析清单 6-10 中SignupActivityTestForm的行为。我们从initializeFormFields()方法开始。在这里,我们首先收集代表我们的字段的所有控件。然后,我们将所有字段作为必填字段注册到表单中。此外,我们创建一个PasswordRule,它将password1password2字段作为输入。

当注册按钮(见图 6-1 和清单 6-11)被点击时,它调用函数signupButtonClick()。这个方法又调用基类FormActivity定义的validateForm()方法。(关于这个方法的实现,请参见清单 6-3。)如果字段无效,它们会自动设置错误指示器和消息。用户焦点被带到第一个出错的字段。

如果整个表单都是有效的,那么控制转到signupSuccessful()方法。该方法只是调用BaseActivity定义的gotoActivity()来调用WelcomeActivity,如图 6-5 所示。

Note

注意,我们没有给出 WelcomeActivity 的源代码。如果你想了解 Android 可以完成的最简单的活动,你可以下载这一章的内容。

在现实世界中,注册可能涉及到服务器端调用,要求您提供进度对话框。当你需要的时候,这些条款都在BaseActivity里。(参见清单 6-1 中的BaseActivity。)

还要注意,在用于SignupActivityTestForm的布局文件(清单 6-11)中,我们使用了android:inputType属性来描述输入字段的性质。这是 Android 中的一个关键条款,它限制了可以在文本字段中输入的内容。我们在本章的参考资料中提供了一个 URL,在那里你可以找到所有可能的inputTypes。这些输入类型包括:

text

textCapCharacters

textCapWords

textMultiLine

textUri

textEmailSubject

textEmailAddress

textPersonName

textPostalAddress

textWebEditText

number

numberSigned

numberDecimal

numberPassword

phone

datetime

date

time

最后,请注意,您可以从本章末尾指定的 URL 下载示例程序,亲自测试该行为。

对创建基于表单的活动的改进

创建基于表单的活动是乏味且重复的。我们在这里概述了如何简化这项工作。您还可以根据自己的需要进一步定制和优化这个框架。例如,您可能希望使用 Java 元数据注释来注册字段。在这一章中,我们使用 Java 代码来使框架对你是透明的。或者您可能希望两种方法都允许。在这一章中,我们还使用了继承方法,将各自的活动规定为相互扩展。这可能会带来一些限制,因为除了接口之外,multiple inheritance在 Java 中不可用。您可能希望将这种方法转换为基于委托的方法,以便缓解这种约束。也有可能你的表单在 Android Fragment中,而不在活动中。在这种情况下,您需要定制您的框架来适应片段,而不是活动。

如果您有许多基于表单的活动,您可能希望使用一个简单的代码生成框架来创建活动类和 xml 布局文件,并注册字段以进行验证。然后,您可以使用生成的代码作为起点来修改代码。这里有一个这种意图的简单例子。比方说,你想参加一个像我们在本章中所展示的活动。你只要说:

<form>

<email>

<userid>

<password1>

<password2>

<signup type="button">

</form>

现在代码生成器可以创建所有的工件:activity 类、布局 xml 文件、创建字段的必要方法等等。事实上,我们鼓励您根据自己的具体需求来完善这个解决方案。

参考

我们发现以下链接对本章的研究很有帮助。

摘要

在编写移动应用时,基于表单的活动也很常见。本章介绍了一个用于验证表单域的灵活框架。开发人员可以进一步增强这个框架,以满足他们的特定需求。

复习问题

以下问题有助于巩固您在本章中学到的知识:

What is a good way to write form-based activities in Android?   What base Android SDK features are available to aid field validations in the Android SDK?   How do you use regular expressions to validate form fields?   What is an android:inputType attribute and how many input types are available?   How can you abstract progress dialogs in base classes?   How can you abstract alerts in base classes?

七、使用电话 API

Abstract

许多 Android 设备都是智能手机,但到目前为止,我们还没有谈到如何编写使用手机功能的应用。在本章中,我们将向您展示如何发送和接收短信息服务(SMS)信息。我们还将涉及 Android 中电话 API 的其他几个有趣的方面,包括会话发起协议(SIP)功能。SIP 是用于实现互联网协议语音(VoIP)的 IETF 标准,通过该标准,用户可以在互联网上进行类似电话的呼叫。SIP 也可以处理视频。

许多 Android 设备都是智能手机,但到目前为止,我们还没有谈到如何编写使用手机功能的应用。在本章中,我们将向您展示如何发送和接收短信息服务(SMS)信息。我们还将涉及 Android 中电话 API 的其他几个有趣的方面,包括会话发起协议(SIP)功能。SIP 是用于实现互联网协议语音(VoIP)的 IETF 标准,通过该标准,用户可以在互联网上进行类似电话的呼叫。SIP 也可以处理视频。

使用 SMS

SMS 代表短消息服务,但它通常被称为文本消息。Android SDK 支持发送和接收文本消息。通过使用 SMS 管理器,您可以发送和接收任意长度的消息。如果消息超过了单个消息的字符限制,SMS 管理器提供了一种以块为单位发送较长消息的方法。SMS 管理器还提供发送消息成功或失败的状态更新。我们将从讨论如何用 SDK 发送 SMS 消息开始。

发送短信

要从您的应用发送文本消息,您需要将android.permission.SEND_SMS权限添加到您的清单文件中,然后使用android.telephony.SmsManager类。这个例子的第一段 Java 代码见清单 7-1,它完成了消息发送。

清单 7-1。发送 SMS(文本)消息

public class TelephonyDemo extends Activity

{

protected static final String TAG = "TelephonyDemo";

protected static final String SENT_ACTION =

"com.androidbook.telephony.SMS_SENT_ACTION";

protected static final String DELIVERED_ACTION =

"com.androidbook.telephony.SMS_DELIVERED_ACTION";

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.main);

}

public void doSend(View view) {

EditText addrTxt =

(EditText)findViewById(R.id.addrEditText);

EditText msgTxt =

(EditText)findViewById(R.id.msgEditText);

try {

sendSmsMessage(

addrTxt.getText().toString()

msgTxt.getText().toString());

} catch (Exception e) {

Toast.makeText(this, "Failed to send SMS"

Toast.LENGTH_LONG).show();

e.printStackTrace();

}

}

private void sendSmsMessage(String address, String message)

throws Exception

{

SmsManager smsMgr = SmsManager.getDefault();

// Split the message up into manageable chunks if needed

ArrayList<String> messages = smsMgr.divideMessage(message);

if(messages.size() > 1) {

int count = messages.size();

// Will need to send with multipart

// so prepare the pending intents

ArrayList<PendingIntent> sentPIs =

new ArrayList<PendingIntent>(count);

ArrayList<PendingIntent> deliveredPIs =

new ArrayList<PendingIntent>(count);

for(int i = 0; i<count; i++) {

sentPIs.add(PendingIntent.getBroadcast(

TelephonyDemo.this, 0

new Intent(SENT_ACTION), 0));

deliveredPIs.add(PendingIntent.getBroadcast(

TelephonyDemo.this, 0

new Intent(DELIVERED_ACTION), 0));

}

smsMgr.sendMultipartTextMessage(address, null

messages, sentPIs, deliveredPIs);

Toast.makeText(this, "Multipart SMS Sent"

Toast.LENGTH_LONG).show();

}

else {

smsMgr.sendTextMessage(address, null, message

PendingIntent.getBroadcast(

TelephonyDemo.this, 0

new Intent(SENT_ACTION), 0)

PendingIntent.getBroadcast(

TelephonyDemo.this, 0

new Intent(DELIVERED_ACTION), 0)

);

Toast.makeText(this, "SMS Sent"

Toast.LENGTH_LONG).show();

}

}

}

Note

我们会在本章末尾给你一个 URL,你可以用它来下载本章中的项目。这将允许您将这些项目直接导入到 Eclipse 中。

发送 SMS 消息的代码是通过在 UI 中单击按钮来调用的。地址和文本消息被传递给sendSmsMessage()。您可能想在您的实际应用中做一些编辑检查。SmsManager有一个名为divideMessage()的方法,将消息字符串分割成符合 SMS 规范的块。如果有一个以上的块,你要用sendMultipartTextMessage()的方法;否则,你要用sendTextMessage()。无论哪种情况,您都想知道发送是否成功。这就是PendingIntent的用武之地。

通过包含每个块的发送状态和交付状态的PendingIntent(或者整个消息,如果它适合一条 SMS 消息的话),您的应用可以被通知失败,在某些情况下,成功。SmsManager可以将意图广播回您的应用,让它知道您发送的 SMS 消息发生了什么。这些意图由一个BroadcastReceiver处理,其源代码如清单 7-2 所示。

清单 7-2。接收 SMS 状态意向

public class MyBReceiver extends BroadcastReceiver {

@Override

public void onReceive(Context context, Intent intent) {

String action = intent.getAction();

Log.d(TelephonyDemo.TAG, "Got action of " + action);

if(TelephonyDemo.SENT_ACTION.compareTo(action) == 0) {

Log.d(TelephonyDemo.TAG, "SMS sent intent received.");

switch(getResultCode()) {

case Activity.RESULT_OK:

Log.d(TelephonyDemo.TAG, "SMS sent OK.");

break;

case SmsManager.RESULT_ERROR_RADIO_OFF:

Log.d(TelephonyDemo.TAG

"*** SMS not sent. Radio is off.");

break;

case SmsManager.RESULT_ERROR_NO_SERVICE:

Log.d(TelephonyDemo.TAG

"*** SMS not sent. No SMS service.");

break;

case SmsManager.RESULT_ERROR_NULL_PDU:

Log.d(TelephonyDemo.TAG

"*** SMS not sent. PDU was null.");

break;

case SmsManager.RESULT_ERROR_GENERIC_FAILURE:

Log.d(TelephonyDemo.TAG

"*** SMS not sent. Unknown failure.");

break;

default:

Log.d(TelephonyDemo.TAG, "*** Unknown sent code: "

+ getResultCode());

break;

}

}

if(TelephonyDemo.DELIVERED_ACTION.compareTo(action) == 0) {

Log.d(TelephonyDemo.TAG, "SMS delivered intent received.");

switch(getResultCode()) {

case Activity.RESULT_OK:

Log.d(TelephonyDemo.TAG, "SMS delivered.");

break;

case Activity.RESULT_CANCELED:

Log.d(TelephonyDemo.TAG, "*** SMS not delivered.");

break;

default:

Log.d(TelephonyDemo.TAG, "*** Unknown delivery code: "

+ getResultCode());

break;

}

}

}

}

当试图向运营商的 SMS 服务器发送消息(或消息的一部分)时,将触发BroadcastReceiver。您会注意到有两个动作可以返回给这个应用:发送状态和交付状态。重要的是要认识到发送状态比交付状态更可靠。根据我们的经验,也根据 Android 的例子来判断,不能保证收到已交付的状态。也就是说,您可能会收到也可能不会收到短信发送状态的任何指示。但是,如果您的应用被通知交付失败,包含清单 7-2 中的代码来指示交付状态也无妨。但是,不要依赖于收到一个肯定的已发送状态来确定 SMS 消息已发送,否则您可能会不必要地重新发送。

在许多演示应用中,BroadcastReceiveronResume()注册,在onPause()取消注册。然而,由于您可能希望接收广播,即使您的活动在后台进行,您将希望使用自己注册的BroadcastReceiver来处理广播。清单 7-3 显示了注册您的BroadcastReceiverAndroidManifest.xml部分。

清单 7-3。MyBReceiver 的 AndroidManifest.xml

<receiver android:name="MyBReceiver">

<intent-filter>

<action

android:name="com.androidbook.telephony.SMS_SENT_ACTION" />

</intent-filter>

<intent-filter>

<action

android:name="com.androidbook.telephony.SMS_DELIVERED_ACTION" />

</intent-filter>

</receiver>

清单 7-1 中的例子演示了使用 Android SDK 发送 SMS 文本消息。用户界面有两个EditText字段:一个用于捕获 SMS 接收者的目的地址(电话号码),另一个用于保存文本消息。用户界面还有一个发送短信的按钮,如图 7-1 所示。

A978-1-4302-4951-1_7_Fig1_HTML.jpg

图 7-1。

The UI for the SMS example

测试该应用时,您可以向同一台设备发送文本消息。观察 LogCat 中指示应用正在做什么的消息。示例中有趣的部分是sendSmsMessage()方法。该方法使用SmsManager类的sendTextMessage()方法来发送 SMS 消息。下面是SmsManager.sendTextMessage()的签名:

sendTextMessage(String destinationAddress, String smscAddress

String textMsg, PendingIntent sentIntent

PendingIntent deliveryIntent);

在本例中,您只填充了目的地址和文本消息参数。但是,您可以自定义该方法,使其不使用默认的 SMS 中心(蜂窝网络上发送 SMS 消息的服务器的地址)。如上图所示,还有一种发送多部分消息的方法,那就是sendMultipartTextMessage()。SmsManager 还有一种发送数据消息的方法,使用字节数组代替字符串消息。此方法还允许在 SMS 服务器上指定备用端口号。

发送 SMS 消息有两个主要步骤:发送和递送。当每个步骤完成时,如果您的应用提供了一个未决的意图,它就会被广播。您可以将您想要的任何内容放入挂起的 intent 中,比如 action,但是传递到您的BroadcastReceiver的结果代码将特定于 SMS 发送或交付。此外,根据 SMS 系统的实施情况,您可能会获得与无线电错误或状态报告相关的额外数据。

如果没有挂起的意图,您的代码就无法判断文本消息是否发送成功。不过,在测试时,你可以分辨出。如果您在一个仿真器中启动这个示例应用,并启动仿真器的另一个实例(从命令行或从 Eclipse 窗口➤ Android SDK 和 AVD 管理器屏幕),您可以使用另一个仿真器的端口号作为目的地址。端口号是出现在模拟器窗口标题栏中的数字,通常类似于 5554。单击 Send Text Message 按钮后,您应该会在另一个模拟器中看到一个通知,表明您的文本消息已在另一端收到。

发送短信只是故事的一半。现在,我们将向您展示如何监控收到的短信。

监控收到的短信

您使用刚刚创建的相同应用来发送 SMS 消息,并且您添加了一个BroadcastReceiver来监听动作android.provider.Telephony.SMS_RECEIVED .当设备接收到 SMS 消息时,这个动作由 Android 广播。

当您注册您的接收器时,您的应用将在收到 SMS 消息时得到通知。监控收到的 SMS 消息的第一步是请求接收它们的许可。为此,您需要向清单文件添加android.permission.RECEIVE_SMS权限。要实现接收者,您需要编写一个扩展android.content.BroadcastReceiver的类,然后在清单文件中注册接收者。清单 7-4 包括了AndroidManifest.xml文件和你的 receiver 类。请注意,这两种权限都出现在清单文件中,因为您仍然需要为上面创建的活动发送权限。

清单 7-4。监控短信

<!-- This file is AndroidManifest.xml -->

<manifest xmlns:android="http://schemas.android.com/apk/res/android

package="com.androidbook.telephony" android:versionCode="1"

android:versionName="1.0">

<uses-permission android:name="android.permission.RECEIVE_SMS"/>

<uses-permission android:name="android.permission.SEND_SMS"/>

[ ... ]

<receiver android:name="MySMSMonitor">

<intent-filter>

<action

android:name="android.provider.Telephony.SMS_RECEIVED"/>

</intent-filter>

</receiver>

[ ... ]

</manifest>

// This file is MySMSMonitor.java

import android.content.BroadcastReceiver;

import android.content.Context;

import android.content.Intent;

import android.telephony.SmsMessage;

import android.util.Log;

public class MySMSMonitor extends BroadcastReceiver

{

private static final String ACTION =

"android.provider.Telephony.SMS_RECEIVED";

@Override

public void onReceive(Context context, Intent intent)

{

if(intent!=null && intent.getAction()!=null &&

ACTION.compareToIgnoreCase(intent.getAction())==0)

{

Object[] pduArray= (Object[]) intent.getExtras().get("pdus");

SmsMessage[] messages = new SmsMessage[pduArray.length];

for (int i = 0; i<pduArray.length; i++) {

messages[i] = SmsMessage.createFromPdu(

(byte[])pduArray [i]);

Log.d("MySMSMonitor", "From: " +

messages[i].getOriginatingAddress());

Log.d("MySMSMonitor", "Msg: " +

messages[i].getMessageBody());

}

Log.d("MySMSMonitor","SMS Message Received.");

}

}

}

清单 7-4 的顶部是BroadcastReceiver拦截 SMS 消息的清单定义。短信监控类是MySMSMonitor。该类实现了抽象的onReceive()方法,当 SMS 消息到达时,系统会调用该方法。测试应用的一种方法是使用 Eclipse 中的模拟器控件视图。运行模拟器中的应用,并前往窗口➤显示查看➤其他➤安卓➤模拟器控制。用户界面允许您向模拟器发送数据,以模拟接收 SMS 消息或电话呼叫。如图 7-2 所示,您可以通过填充“来电号码”字段并选择 SMS 单选按钮向模拟器发送 SMS 消息。接下来,在消息字段中键入一些文本,然后单击发送按钮。这样做向模拟器发送一条 SMS 消息,并调用您的BroadcastReceiveronReceive()方法。

A978-1-4302-4951-1_7_Fig2_HTML.jpg

图 7-2。

Using the Emulator Control UI to send SMS messages to the emulator

onReceive()方法将具有广播意图,它将包含bundle属性中的SmsMessage。你可以通过调用intent.getExtras().get("pdus")来提取SmsMessage。该调用返回以协议描述单元(PDU)模式定义的对象数组,这是一种表示 SMS 消息的行业标准方式。然后您可以将 PDU 转换成 Android SmsMessage对象,如清单 7-4 所示。如您所见,您从 intent 获得了作为对象数组的 PDU。然后构建一个与 PDU 数组大小相等的SmsMessage对象数组。最后,迭代 PDU 数组,并通过调用SmsMessage.createFromPdu()从 PDU 创建SmsMessage对象。

你读完收到的信息后要做的事情一定要快。广播接收机在系统中获得高优先级,但是它的任务必须快速完成,并且它不会被放在前台供用户查看。因此,你的选择是有限的。你不应该做任何直接的 UI 工作。发布一个通知是好的,启动一个服务来继续工作也是好的。一旦onReceive()方法完成,onReceive()方法的托管进程随时可能被终止。启动一个服务是可以的,但是绑定到一个服务就不行了,因为这需要您的进程存在一段时间,而这可能不会发生。

现在,让我们通过查看如何使用各种 SMS 文件夹来继续我们关于 SMS 的讨论。

使用 SMS 文件夹

访问 SMS 收件箱是另一个常见的需求。首先,向清单文件添加 read SMS 权限(android.permission.READ_SMS)。添加此权限使您能够阅读 SMS 收件箱。

要阅读 SMS 消息,您可以在 SMS 收件箱上执行一个查询,如清单 7-5 所示。

清单 7-5。显示来自 SMS 收件箱的消息

<?xml version="1.0" encoding="utf-8"?>

<!-- This file is /res/layout/sms_inbox.xml -->

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android

android:orientation="vertical"

android:layout_width="fill_parent"

android:layout_height="fill_parent" >

<TextView android:id="@+id/row"

android:layout_width="fill_parent"

android:layout_height="fill_parent"/>

</LinearLayout>

// This file is SMSInboxDemo.java

import android.app.ListActivity;

import android.database.Cursor;

import android.net.Uri;

import android.os.Bundle;

import android.widget.ListAdapter;

import android.widget.SimpleCursorAdapter;

public class SMSInboxDemo extends ListActivity {

private ListAdapter adapter;

private static final Uri SMS_INBOX =

Uri.parse("content://sms/inbox");

@Override

public void onCreate(Bundle bundle) {

super.onCreate(bundle);

Cursor c = getContentResolver()

.query(SMS_INBOX, null, null, null, null);

startManagingCursor(c);

String[] columns = new String[] { "body" };

int[]      names = new int[]    { R.id.row };

adapter = new SimpleCursorAdapter(this, R.layout.sms_inbox

c, columns, names);

setListAdapter(adapter);

}

}

清单 7-5 中的代码打开 SMS 收件箱并创建一个列表,列表中的每一项都包含 SMS 消息的正文部分。清单 7-5 的布局部分包含一个简单的TextView,它将在一个列表项中保存每条消息的正文。要获得 SMS 消息列表,您需要创建一个指向 SMS 收件箱(content://sms/inbox)的 URI,然后执行一个简单的查询。然后过滤短信正文,并设置ListActivity的列表适配器。执行清单 7-5 中的代码后,您会在收件箱中看到一个 SMS 消息列表。确保在模拟器上运行代码之前,使用模拟器控件生成一些 SMS 消息。

因为您可以访问 SMS 收件箱,所以您希望能够访问其他与 SMS 相关的文件夹,如“已发送”或“草稿”文件夹。访问收件箱和访问其他文件夹的唯一区别是您指定的 URI。例如,您可以通过对content://sms/sent执行查询来访问已发送文件夹。以下是 SMS 文件夹的完整列表以及每个文件夹的 URI:

  • 全部:content://sms/all
  • 收件箱:content://sms/inbox
  • 已发送:content://sms/sent
  • 草稿:content://sms/draft
  • 发件箱:content://sms/outbox
  • 失败:content://sms/failed
  • 排队:content://sms/queued
  • 未送达:content://sms/undelivered
  • 对话:content://sms/conversations

Android 结合了 MMS 和 SMS,并允许您使用mms-sms权限同时访问两者的内容供应器。因此,您可以访问这样的 URI:

content://mms-sms/conversations

发送电子邮件

既然您已经看到了如何在 Android 中发送 SMS 消息,您可能会认为您可以访问类似的 API 来发送电子邮件。不幸的是,Android 并没有为你提供发送电子邮件的 API。普遍的共识是,用户不希望应用在他们不知情的情况下代表他们发送电子邮件。相反,要发送电子邮件,你必须通过注册的电子邮件应用。例如,您可以使用ACTION_SEND来启动电子邮件应用,如清单 7-6 所示。

清单 7-6。通过意向启动电子邮件应用

Intent emailIntent=new Intent(Intent.ACTION_SEND);

String subject = "Hi!";

String body = "hello from android....";

String[] recipients = new String[]{"``aaa@bbb.com

emailIntent.putExtra(Intent.EXTRA_EMAIL, recipients);

emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject);

emailIntent.putExtra(Intent.EXTRA_TEXT, body);

emailIntent.setType("message/rfc822");

startActivity(emailIntent);

这段代码启动默认的电子邮件应用,并允许用户决定是否发送电子邮件。您可以添加到电子邮件意向中的其他“额外内容”包括EXTRA_CCEXTRA_BCC

让我们假设您想要在邮件中发送一个电子邮件附件。要做到这一点,您可以使用类似下面这样的代码,其中Uri是对您想要作为附件的文件的引用:

emailIntent.putExtra(Intent.EXTRA_STREAM

Uri.fromFile(new File(myFileName)));

接下来,我们来谈谈电话管理器。

使用电话管理器

电话 API 还包括电话管理器(android.telephony.TelephonyManager),您可以使用它来获取有关设备上电话服务的信息、获取订户信息以及注册电话状态更改。一个常见的电话用例要求应用对来电执行业务逻辑。例如,音乐播放器可能会因有来电而暂停播放,并在通话结束后继续播放。

监听电话状态变化的最简单方法是在android.intent.action.PHONE_STATE上实现一个广播接收器。你可以用同样的方法监听收到的短信,如上所述。另一种方法是使用电话管理器。在本节中,我们将向您展示如何注册电话状态更改以及如何检测来电。清单 7-7 显示了细节。

清单 7-7。使用电话管理器

<?xml version="1.0" encoding="utf-8"?>

<!-- This file is res/layout/main.xml -->

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android

android:orientation="vertical"

android:layout_width="fill_parent"

android:layout_height="fill_parent"

>

<Button

android:id="@+id/callBtn"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Place Call"

android:onClick="doClick"

/>

<Button

android:id="@+id/quitBtn"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Quit"

android:onClick="doClick"

/>

</LinearLayout>

// This file is PhoneCallActivity.java

package com.androidbook.phonecall.demo;

import android.app.Activity;

import android.content.Context;

import android.content.Intent;

import android.net.Uri;

import android.os.Bundle;

import android.telephony.PhoneStateListener;

import android.telephony.TelephonyManager;

import android.util.Log;

import android.view.View;

public class PhoneCallActivity extends Activity {

private static final String TAG = "PhoneCallDemo";

private TelephonyManager teleMgr = null;

private MyPhoneStateListener myListener = null;

@Override

protected void onCreate(Bundle savedInstanceState)

{

super.onCreate(savedInstanceState);

setContentView(R.layout.main);

teleMgr =

(TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);

myListener = new MyPhoneStateListener();

teleMgr.listen(myListener, PhoneStateListener.LISTEN_CALL_STATE);

String myIMEI = teleMgr.getDeviceId();

Log.d(TAG, "device IMEI is " + myIMEI);

if (teleMgr.getSimState() == TelephonyManager.SIM_STATE_READY) {

String country = teleMgr.getSimCountryIso();

Log.d(TAG, "SIM country ISO is " + country);

}

}

// Only unregister the listener if this app is going away.

// Otherwise, when the user tries to make or receive a phone

// call, this app will get paused and we don't want to stop

// listening when we're put into the background.

@Override

public void onDestroy() {

super.onDestroy();

Log.d(TAG, "In onDestroy");

teleMgr.listen(myListener, PhoneStateListener.LISTEN_NONE);

}

public void doClick(View target) {

switch(target.getId()) {

case R.id.callBtn:

Intent intent = new Intent(Intent.ACTION_VIEW

Uri.parse("tel:5551212"));

startActivity(intent);

break;

case R.id.quitBtn:

finish();

break;

default:

break;

}

}

public class MyPhoneStateListener extends PhoneStateListener

{

@Override

public void onCallStateChanged(int state, String incomingNumber){

super.onCallStateChanged(state, incomingNumber);

switch(state)

{

case TelephonyManager.CALL_STATE_IDLE:

Log.d(TAG, "call state idle...incoming number ["+

incomingNumber+"]");

break;

case TelephonyManager.CALL_STATE_RINGING:

Log.d(TAG, "call state ringing...incoming number ["+

incomingNumber+"]");

break;

case TelephonyManager.CALL_STATE_OFFHOOK:

Log.d(TAG, "call state offhook...incoming number ["+

incomingNumber+"]");

break;

default:

Log.d(TAG, "call state ["+state+"]");

break;

}

}

}

}

使用电话管理器时,请确保将android.permission.READ_PHONE_STATE权限添加到您的清单文件中,以便您可以访问电话状态信息。如清单 7-7 所示,通过实现一个PhoneStateListener并调用TelephonyManagerlisten()方法,您可以得到关于电话状态变化的通知。当一个电话来了,或者电话状态改变了,系统会用新的状态调用你的PhoneStateListeneronCallStateChanged()方法。当您尝试这样做时,您将会看到,来电号码仅在状态为CALL_STATE_RINGING时可用。在本例中,您向 LogCat 写入了一条消息,但是您的应用可以在它的位置实现定制的业务逻辑,比如暂停音频或视频的回放。

Note

如果你的应用由于电话(或通知或警报)需要改变音量,你应该研究 Android 的音频聚焦功能集。本书不涉及音频聚焦。

为了模拟来电,您可以使用 Eclipse 的 Emulator Control UI——与您用来发送 SMS 消息的 UI 相同(见图 7-2)——但是选择语音而不是 SMS。

请注意,我们在onDestroy()中告诉TelephonyManager停止向我们发送更新。当你的活动结束时,关闭信息总是很重要的。否则,TelephonyManager可能会保留对您的对象的引用,并阻止它在以后被清除。然而,当活动进入后台时,您仍然希望接收更新。

此示例仅处理可供监听的一种电话状态。查看其他人的PhoneStateListener文档,包括例如LISTEN_MESSAGE_WAITING_INDICATOR。当处理电话状态变化时,您可能还需要获取订户(用户)的电话号码。TelephonyManager.getLine1Number()会把那个还给你的。

你可能想知道是否有可能通过代码接听电话。不幸的是,目前 Android SDK 没有提供这样做的方法,尽管文档暗示您可以通过一个动作ACTION_ANSWER来激发一个意图。在实践中,这种方法还不可行,尽管您可能想检查一下自撰写本文以来是否已经解决了这个问题。似乎有一些黑客在一些设备上工作,但不是所有的。在网上搜索一下ACTION_ANSWER应该能找到它们。

类似地,您可能希望通过代码拨出电话。在这里,你会发现事情更容易。进行出站呼叫的最简单方法是通过如下代码调用拨号器应用:

Intent intent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:5551212"));

startActivity(intent);

注意,为了实际拨号,您的应用将需要android.permission.CALL_PHONE权限。否则,当您的应用试图调用拨号器应用时,您将得到一个SecurityException。要在没有此权限的情况下进行拨号,请将意向操作更改为Intent.ACTION_VIEW,这将导致拨号器应用显示您要拨打的号码,但用户需要按下发送按钮来发起呼叫。

检测手机状态变化的另一个方法是注册一个广播接收器来检测手机状态变化(android.intent.action.PHONE_STATE),类似于本章开头示例中接收 SMS 消息的方式。这可以在代码中完成,或者您可以在清单文件中指定一个标记。

会话发起协议(SIP)

Android 2.3 (Gingerbread)在 android.net.sip 包中引入了支持 SIP 的新特性。SIP 是互联网工程任务组(IETF)标准,用于协调通过网络连接发送语音和视频,以便在通话中将人们联系在一起。这项技术有时被称为 IP 语音(VoIP),但请注意,实现 VoIP 的方式不止一种。

例如,Skype 使用一种专有协议来实现 VoIP,它与 SIP 不兼容。SIP 也不等同于谷歌语音。Google Voice(在撰写本文时)并不直接支持 SIP,尽管有一些方法可以将 Google Voice 与 SIP 供应器集成在一起,以便将事情联系在一起。Google Voice 为您设置了一个新的电话号码,您可以用它来连接其他电话,如您的家庭电话、工作电话或手机。一些 SIP 供应器将生成一个可用于 Google Voice 的电话号码,但在这种情况下,Google Voice 并不真正知道该号码是用于 SIP 帐户的。搜索互联网会发现相当多的 SIP 供应器,许多有合理的通话费率,有些是免费的。

值得注意的是,SIP 标准没有解决通过网络传递音频和视频数据的问题。SIP 只涉及建立和拆除设备之间的直接连接,以允许音频和视频数据流动。客户端计算机程序使用 SIP 以及音频和视频编解码器和其他库来建立用户之间的呼叫。SIP 呼叫经常涉及的其他标准包括实时传输协议(RTP)、实时流协议(RTSP)和会话描述协议(SDP)。Android 3.1 带来了 Android 对 RTP 的直接支持,在 android.net.rtp 包中。RTSP 支持 MediaPlayer 已经有一段时间了,尽管不是所有的 RTSP 服务器都与 Android 的 MediaPlayer 兼容。SDP 是用于描述多媒体会话的应用级协议,因此您将看到 SDP 格式的消息内容。

用户可以在台式电脑上拨打 SIP 电话,而不会产生长途电话费。计算机程序可以很容易地在移动设备上运行,如 Android 智能手机或平板电脑。SIP 计算机程序通常被称为“软电话”移动设备上的软电话的真正优势是当设备使用 Wi-Fi 连接到互联网时,用户没有使用任何无线分钟,但仍然能够拨打或接听电话。在接收端,软电话必须已经向 SIP 供应器注册了它的位置和功能,以便供应器的 SIP 服务器可以响应 invite 请求来建立直接连接。例如,如果接收者的软电话不可用,SIP 服务器可以将呼入请求定向到语音邮件账户。

Google 为 SIP 提供了一个演示应用,叫做 SipDemo。现在让我们来研究一下这个应用,这样您就可以理解它是如何工作的了。如果你是 SIP 的新手,某些方面并不明显。如果你想尝试 SipDemo,你可能需要一个支持 SIP 的 Android 物理设备。这是因为在撰写本文时,Android 模拟器不支持 SIP(或 Wi-Fi)。互联网上有一些让 SIP 在模拟器中工作的尝试,当你读到这篇文章时,其中一些可能很容易实现并且很健壮。

要使用 SipDemo,您需要从 SIP 供应器处获得一个 SIP 帐户。您需要有您的 SIP ID、SIP 域名(或代理)和您的 SIP 密码。然后将它们插入 SipDemo 应用的首选项屏幕,供应用使用。最后,你需要一个从你的设备到互联网的 Wi-Fi 连接。然而,如果您不想在设备上实际试验 SipDemo,您应该仍然能够理解本节的其余部分。SipDemo 如图 7-3 所示。

A978-1-4302-4951-1_7_Fig3_HTML.jpg

图 7-3。

The SipDemo application with the menu showing

要将 SipDemo 作为新项目加载到 Eclipse 中,请使用 New Android Project 向导,单击 Android Sample 项目选项,在 Build Target 部分选择 Android 2.3 或更高版本,然后选择 SipDemo。单击 Finish,Eclipse 将为您创建新项目。您可以不做任何更改就运行这个项目,但如前所述,除非设备支持 SIP,启用 Wi-Fi,您在某个地方有一个 SIP 帐户,您使用菜单按钮编辑您的 SIP 信息,并且您使用菜单按钮发起呼叫,否则它不会做任何事情。您将需要一些其他 SIP 帐户来调用,以便测试应用。按下屏幕上的大麦克风图像,您就可以与对方通话。这个演示应用也可以接收来电。现在我们来谈谈 android.net.sip 包的内部工作原理。

android.net.sip 包有四个基本类:SipManager、SipProfile、SipSession 和 SipAudioCall。SipManager 是这个包的核心;它提供对 SIP 其余功能的访问。您调用 SipManager 的静态 newInstance()方法来获取 SipManager 对象。使用 SipManager 对象,您可以为大多数 SIP 活动获取 SipSession,或者为纯音频呼叫获取 SipAudioCall。这意味着谷歌在 android.net.sip 包中提供了超出标准 sip 所提供的功能——即建立音频通话的能力。

SipProfile 用于定义将相互通话的 SIP 帐户。这并不直接指向终端用户的设备,而是指向 SIP 供应器的 SIP 账户。服务器帮助完成建立实际连接的其余细节。

SipSession 是奇迹发生的地方。建立一个会话包括您的 SipProfile,以便您的应用可以让您的 SIP 供应器的服务器知道自己。您还通过了 SipSession。事件发生时将被通知的侦听器实例。一旦设置了 SipSession 对象,您的应用就可以调用另一个 SipProfile 或接收来电。侦听器有一系列回调,因此您的应用可以正确处理会话状态的变化。

截至蜂巢,最简单的就是使用 SipAudioCall。逻辑就是把麦克风和扬声器连接到数据流上,这样你就可以和对方进行对话。SipAudioCall 上有许多管理静音、保持等的方法。所有的音频片段也为你处理。

除此之外,你还有工作要做。SipSession 类具有用于发出出站调用的makeCall()方法。主要参数是会话描述(字符串形式)。这是需要做更多工作的地方。构建会话描述需要根据前面提到的会话描述协议(SDP)进行格式化。理解接收到的会话描述意味着根据 SDP 对其进行解析。SDP 的标准文档位于: http: / / tools。ietf。org/ html/ rfc4566 。不幸的是,Android SDK 不提供对 SDP 的任何支持。然而,感谢一些非常善良的人,有几个免费的 Android SIP 应用已经建立了这个功能。分别是 sipdroid ( http://code.google.com/p/sipdroid/ )和 csipsimple ( http://code.google.com/p/csipsimple/ )。

我们甚至还没有开始考虑管理 SIP 客户端之间的视频流的编解码器,尽管 sipdroid 有这个功能。SIP 的另一个非常吸引人的方面是能够在两个以上的人之间建立电话会议。这些主题超出了本书的范围,但是我们希望您能够理解 SIP 能为您做些什么。

请注意,SIP 应用至少需要android.permission.USE_SIPandroid.permission.INTERNET权限才能正常运行。如果您使用 SipAudioCall,您还需要android.permission.RECORD_AUDIO权限。假设你用的是 Wi-Fi,你应该加上android.permission.ACCESS_WIFI_STATEandroid.permission.WAKE_LOCK。将下面的标签作为<manifest>的子标签添加到您的AndroidManifest.xml文件中也是一个好主意,这样您的应用将只能安装在支持 SIP 的硬件设备上:

<uses-feature android:name="android.hardware.sip.voip" />

参考

这里有一些对您可能希望进一步探索的主题有帮助的参考。

摘要

本章讨论了 Android 电话 API。特别是:

You learned how to send and receive an SMS message.   You learned about SMS folders and reading SMS messages.   We covered the sending of e-mail from an application.   You learned about the TelephonyManager and how to detect an incoming call.   You saw how SIP can be used to create a VoIP client program.  

复习问题

你可以用下面的问题来巩固你对本章的理解。

Can an SMS message contain more than 140 characters?   True or false? You get an SmsManager instance by calling Context.getSystemService(MESSAGE_SERVICE).   Where is the ADT feature that allows you to send a test SMS message to an emulator?   Can an application send an e-mail without the user’s knowing?   Can an application send an SMS message without the user’s knowing?   Can an application make a phone call without the user’s knowing?   Is SIP the same as Skype?   What are the four main classes of the android.net.sip package?   Which SIP class defines the SIP accounts that will be talking to each other?   What tag do you put into the AndroidManifest.xml file to ensure a SIP app will be seen only by devices that support SIP?   What permissions are needed in order to make SIP work properly?

八、高级调试和分析

Abstract

在您学习 Android 的这个阶段,您可能已经掌握了一些应用,并且您可能已经遇到了一些来自它们的意外行为。这一章花了一些时间来探索调试应用的高级方法,这样你就可以深入了解应用的内部并发现发生了什么。本章还介绍了如何分析您的应用,以确保它尽可能好地执行,并帮助您确保它没有做它不应该做的事情。

在您学习 Android 的这个阶段,您可能已经掌握了一些应用,并且您可能已经遇到了一些来自它们的意外行为。这一章花了一些时间来探索调试应用的高级方法,这样你就可以深入了解应用的内部并发现发生了什么。本章还介绍了如何分析您的应用,以确保它尽可能好地执行,并帮助您确保它没有做它不应该做的事情。

您将在这里探索各种工具和功能,许多都在 Eclipse 中,并带有用于 Eclipse 的 Android Developer Tools (ADT)插件。您将了解到在哪里可以找到这些工具以及它们是如何工作的。在这个过程中,您将使用一些故意写得很差的示例应用来演示这些工具如何发现问题。

Eclipse Debug 透视图是 Eclipse 附带的标准透视图,它并不特定于 Android 编程。然而,你很快就会知道用它能做什么。Android dal vik Debug Monitor Server(DDMS)透视图有很多非常有用的功能来帮助您调试应用。其中包括设备视图(用于查看您连接到的设备)、仿真器控制(用于发送电话呼叫、SMS 消息和 GPS 坐标)、文件浏览器(用于查看/传输设备上的文件)、网络统计(用于查看进出设备的流量)、线程、堆、分配跟踪器(用于查看您的应用内部)、systrace(用于查看您的 Android 设备内部)和 OpenGL tracer。您还将了解 Traceview,它使分析来自应用的转储文件变得更加容易。

本章还深入到了层次视图的视角,因此您可以遍历正在运行的应用的实际视图结构。最后,您覆盖了StrictMode类,该类可用于捕获违反策略的行为,以捕捉可能导致糟糕用户体验的设计错误。

启用高级调试

当您在模拟器中测试时,Eclipse Android Developer Tools(ADT)插件会负责为您设置一切,以便您可以使用即将看到的所有工具。

关于在真实设备上调试应用,您需要知道两件事。首先,应用必须设置为可调试的。这包括将android:debuggable="true"添加到AndroidManifest.xml文件中的<application>标签中。幸运的是,ADT 正确地设置了这一点,所以您不必这样做。当您为模拟器创建调试版本或者直接从 Eclipse 部署到设备时,这个属性被 ADT 设置为true。当您导出应用来创建它的生产版本时,ADT 会自动将debuggable设置为false。请注意,如果您在AndroidManifest.xml中自行设置,无论如何它都会保持设置。

第二件要知道的事情是,设备必须进入 USB 调试模式。要找到 Gingerbread 之前的 Android 版本的设置,请转到设备的设置屏幕,选择应用,然后选择开发。确保选择了“启用 USB 调试”。

在较新版本的 Android(冰激凌三明治和更高版本)上,进入设置,选择开发者选项,并启用 USB 调试。如果你没有看到开发者选项,你必须做一个简单的技巧来取消隐藏它们。从设置列表中,选取“关于电话”,然后向下滚动,直到看到内部版本号。快速重复点击这个按钮——七次应该就可以了——你会得到一条消息,告诉你现在是开发者了,菜单选项会出现在设置中。

Easter Egg Note

如果你运行的是软糖豆,要获得更多乐趣,在“关于手机”下找到 Android 版本条目,快速点击多次,直到你看到一个大大的微笑软糖豆。按住果冻豆,直到你看到一片漂浮的豆子。触摸并投掷这些豆子。按“后退”按钮退出。

调试视角

尽管 LogCat 对于查看日志消息非常有用,但是您肯定希望在应用运行时获得更多控制和更多信息。在 Eclipse 中调试相当容易,在互联网上的很多地方都有详细的描述。因此,这一章不会详细介绍 Eclipse,但是这些是您可以获得的一些有用的特性:

  • 在代码中设置断点,以便在应用运行时执行在断点处停止
  • 检查变量
  • 单步执行并进入代码行
  • 将调试器附加到已经运行的应用
  • 断开与您连接的应用的连接
  • 查看堆栈跟踪
  • 查看线程列表
  • 查看日志目录

图 8-1 显示了一个示例屏幕布局,展示了您可以使用 Debug 透视图做些什么。

A978-1-4302-4951-1_8_Fig1_HTML.jpg

图 8-1。

The Debug perspective

您可以从 Java 透视图(您编写代码的地方)开始调试应用,方法是右键单击项目并选择 debug as➤Android application;这将启动应用。您也可以从“运行”菜单中选择“调试”,从工具栏中选择“调试”,或者使用键盘快捷键 F11。您可能需要切换到 Debug 透视图来进行调试。

DDMS 的视角

DDMS 代表达尔维克调试监控服务器。这个透视图使您能够深入了解仿真器或设备上运行的应用,允许您观察线程和内存,并在应用运行时收集统计信息。图 8-2 显示了它在您的工作站上的外观。请注意,当本节使用术语“设备”时,它指的是设备或仿真器。

或者,您可以导航到 Android SDK 目录,在工具下找到 monitor 程序。启动它会产生与您在 Eclipse 中看到的相同的 DDMS 窗口。

A978-1-4302-4951-1_8_Fig2_HTML.jpg

图 8-2。

The DDMS perspective

在图 8-2 的左上角,注意设备视图。这将向您显示连接到工作站的所有设备(您可以同时连接多个设备或仿真器),如果您展开视图,还会显示可用于调试的所有应用。在图 8-2 中,您正在查看一个仿真器,因此股票应用看起来可供调试。在真实设备上,您可能只看到少数应用(如果有的话)。不要忘记,如果你正在一个真实的设备上调试一个生产应用,你可能需要调整AndroidManifest.xml文件来设置android:debuggabletrue

Devices 视图中的按钮用于开始调试应用、更新堆、转储堆和 CPU 性能分析代理(HPROF)文件、进行垃圾收集(GC)、更新线程列表、启动方法性能分析、停止进程、拍摄设备屏幕、分析视图层次结构以及生成系统跟踪或捕获 OpenGL 跟踪。按钮如图 8-3 所示。让我们从左到右更详细地了解每一项。除了摄像头和 systrace 按钮之外,所有这些按钮都适用于在设备视图列表中选择的任何应用。如果您没有看到任何列出的应用,您可能需要点按设备名称旁边的+号。或者您可能需要将应用设置为可调试的,如上所述。

A978-1-4302-4951-1_8_Fig3_HTML.jpg

图 8-3。

The DDMS advanced debugging buttons

调试按钮

绿色的小 bug 按钮开始调试所选的应用。单击它会将您带到刚才描述的 Debug 透视图。这个选项的好处是您可以将调试器附加到正在运行的应用上。您可以让应用进入您希望开始调试的状态,选择它,然后单击此按钮。然后,当您继续运行应用时,断点将导致执行停止,您可以检查变量并单步执行代码。

调试 Android 应用与调试 Eclipse 中的任何其他应用没有什么不同。如前所述,您可以设置断点、检查变量以及单步执行代码。本章不会深入讨论这些细节,但是互联网上有很多资源可以学习更多关于 Eclipse 中调试的知识。

堆按钮

接下来的三个按钮用于分析正在运行的进程的内存堆。您希望您的应用使用尽可能少的内存,并且不要过于频繁地分配内存。与使用 Debug 按钮类似,您选择想要检查的应用,然后单击 Update Heap 按钮(这三个按钮中的第一个)。您应该只选择您正在积极调试的应用。在图 8-2 右侧的堆视图选项卡中,您可以点击原因 GC 按钮来收集关于堆中内存的信息。将显示摘要结果,详细结果如下。然后,对于每种类型和大小的已分配内存,您可以看到有关内存使用情况的更多详细信息。

您将在此视图中寻找的一些内容包括:

  • 大对象的高计数:您可能会有几个大小在几 KB 范围内的对象;这很正常。如果你看到许多大对象,这可能意味着你的代码正在一遍又一遍地重新创建一些大对象;这可能是个问题。选择统计列表中的每一行,并查看下图。如果你在图的右边看到一个高的长条,那表示有很多大的物体。
  • 一些非常大的对象:一般来说,移动应用应该避免创建这些对象,因为内存是一种宝贵的资源。标有“最大”的一栏会告诉你最大的物体是什么,接下来你会看到如何找到它的来源。
  • 任何大小的对象都有很高的计数:避免应用垃圾收集暂停的最好方法是首先不要创建大量垃圾。如果您的应用在几秒钟内创建数 MB 的对象,您的用户将会遇到 GC 暂停,这会影响用户体验。使用“计数”列或图表来标识出现频率非常高的任何类型的对象。

“转储 HPROF 文件”按钮就是这样做的:它给你一个 HPROF 文件。如果您已经安装了 Eclipse Memory Analyzer (MAT)插件,那么这个文件将被处理并显示结果。这可能是查找内存泄漏的一种强有力的方法。默认情况下,HPROF 文件是在 Eclipse 中打开的,但是如果没有 MAT 插件,它不会有很大帮助。有关该插件的更多信息,请参见本章末尾的参考资料。根据您工作站的电源,此操作可能需要一分钟或更长时间,因此如果看起来什么也没发生,请耐心等待。在 Android ➤ DDMS 下有一个偏好设置,你可以选择保存到一个文件。

MAT 将报告应用中对象的内存使用情况。对于您在上面的堆中可能已经看到的任何问题,该工具可以更深入地挖掘,以确定问题对象来自哪里。例如,从 MAT 视图的 Overview 选项卡中,如果您单击 Top Consumers 链接,您将看到占用大部分堆的对象。相反,如果您单击泄漏可疑点,您将看到可能泄漏内存的对象。这个报告甚至可以显示对象引用名,以帮助定位分配内存的代码。

线程按钮

“更新线程”按钮用所选应用的当前线程集填充右侧的“线程”选项卡。这是观察线程创建和销毁的好方法,也是了解应用中线程级别发生了什么的好方法。在线程列表下面,您可以通过跟踪看起来像堆栈跟踪的内容(对象、源代码文件引用和行号)来查看线程的位置。

例如,谷歌地图应用使用了大量线程,你可以通过在设备视图中选择应用,然后点击更新线程按钮来观察它们的来去。在右侧的线程视图中,当 Maps 与各种 Google Maps 服务对话时,您会看到许多线程。双击其中一个线程条目会在下面的列表中显示详细信息。

下一个按钮 Start Method Profiling 允许您收集应用中方法的信息,包括调用次数和计时信息。单击该按钮,与应用进行交互,然后再次单击该按钮(它在开始和停止方法分析之间切换)。当您单击 Stop Method Profiling 时,Eclipse 将切换到 Traceview 视图,这将在本章的下一节中介绍。与 HPROF 转储一样,打开 Traceview 视图可能需要一分钟或更长时间,这取决于您的工作站的能力,因此如果 Eclipse 似乎没有做任何事情,请耐心等待。

停止按钮

“停止”按钮(看起来像停止标志)允许您停止选定的进程。这是一个硬应用停止—它不像单击后退按钮,后者只影响一个活动。在这种情况下,整个应用都会消失。

相机按钮

无论在设备视图中选择了哪个应用,看起来像照相机的按钮都会捕捉设备屏幕的当前状态。然后,您可以刷新图像、旋转图像、保存图像或复制图像。“保存”选项仅使用 PNG 格式,但如果您单击“复制”按钮,则可以粘贴到其他工具(例如,“绘画”)中,并以该工具使用的任何格式保存。

“转储视图层次结构”按钮

此按钮(官方称为 UI Automator 的转储视图层次)作用于所选的应用,以捕捉当前屏幕和屏幕上的所有视图,无论是否可见。一旦捕获,将显示视图层次视图,类似于图 8-4 。

A978-1-4302-4951-1_8_Fig4_HTML.jpg

图 8-4。

The View Hierarchy view

这个特性的目的是提供使用 UI Automator 创建自动化 UI 测试所需的信息。右上角的窗口显示了左侧截图中所有视图的层次结构。记住,屏幕上的所有东西都是视图对象,包括文本和按钮。对于选定的布局或视图,详细信息显示在右下窗口中。

该工具与本章后面讨论的视图层次透视图类似,但有所不同。这个视图简单地向您展示了视图及其层次结构。稍后描述的工具为您提供了更多关于视图呈现的信息。

系统跟踪按钮

系统跟踪按钮启动一个对话框,您可以设置从仪器捕获系统跟踪(系统跟踪)的参数。系统跟踪许多系统进程以及您的程序的方法调用。它用于在非常低的级别识别可能导致用户体验问题的问题,例如响应延迟。它允许您选择问题中可能涉及的流程,并轻松地比较方法调用时间,以查看什么影响什么,甚至跨流程边界—例如,当使用服务时。systrace 的输出是一个 HTML 文件,您可以将其加载到浏览器(Chrome 首选)中进行分析。

在这种特定情况下,模拟器不能用于获取系统跟踪,您必须有一个物理设备。该设备必须运行 Android 4.1 或更高版本,并且必须能够捕获系统跟踪。谷歌 Nexus 设备已启用,三星 Galaxy S3 也是如此。要查看您的设备是否能够运行,请检查其文件系统,查看/sys/kernel/debug 目录是否存在。如果它确实存在,那么你就可以走了。systrace 的某些功能需要设备的 root 访问权限,而设备制造商可能不支持其他功能。你会得到错误信息,告诉你什么是不支持的,所以你可能会在某种程度上受到限制,你可以做什么与您的特定设备。

设置跟踪

在开始跟踪之前,您必须在设备上做一些准备工作。首先,设备必须连接到您的工作站,并启用 USB 调试,如上所述。在“设置”的“开发人员选项”部分,您还会发现一个名为“启用跟踪”的条目。在这里选择你想追踪的东西。单击 OK,准备运行应用进行捕获。

现在,将你的注意力转向你的工作站和当你点击 Systrace 按钮时弹出的对话框,如图 8-5 所示。在捕获之前或之后(或两者都有),您可能希望在设备上运行 ps 命令并捕获输出。以下内容在您的工作站上运行良好:

adb shell ps > c:\temp\ps.txt

使用任何适合您平台的输出文件名。这将捕获在分析跟踪信息时有用的活动进程列表。

A978-1-4302-4951-1_8_Fig5_HTML.jpg

图 8-5。

The Systrace dialog

前两个字段不言自明。为了捕获所有数据,跟踪缓冲区大小(kb)字段需要相当大。在这个例子中,缓冲区被设置为 10MB,这似乎工作得很好。如果您发现您的采集被截断,您可以选取更大的尺寸。

前四个跟踪事件选项显然与 CPU 活动有关,这是您希望看到的。并非所有设备都支持所有这些事件。如果您收到与这些事件类型之一相关的错误消息,只需在此对话框中取消选择它,然后重试。对于需要 root 的跟踪事件,您需要使用命令adb root重启 adbd。当然,这只在你的设备安装了不安全的boot.img后才有效。

跟踪标记与设备上设置➤开发人员选项➤启用跟踪下的选项相匹配。除了 am(活动管理器)和 wm(窗口管理器)之外,大多数标签都是不言自明的。您不需要在对话框中选择您在设备上启用的所有标签,但是对于您在对话框中选择的任何标签,您必须已经在设备上选择了相应的启用跟踪设置选项。您不需要按照说明执行 adb shell 命令,除非出于某种原因,您没有得到有效的 trace.html 文件作为输出。当您单击“确定”时,跟踪将开始,一个对话框将出现在您的工作站上,您应该开始在设备上运行您的应用,以便您可以捕获所有事件的详细信息。一旦捕获完成,您将有一个大的 HTML 文件加载到 Chrome 浏览器中。当你读到这篇文章的时候,其他浏览器如 IE、Firefox 或 Safari 可能还能用,但你最好的选择是 Chrome。

如果你冒险进入 Android 开发者网站,阅读了那里关于下载 python 来让 systrace 工作的说明,那就算了。从版本 21 开始,ADT 中的代码使得 systrace 不需要 python。事实上,让基于 python 的 systrace 工作存在很多问题。您最好使用 ADT 功能。

分析痕迹

一旦将捕获的跟踪文件加载到 Chrome 中,您将看到一个类似图 8-6 的屏幕。在左侧,您将看到已经捕获的事件类型、活动等。使用右侧的滚动条查看捕获的所有内容。在图形窗口的顶部,您会看到一个时间刻度。使用 w 和 s 键可以扩大或缩小时间范围。z 键将时间刻度恢复到原始状态。图形窗口显示左侧所示线程每次执行的行。a 和 d 键分别向左和向右移动线条。

A978-1-4302-4951-1_8_Fig6_HTML.jpg

图 8-6。

A systrace capture file in Chrome

毫无疑问,你已经发现,有太多的东西要同时看。你需要一些路标。幸运的是,您可以单击时间刻度来添加垂直线,向下延伸到所有行。这允许您关注特定的时间片并比较线程。如果你愿意,你可以有两条以上的垂直线;您可以再次单击某一行来删除它;你可以点击并拖动一条线来移动它。您也可以单击最左侧的 x 来删除您认为不重要的行。数据仍然保存在 systrace 文件中,所以如果需要的话,可以随时重新加载。

放大图形窗口中的线,直到看到一些宽度。现在点击一条线。您将在窗口的下部看到有关该事件时间的一些详细信息。点击另一行,你会看到它的细节。现在,单击并拖动一个方框,将一串线包围起来。当您拖动时,您将看到该框的时间片持续时间,当您放开时,该框中的事件将在窗口的下部汇总。这是一种非常强大的方式来查看设备上发生的一切。

要看的几个关键行是 SurfaceFlinger 和它的第二线程 surfaceflinger。如果设备上一切顺利,SurfaceFlinger 将看起来非常规则,非常短的线条均匀分布。这确保了用户体验的快速响应。事实上,所有的应用都应该表现出这种行为——也就是说,应用中的处理应该简短而有规律。任何时候你的应用处理一个事件(一个用户点击,接收一个广播),它应该快速地做它能做的任何事情,并确保主线程没有被阻塞。如果你看到 SurfaceFlinger 线路中断,这意味着有其他东西占用了设备的资源,并可能导致用户体验不稳定。有了这个 systrace 工具,您可以准确地跟踪坏事情发生的地方,并可能采取一些措施来解决问题。

对 Android 底层架构的完整解释超出了本章的范围,但是您可能有兴趣了解事件源列表中的一些项目。VSYNC 是垂直同步,Android 使用它来管理显示缓冲区。绑定器是数据在进程间传递的方式,包括传递到图形服务器。

OpenGL ES 的启动 OpenGL 跟踪按钮/跟踪器

此按钮启动一个对话框,允许您捕捉 OpenGL 帧进行分析。或者,至少应该是这样。使用这种方法获得 OpenGL 跟踪功能存在问题。使用 Tracer for OpenGL ES 透视图可能会更成功。使用窗口菜单查找并启动该透视图。然后使用这里的开始跟踪按钮(看起来和 DDMS 屏幕上的开始 OpenGL 跟踪按钮一样)来得到如图 8-7 所示的对话框。

A978-1-4302-4951-1_8_Fig7_HTML.jpg

图 8-7。

An OpenGL trace dialog

该设备可能会默认为你。对于应用包,输入要跟踪的活动包的名称。对于要启动的活动,请输入活动的名称。如果在活动名称上出现错误,请尝试在活动名称前面加上一个句点(。).选择收集选项并提供文件名—通常以. gltrace 结尾。准备好后按 trace 按钮。将出现一个新的对话窗口,显示帧捕获的进度。当你收集到你想要的东西时,按停止追踪按钮。

然后,透视图会发生变化,以显示帧捕获的结果。您可以使用这个屏幕来查看 OpenGL 的工作情况。如果需要,请单击“打开保存的 OpenGL 跟踪文件”按钮,以加载您在上一个对话框中指定的文件。使用帧滑块选择一个帧,然后查看 OpenGL ES 调用。绘图命令将以蓝色突出显示。

设备视图菜单

最后,菜单包括所有的按钮功能;此外,还有一个复位 adb 菜单项。Adb 是 Android Debug Bridge,一个在您的工作站上运行的服务器进程,用于与您工作站上的仿真器或连接到它的设备进行对话。重置 adb 选项重新启动 adb 服务器,以防出现不同步,您无法再看到设备或仿真器。这将(实际上)刷新视图中的设备列表。重置 adb 服务器的另一种方法是在工具窗口中使用以下一对命令:

adb kill-server

adb start-server

分配跟踪器

图 8-8 显示了分配跟踪器选项卡。这使您可以开始跟踪单个内存分配。单击“开始跟踪”后,练习您的应用,然后单击“获取分配”。将显示该时间段内的内存分配列表,您可以单击特定的分配来查看其来源(类、方法、源代码文件引用和行号)。停止跟踪按钮在那里,所以你可以重置和重新开始。

A978-1-4302-4951-1_8_Fig8_HTML.jpg

图 8-8。

The Allocation Tracker view

可以单击分配列表的列,按该列进行排序。例如,这使得寻找大的分配变得容易。还有一个过滤字段,可以很容易地将显示的行限制为包含该字段中输入的文本的行。通过在应用的包名的开头键入,列表将会缩小,以显示从您的代码中进行的分配。然后,您可以按“分配于”列进行排序,并查找重复分配的同一类对象的多次出现。在所示的例子中,Paint 类的重复实例化可能是一个问题。我们稍后将对此进行更详细的探讨。

特蕾西

您已经看到了如何收集应用中方法执行的统计信息。使用 DDMS,您可以执行方法分析,之后 Traceview 窗口会显示结果。图 8-9 显示了这种情况。

A978-1-4302-4951-1_8_Fig9_HTML.jpg

图 8-9。

Traceview

使用前面展示的技术来启动这个视图,您将获得应用中在 DDMS 捕获方法调用信息时执行的所有方法的结果。您在捕获方法时对应用的练习越多,您在这个视图中获得的信息就越多。一种方法是捕获特定应用操作的方法,这样您就可以专注于这段时间内发生的事情。如果您有一个具有大量功能的大型应用,这种方法可能需要很长时间。在这种情况下,您可能希望测试更长的时间,尽管这样您将有大量的数据要处理,这可能会隐藏一些不太严重的问题。

从图 8-9 中可以看出,应用中每个线程的活动都以图形方式显示,让您知道哪些线程正在工作以及何时工作。如果您将鼠标放在线程行上的每个条上,您将会看到上面关于进行了哪个方法调用的信息,以及 CPU 计时。

请注意,分析的结果显示了调用的内容、频率以及每个方法花费的时间。细分是按线程,用颜色编码。可以单击此处的列,根据该值对结果进行排序。

查找问题的一个好方法是按降序排列包含 Cpu 时间百分比(即最大值在顶部)。这将表明时间在你的应用中的花费,从多到少。但是每个值都包括从每个方法内部调用方法所花费的时间。通过查看具有这种排序顺序的前几十行,您通常可以发现一些意外的方法是否占用了太多的总时间。当您捕获方法调用时,您应该对应用应该在做什么有所了解,如果时间花在了不应该花的地方,那么您可以去调查一下。

使用这种排序顺序要注意的另一件事是靠近顶部的 Excl Cpu Time %中的一个大值。该值表示仅在方法代码中花费的时间,不包括在此方法的方法调用中花费的时间。这是在这种特定方法中花费时间的更真实的度量,因此高值表明该方法正在做大量的工作。您需要判断该方法是否应该做大量的工作。如果用这种方法花费的时间看起来很长,去看看并找出原因。当然,您可以直接按该列进行排序,并查看使用最多 CPU 时间的方法是否是您所期望的方法。

您可以单击一个方法调用的行,它将显示对此方法的父调用、对此方法的方法的子调用,以及每个调用的计时。您可以单击父方法或子方法,将视图切换到该方法。当然,单击一个子方法将会在子方法的父方法列表中显示当前的方法。或者点击一个父方法将会在父方法的子方法列表中显示当前的方法。通过这种方式,您可以上下遍历应用方法调用树,以查看时间都花在了哪里。

您还可以通过使用android.os.Debug类获得 Android 应用的更具体的跟踪信息,该类提供了一个开始跟踪方法(Debug.startMethodTracing("basename"))和一个停止跟踪方法(Debug.stopMethodTracing())。Android 在设备的 SD 卡上创建一个跟踪文件,文件名为basename.trace,尽管你可以指定一个完整的路径名,该文件将转到那里。您将开始和停止代码放在想要跟踪的内容周围,从而限制了收集到跟踪文件中的数据量。然后,您可以将跟踪文件复制到您的工作站,并使用 Android SDK 工具目录中包含的traceview工具查看跟踪输出,跟踪文件名作为traceview的唯一参数。

Note

当将方法捕获到跟踪文件中时,会有一些额外的开销影响应用方法的计时。时间不应该被认为是绝对准确的,而是相对的。没有追踪,一切都会进行得更快;但是无论是否开启追踪,最快的部分很可能是最快的。在跟踪开启的情况下,最慢的部分相对于其他部分仍然是最慢的。

测试您的调试技能

现在您已经了解了调试工具,下面是一个测试,看看您是否可以调试一个存在已知问题的应用。从本书的网站( www.androidbook.com/expertandroid/projects )下载第八章的示例程序 NoteBad,使用名为 expert Android _ Ch08 _ debugging . zip 的文件。这是 Android 示例应用记事本,但在投入生产之前需要进行一些调整。将这个项目导入 Eclipse,然后在真实设备或模拟器上运行它。使用菜单按钮和添加笔记菜单选项在应用中创建一些笔记。“后退”按钮保存当前便笺并使用户返回便笺列表。您可能会注意到,也可能没有注意到这个应用有些迟缓;没关系,您将使用工具来查找性能问题的原因。

单击 Start Method Profiling 按钮开始捕获方法调用。现在,通过编辑您创建的笔记来练习应用。单击 Stop Method Profiling 按钮,等一会儿,查看出现的 Traceview 视图。双击。跟踪标签,如果需要的话,让它充满窗口。现在看看列表中的方法调用。看到什么异常了吗?比如从 note editor linededittext.ondrawTextView.onDraw的方法调用所花费的时间大幅下降?您是否注意到noteeditorlinededittext . ondraw 到 TextView.onDraw 的方法调用所花费的时间大幅下降?您是否注意到 note editor linededittext . ondraw 的 Excl Cpu 时间%明显大于它周围的任何东西?这个方法绝对需要研究。

如果单击列表中的 note editor $ linededittext . ondraw 行,它将展开以显示父母和子女。孩子之下是自我,占据了大部分时间。这意味着 onDraw 方法中的代码做了大量的处理,而不是在 onDraw 调用的方法中。打开 NoteEditor.java 文件并导航到 onDraw 方法。您是否看到了导致严重延迟的内部 for 循环?如果你去掉这个没有意义的小循环,再试一次,你会发现在这个方法上花费的时间大大减少了。

现在,在编辑已经创建的另一个注释时,使用 Allocation Tracker 视图来捕获应用的一些分配事件。一旦您点击 Get Allocations 按钮,您将看到列表中显示了相当多的行。要将列表限制为仅来自代码的分配,请在筛选器字段中键入 com.androidbook。当你输入的时候,你会看到列表缩小到只有包含 com.androidbook 的行。太奇怪了!为什么要一次又一次地分配一个绘制对象?

单击其中一个分配行,您会看到下面显示该分配来自何处的跟踪。在 NoteEditor.java 双击 onDraw 方法的第一行,Eclipse 会直接带您找到代码。onDraw 方法每秒被调用多次,每次都分配一个新的 Paint 对象是没有意义的。如果你在 NoteEditor.java 向上滚动一点,你会看到真正的绘制对象分配应该在哪里。在 onDraw 方法中,您应该引用现有的对象,而不是每次都创建一个新的对象。继续重构代码以引用 mPaint,而不是在 onDraw 中创建新的 MP aint。现在,当您重新运行应用时,您甚至可能会注意到应用性能的显著提高。

层次视图视角

在 Hierarchy View 透视图中,您在模拟器中(而不是在真实设备上)连接到应用的运行实例。然后,您可以探索应用的视图、它们的结构以及它们的属性。首先,选择您想要的应用。选中并读取后,视图层次以多种方式显示,如图 8-10 所示。

A978-1-4302-4951-1_8_Fig10_HTML.jpg

图 8-10。

The Hierarchy View perspective

您可以在结构中导航,检查属性并确保您没有多余的视图。例如,如果你有许多嵌套的布局,你可以用一个RelativeLayout来代替它们。

当您自己尝试时,您可能会注意到中间窗口视图中的三个彩球。这些(从左到右)对应于查看者在测量、布局和绘制视图(包括封闭视图)方面的表现等级。颜色是相对的,所以红色的球不一定意味着有问题,但它肯定意味着你应该调查。

另请注意所选视图及其上方的信息。它不仅包括该视图图像的捕获,还显示了测量、布局和绘制该视图的绝对时间。这些都是有价值的数据,可以帮助你确定是否真的需要深入了解这个观点并做出改进。除了如前所述的折叠布局,您还可以改变视图的初始化方式和绘制工作量。如果您的代码创建了大量的对象,您可以重用对象来避免这种开销。使用后台线程、加载程序或其他技术来完成可能需要很长时间的工作。

像素完美视图

与设备视图中的层次视图和相机按钮类似,您可以获取当前屏幕图像并将其显示在像素完美视图中。这个 Eclipse 插件为您提供了一个放大的图像查看器,允许您查看单个像素及其相关的颜色。这个特性的有趣之处在于,你可以覆盖另一个图形(比如屏幕模型)并将其与当前屏幕进行比较。如果你需要复制一个特定的外观,这是一个很好的方法来看看你做得如何。

亚行司令部

您可以从命令行(或工具窗口)使用其他几个调试工具。Android Debug Bridge ( adb)命令允许您安装、更新和删除应用。它位于 Android SDK 目录中的 platform-tools 下。您可以在模拟器或设备上启动一个 shell,并从那里运行 Android 提供的 Linux 命令子集。例如,您可以浏览文件系统、列出进程、读取日志,甚至连接到 SQLite 数据库并执行 SQL 命令。例如,以下命令(在工具窗口中)在模拟器上创建一个外壳:

adb –e shell

注意-e来指定一个仿真器。如果您正在连接设备,请使用-d。在仿真器 shell 中,您拥有提升的 Linux 特权,而在真实设备上则没有(除非您已经对其进行了根操作)。这意味着您可以在模拟器中浏览 SQLite 数据库,但是您不能在真实的设备上这样做,即使它是您的应用!

键入不带参数的adb显示了adb命令的所有可用功能。

当您以这种方式连接时,通过 adb 的普通 shell 的限制是由于默认的 userid。在幕后,Android 使用一个 Linux 变种,所以最终的用户 id 是 root。有很多在 Android 设备上获得 root 的技术,如果你能在自己的设备上获得,你会发现有很多可能性。例如,使用 Android 设备的 root 访问权限,您将能够检查设备上的任何和所有 SQLite 数据库。因为获取 root 的技术对于每种设备都是不同的,而且事实上由于制造商试图消除获取 root 的能力而不断变化,所以本书不会详细介绍如何为您的设备设置 root。请注意,试图获取 root 可能会损坏您的设备,使其无法使用。

模拟器控制台

另一种强大的调试技术是运行模拟器控制台,它显然只适用于模拟器。要在模拟器启动并运行后开始,请在工具窗口中键入以下内容:

telnet localhost port#

其中port#是仿真器监听的位置。端口号通常显示在模拟器窗口标题中,通常是一个值,如 5554。模拟器控制台启动后,您可以键入命令来模拟 GPS 事件、SMS 消息,甚至电池和网络状态的变化。有关模拟器控制台命令及其用法的链接,请参见本章末尾的参考资料。

严格模式

Android 2.3 引入了一个名为StrictMode的新调试功能,据谷歌称,该功能用于对 Android 可用的谷歌应用进行数百次改进。那么它是做什么的呢?它报告违反与线程和虚拟机相关的策略的情况。如果检测到违反策略的情况,您会收到一个警报,其中包括一个堆栈跟踪,显示发生违规时您的应用所在的位置。您可以使用警报强制崩溃,也可以记录警报并让您的应用继续运行。

严格模式策略

StrictMode目前提供两种类型的策略。第一个策略与线程相关,主要针对主线程(也称为 UI 线程)运行。从主线程读取和写入磁盘不是好的做法,从主线程执行网络访问也不是好的做法。Google 在磁盘和网络代码中加入了StrictMode钩子;如果您为您的一个线程启用了StrictMode,并且该线程执行磁盘或网络访问,那么您会得到警告。您可以选择您想要警告ThreadPolicy的哪些方面,并且您可以选择警告方法。

您可以查找的一些违规包括自定义慢速调用、磁盘读取、磁盘写入和网络访问。对于警报,您可以选择写入 LogCat、显示对话框、闪烁屏幕、写入 DropBox 日志文件或使应用崩溃。最常见的选择是写入 LogCat 并使应用崩溃。清单 8-1 展示了一个为线程策略设置StrictMode的例子。

清单 8-1。设置StrictModeThreadPolicy

StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()

.detectDiskReads()

.detectDiskWrites()

.detectNetwork()

.penaltyLog()

.build());

注意,Builder类使得设置StrictMode变得很容易。定义策略的Builder方法都返回一个对Builder对象的引用,所以这些方法可以链接在一起,如清单 8-1 所示。最后一个方法调用build(),返回一个ThreadPolicy对象,它是StrictModesetThreadPolicy()方法所期望的参数。注意setThreadPolicy()是一个静态方法,所以不需要实例化一个StrictMode对象。

在内部,setThreadPolicy()使用策略的当前线程,因此根据ThreadPolicy评估后续线程操作,并在必要时发出警报。在这个示例代码中,策略被定义为通过向 LogCat 发送消息来警告磁盘读取、磁盘写入和网络访问。您可以使用detectAll()方法来代替特定的检测方法。您还可以使用不同的或附加的惩罚方法。例如,您可以使用penaltyDeath()让应用在将StrictMode警告消息写入 LogCat(作为penaltyLog()方法调用的结果)后崩溃。

因为您在一个线程上启用了StrictMode,一旦您启用了它,您就不需要一直启用它。因此,您可以在主活动的onCreate()方法开始时启用StrictMode,该方法在主线程上运行,然后它将为该主线程上发生的所有事情启用。根据您想要查找的违规类型,第一个活动可能会很快发生,足以启用StrictMode。您也可以通过扩展Application类并向应用的onCreate()方法添加StrictMode设置来在您的应用中启用它。可以想象,任何在线程上运行的东西都可以设置StrictMode,但是你当然不需要从任何地方调用设置代码;一次就够了。

ThreadPolicy类似,StrictMode也有一个VmPolicyVmPolicy可以检查几种不同类型的内存泄漏。一个VmPolicy通过一个类似的Builder类创建,如清单 8-2 所示。VmPolicyThreadPolicy的一个区别是VmPolicy不能通过对话框报警。

清单 8-2。设置StrictModeVmPolicy

StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()

.detectActivityLeaks()

.detectLeakedClosableObjects()

.detectLeakedRegistrationObjects()

.detectLeakedSqlLiteObjects()

.penaltyLog()

.penaltyDeath()

.build());

关闭 StrictMode

因为设置发生在一个线程上,StrictMode即使在控制从一个对象到另一个对象流动时也能发现违规。当违规发生时,您可能会惊讶地意识到代码正在主线程上运行,但是堆栈跟踪可以帮助您了解违规是如何发生的。然后,您可以通过将该代码移动到它自己的后台线程来采取措施解决这个问题。或者你可能决定让事情保持原样。这取决于你。当然,当您的应用投入生产时,您可能希望关闭StrictMode;您不希望您的代码因为一个警告而对用户崩溃。

有几种方法可以关闭生产应用的StrictMode。最直接的方法是删除调用,但是这使得继续开发应用变得更加困难。在调用StrictMode代码之前,你可以定义一个应用级的布尔值并测试它。在向外界发布应用之前将布尔值设置为 false 实际上禁用了StrictMode

一种更优雅的方法是利用应用的调试模式,如AndroidManifest.xml中所定义的。这个文件中<application>标签的属性之一是android:debuggable。如前所述,当您想要调试应用时,将该值设置为true;这样做的结果是ApplicationInfo对象获得一个标志集,然后您可以在代码中读取它。清单 8-3 展示了如何利用这一点,当应用处于调试模式时,StrictMode是活动的(当应用不处于调试模式时,StrictMode是不活动的)。

清单 8-3。设置StrictMode仅用于调试

// Return if this application is not in debug mode

ApplicationInfo appInfo = context.getApplicationInfo();

int appFlags = appInfo.flags;

if ((appFlags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {

// Do StrictMode setup here

}

请记住,当在模拟器或设备上启动应用的开发版本时,ADT 会将该属性设置为true,因此会启用前面代码中的StrictMode。当您导出应用来创建生产版本时,ADT 将属性设置为false

严格模式练习

作为一个练习,进入 Eclipse,复制一个您到目前为止开发的应用。您必须选择 2.3 或更高版本的构建目标,这样它才能找到StrictMode类。在首先启动的活动的onCreate()方法中,添加类似清单 8-1、8-2 和 8-3 中的代码;在模拟器中运行 Android 2.3 或更高版本的程序。在使用应用时,您可能会在 LogCat 中偶尔看到违规消息。

参考

以下是一些对您可能希望进一步探索的主题有帮助的参考:

  • http://developer.android.com/guide/developing/tools/index.html :这里描述的 Android 调试工具的开发者文档。
  • http://developer.android.com/guide/developing/devices/emulator.html#console :仿真器控制台命令的语法和用法。这允许您使用命令行界面来模拟在模拟器中运行的应用的事件。
  • www.eclipse.org/mat/:Eclipse 项目名为内存分析器(MAT)。您可以使用这个插件来读取由 DDMS 功能收集的 HPROF 文件。在 MAT 主页上,查找下载链接。MAT 可以作为独立工具下载,与保存的 HPROF 文件一起使用。或者您将看到一个更新站点链接,您可以使用 Eclipse 的安装新软件对话框来获取插件。安装好插件后,将 Android ➤ DDMS 首选项更改为在 Eclipse 中打开 HPROFs。

摘要

本章介绍了以下内容:

  • 如何设置 Eclipse 和您的设备进行调试?
  • Debug 透视图,它允许您停止应用来检查变量值,并逐句通过代码。
  • DDMS 透视图,它有相当多的工具用于调查线程、内存和方法调用,以及拍摄屏幕快照和生成事件发送到模拟器。
  • 从 DDMS 和命令行重置 adb 服务器。
  • Traceview,它显示应用运行时调用的方法,以及帮助您识别需要注意的问题方法以获得更好的用户体验的统计信息。
  • Hierarchy 视图,显示正在运行的应用的视图结构,并包括帮助您调整和排除应用故障的指标。
  • adb命令,可用于登录设备并查看周围。
  • 模拟器控制台,这是从命令行与模拟器对话的好方法。想象一下脚本的可能性。
  • 一个特殊的类,用于验证你的应用没有在主线程中做不推荐的事情,比如磁盘或网络 I/O。

复习问题

以下是一些你可以问自己的问题,以巩固你对这个话题的理解:

True or false? If you want to debug an application, you must explicitly set the android:debuggable attribute to true in the <application> tag of the AndroidManifest.xml file.   Name four things you can do with your application while using the Eclipse Debug perspective.   Is it possible to connect more than one device and/or emulator to Eclipse at the same time? If so, where do you select which application you want to work with?   Which DDMS feature do you use to get statistics for an application’s current memory allocations?   How do you determine how many threads are running in your application?   How do you find out the number of times a particular method is called in your application, and what the time of execution is within that method?   Where do you go to capture a picture of a device’s screen?   What Eclipse perspective is used to analyze the structure of an application’s views?   What do the three colored balls mean in this perspective? Does yellow mean you have a big problem? Does red?   If you see a yellow or red ball and want to know how bad the situation is, what should you do to see the actual numeric metric values?   If you want to look at method profiles, but you don’t want to see all the methods for the entire application, what do you do?   How do you create a Linux shell inside a running emulator?   Can you also do this on a real device? If so, are there any limitations to what you can do on a real device?   How do you figure out the port number of an emulator so you can connect to it using the Emulator Console?   What two main things does StrictMode check for?