安卓最佳实践(一)
一、开始之前
2011 年末,随着我对 Android 开发越来越感兴趣,我试图寻找一本书,希望它能让我的开发更上一层楼。我已经完成了几个应用,并想知道其他人在做什么,我可能已经错过了。当然,谷歌有大量的 Android 文档,但是 Android 文档有一些奇怪的建议;他们建议使用 jUnit3 进行我的单元测试,这感觉像是在倒退。我已经知道了现有的用于 Android 的 jUnit4 测试框架,比如 Roboelectric,所以可能还有其他一些我错过的、我根本不知道的很酷的东西,它们真的可以帮助我写出更好的代码。
这本书试图将开发者为 Android 平台创建的最佳实践的研究汇集在一起,希望你能在一个地方找到你需要的所有信息。
一旦你编写了一个应用,或者成为了 Android 开发团队的一员,你很快就会明白,如果你不考虑如何组织起来,Android 开发就像任何其他语言或环境一样,会变得混乱和低效。这本书将帮助你采取这些步骤,成为一个运转良好、富有成效的团队。
如果你想做以下一件或多件事,你可以考虑阅读这本书:
- 通过查看最佳实践示例代码,更好地进行 Android 开发。
- 编写更容易扩展和维护的应用。
- 编写更安全的应用。
- 学习如何不仅编写应用的客户端,还编写经常被忽略的服务器端。
Android 简介
Android 是基于 Linux 的智能手机开源操作系统。该公司成立于 2003 年 10 月,2005 年 8 月被谷歌收购。2008 年 10 月发布的 HTC Dream 是第一款运行 Android 的手机。
从开发者的角度来看,Android 应用通常是用 Java 编写的。Google 提供了一个 Android SDK,它提供了必要的库和应用来将 Java 代码转换成可以在 Android 手机上运行的格式。大多数人使用 Eclipse 或命令行来创建 Android 应用。Android Studio 最近作为 Eclipse 的替代产品出现,并有可能在未来一两年内成为首选的 IDE。
Android 是移动设备的首要操作系统,全球超过 75%的设备和 52%的美国市场都在运行它。
在我个人的经历中,曾经有一段时间 Android 开发是红发的继子。所有的开发首先在 iOS 上完成,然后在应用成功后在 Android 上开发。现在安卓手机有了这么大的市场份额,这种情况已经改变了。
谁应该读这本书?
这本书旨在让那些对 Android 有任何熟悉程度的开发人员都能接触到。然而,你的经验程度将决定你认为哪些部分最有用。如果你对 Android 开发完全陌生,或者只是在这里或那里修修补补,这本书应该可以帮助你为未来的 Android 工作培养良好的习惯和实践。如果你发现自己用 Android 做的工作越来越多,那就更是如此。测试、性能分析等等的方法和工具对于培养生产习惯和避免一些经典的陷阱和良好开发的反模式来说是非常好的。如果你最终没有说“我以后再写测试”,那么这本书很适合你。
对于中级或高级 Android 开发人员来说,这本书将带你了解 Android 工具链的当前技术状态的细节;您将看到如何最好地重构和改进现有的代码和应用,它将推动您拥抱一些您可能推迟到现在的高级主题。如果你从未考虑过基于 NDK 的开发,你将在第一时间学会如何正确地做。如果你从来没有必要的资金来做多平台、多手机测试和建模,你可以冒险一试,看看你一直以来都错过了什么。
开始前你需要什么
为了最大限度地利用这本书,预先整理一些日常事务将会消除以后的干扰,并且它将让你直接实现你将在每章中学到的工具和技术。
一个真正的 Android 应用
如果你已经编写了一两个 Android 应用,那么从这本书中获得最佳回报会有所帮助。他们甚至不需要一路走到 Google Play 但理想情况下,如果你经历了这个过程,有真实世界的用户对你的 Android 应用进行了测试,并且你根据他们的反馈或评论进行了修改,这将会有所帮助。
工作开发环境
你需要在你选择的 IDE 上安装 Android SDK:要么用 ADT 工具集安装 EclipseAndroid 开发者工作室;或者对于更喜欢冒险的人来说,像英特尔的 Beacon Mountain 这样的奇特的第三方开发环境。你需要一个实际的设备来跟随我们的一些例子,但是模拟器可以完成书中的大部分代码。
所有的花里胡哨
除了现有的 Android Developer Studio、带 ADT 的 Eclipse 或其他 IDE 之外,您还应该确保拥有 Android SDK 可用的可选库。这些工具包括 SDK 构建工具、与您的 SDK 发布级别相关的 Google APIs、Android 支持库以及适用于您的操作系统的 Web 驱动程序和 USB 驱动程序。
随着每一章的展开,还将向您介绍用于单元测试、手机多样性测试、性能分析等的特定附加工具。我们将在相关章节中逐一讨论这些工具。
示例应用的源代码
我们在每一章中使用的 Android 应用是一个简单的待办事项列表和任务提醒应用。你应该从www.apress.com/9781430258575/下载代码,这样你就可以跟着做了。我们将使用待办事项应用来展示 Android 的最佳实践,在每一章中引导您了解设计模式、性能问题、安全问题等等。
这本书里有什么
这是你在本书过程中可以期待的逐章总结:
- **第二章:**我们从第二章的模式开始。你可能已经对 Android 的用户界面(UI) 模式有些熟悉,这有助于在多种设备上创建一致的用户体验(UX) 。您还将了解如何使用 ActionBarSherlock 和 NineOldAndroids 等其他库来帮助您的老设备用户获得更及时的 Android 体验。
- **第三章:**继 UI 和 UX 模式之后,第三章着眼于实现 MVC 和 MVVM 开发人员设计模式,作为标准 Android 设计的替代方案,然后我们深入研究 Android 注释,以及它如何帮助您创建干净易懂的 Android 代码。
- 第四章: 第四章详细介绍了测试驱动开发(TDD)、行为驱动设计(BDD)和持续集成(CI)的基本敏捷元素,您可以在开发过程中使用它们。我们着眼于 Android SDK 中可用的单元测试,着眼于 Roboelectric、Calabash 和 Jenkins 等工具的好处,以及如何使用它们来创建更高效的敏捷开发环境。
- 第五章: Android 允许你使用 Android NDK 直接合并 C++ 代码,但由于 Java 和 C++ 之间的上下文切换,性能会受到显著影响。然而,有时在 Android 中使用新的或现有的 C++ 代码更有意义,而不必将其移植到 Java。第五章探讨了 C++ 是正确答案的原因,以及在 Android 上使用 c++ 的最佳方式。
- 第六章: 第六章是对几个行业标准的十大安全列表的最新审视,这些列表让你对 Android 安全的注意事项有了更好的了解。这一章的结尾是一个新的列表,它结合了谷歌和 OWASP 的十大列表中的最佳元素。
- **第七章:**设备测试可能是 Android 开发的克星。无论你想创建自己的测试平台还是使用众多在线服务中的一个第八章着眼于驯服设备碎片的实用方法。
- **第八章:**对于商业世界中的大多数 Android 应用,应用的 Android 部分充当后端服务器的客户端。信息通常但不总是通过 REST API 以 JSON 的形式发送。第八章深入探讨了如何与 REST 和 SOAP APIs 对话。您将学习如何创建 REST API ,以及为什么 Richardson 成熟度模型对您的 API 的寿命很重要。您还将使用 Google App Engine 创建自己的 web 服务。
二、Android 模式
我们从第二章开始,看看安卓的设计模式。在我看来,这可能意味着两件事,用户界面设计和架构;我们将在这里同时讨论这两个问题。在“UI 设计模式”一节中,我们将看看 Google 在冰激凌三明治发布时发布的 Android UI 指南。
在编写 Android 应用时,你不必遵循开箱即用的编程结构;有 MVC、MVVM 和 DI 三种选择。在本章的后半部分,“架构设计模式”,我们将会看到一些传统 Android 编程设计的替代方案。
用户界面设计模式
在冰淇淋三明治之前,Android 设计不是很好定义的。许多早期的应用看起来与图 2-1 中的例子非常相似。这个应用有内置的后退按钮功能和类似 iOS 的标签,因为它更有可能是现有 iOS 应用的一个端口;该应用甚至有一个名字,iFarmers,属于 iTunes 应用商店。
图 2-1 。iFarmers 是一款典型的早期 Android 应用
我不想挑出 iFarmers 应用,因为在 Google Play 上有许多类似应用的例子。我敢肯定,应用开发人员推动了更多的 Android 设计,毫无疑问,在那个时候,他们不能指向一个设计资源,并说这是设计 Android 应用的行业标准方式;他们可能被告知继续做下去。
如今,Android 平台更多的是利用庞大的 Android 用户群,而不是 iOS 转换。谷歌还制作了一个设计指南,可在http://developer.android.com/design/get-started/principles.html获得,这些原则就是本节将要解释的。
为了帮助演示不同的最佳实践,我们将在本书中使用一个简单的待办事项应用。因此,首先,让我们看看示例应用的代码;目前它有一个闪屏,如图图 2-2 所示,还有一个添加项目的待办列表屏幕,如图图 2-3 所示。
图 2-2 。TodDoList 应用闪屏
图 2-3 。应用的主要任务列表屏幕
这本书的可下载源代码中提供了该应用的完整代码,但出于我们的目的,我们将使用两个 Java 文件,TodoActivity.java,如清单 2-1 所示,以及TodoProvider.java,您将在清单 2-2 中看到。
清单 2-1 。TodoActivity.java
package com.logicdrop.todos;
import java.util.ArrayList;
import java.util.List;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import android.os.StrictMode;
public class TodoActivity extends Activity
{
public static final String APP_TAG = "com.logicdrop.todos";
private ListView taskView;
private Button btNewTask;
private EditText etNewTask;
private TodoProvider provider;
private OnClickListener handleNewTaskEvent = new OnClickListener()
{
@Override
public void onClick(final View view)
{
Log.d(APP_TAG, "add task click received");
TodoActivity.this.provider.addTask(TodoActivity.this
.getEditText()
.getText()
.toString());
TodoActivity.this.renderTodos();
}
};
@Override
protected void onStart()
{
super.onStart();
}
private void createPlaceholders()
{
this.getProvider().deleteAll();
if (this.getProvider().findAll().isEmpty())
{
List<String> beans = new ArrayList<String>();
for (int i = 0; i < 10; i++)
{
String title = "Placeholder " + i;
this.getProvider().addTask(title);
beans.add(title);
}
}
}
EditText getEditText()
{
return this.etNewTask;
}
private TodoProvider getProvider()
{
return this.provider;
}
private ListView getTaskView()
{
return this.taskView;
}
public void onCreate(final Bundle bundle)
{
super.onCreate(bundle);
this.setContentView(R.layout.main);
this.provider = new TodoProvider(this);
this.taskView = (ListView) this.findViewById(R.id.tasklist);
this.btNewTask = (Button) this.findViewById(R.id.btNewTask);
this.etNewTask = (EditText) this.findViewById(R.id.etNewTask);
this.btNewTask.setOnClickListener(this.handleNewTaskEvent);
this.showFloatVsIntegerDifference();
this.createPlaceholders();
this.renderTodos();
}
private void renderTodos()
{
List<String> beans = this.getProvider().findAll();
Log.d(APP_TAG, String.format("%d beans found", beans.size()));
this.getTaskView().setAdapter(
new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1, beans
.toArray(new String[]
{})));
this.getTaskView().setOnItemClickListener(new OnItemClickListener()
{
@Override
public void onItemClick(final AdapterView<?> parent,
final View view, final int position, final long id)
{
Log.d(APP_TAG, String.format(
"item with id: %d and position: %d", id, position));
TextView v = (TextView) view;
TodoActivity.this.getProvider().deleteTask(
v.getText().toString());
TodoActivity.this.renderTodos();
}
});
}
}
TodoActivity.java控制应用的布局,TodoProvider.java显示在列表 2-2 中,管理你添加到列表中的项目的数据。在应用中,我们已经用初始占位符项目的列表填充了它。
清单 2-2 。TodoProvider.java
package com.logicdrop.todos;
import java.util.ArrayList;
import java.util.List;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import com.logicdrop.todos.TodoActivity;
public class TodoProvider
{
private static final String DB_NAME = "tasks";
private static final String TABLE_NAME = "tasks";
private static final int DB_VERSION = 1;
private static final String DB_CREATE_QUERY = "CREATE TABLE " + TABLE_NAME + " (id integer primary key autoincrement, title text not null);";
private SQLiteDatabase storage;
private SQLiteOpenHelper helper;
public TodoProvider(final Context ctx)
{
this.helper = new SQLiteOpenHelper(ctx, DB_NAME, null, DB_VERSION)
{
@Override
public void onCreate(final SQLiteDatabase db)
{
db.execSQL(DB_CREATE_QUERY);
}
@Override
public void onUpgrade(final SQLiteDatabase db, final int oldVersion,
final int newVersion)
{
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
this.onCreate(db);
}
};
this.storage = this.helper.getWritableDatabase();
}
public synchronized void addTask(final String title)
{
ContentValues data = new ContentValues();
data.put("title", title);
this.storage.insert(TABLE_NAME, null, data);
}
public synchronized void deleteAll()
{
this.storage.delete(TABLE_NAME, null, null);
}
public synchronized void deleteTask(final long id)
{
this.storage.delete(TABLE_NAME, "id=" + id, null);
}
public synchronized void deleteTask(final String title)
{
this.storage.delete(TABLE_NAME, "title='" + title + "'", null);
}
public synchronized List<String> findAll()
{
Log.d(TodoActivity.APP_TAG, "findAll triggered");
List<String> tasks = new ArrayList<String>();
Cursor c = this.storage.query(TABLE_NAME, new String[] { "title" }, null, null, null, null, null);
if (c != null)
{
c.moveToFirst();
while (c.isAfterLast() == false)
{
tasks.add(c.getString(0));
c.moveToNext();
}
c.close();
}
return tasks;
}
}
这是一个非常基础的应用,设计和功能让人想起早期的 Android 2.x 应用,或者我们可以称为经典的 Android。
待办事项列表屏幕的布局在Layout.xml文件中定义,该文件可以在本书的资源文件夹中找到,也显示在清单 2-3 中。
清单 2-3 。 Layout.xml
<?xml version="1.0" encoding="utf-8"?> (change to LinearLayout)
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget31"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<TableRow
android:id="@+id/row"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_below="@+id/tasklist"
android:orientation="horizontal" >
<EditText
android:id="@+id/etNewTask"
android:layout_width="200px"
android:layout_height="wrap_content"
android:text=""
android:textSize="18sp" >
</EditText>
<Button
android:id="@+id/btNewTask"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@+string/add_button_name" >
</Button>
</TableRow>
<ListView
android:id="@+id/tasklist"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true" >
</ListView>
</RelativeLayout>
赫萝
有时很难想象我们刚刚看到的经典(2.x)设计风格和现代赫萝 Android 设计 (4.x)之间的对比,因为技术本身太年轻了。然而,在过去的几年里,手机用户界面的变化是显著的,所以我们确实需要区分这两者。
在我们研究新方法之前,请记住,我们的应用仍然需要占仍在使用经典手机的用户的相对较大比例,目前约占你的用户的四分之一(但这个数字一直在缩小;见http://developer.android.com/about/dashboards/index.html )。还有一种观点认为,我们应该进一步将 Android 3.x 从 Android 4.x 手机中分离出来,但基于你稍后将在第七章图 7-2 中看到的数字,蜂巢或 Android 3.x 已死。
那么赫萝安卓设计到底是什么意思呢?
以下是最基本的 Android 元素列表:
- 操作栏
- 导航抽屉
- 多窗格
在这一章中,我们将把重点放在动作栏上,因为它的变化无处不在,并且与您构建的每个应用都相关。从 Android 4.x 中的硬件动作栏转移到了软件动作栏,如图 2-4 所示。这种设计模式在 Android 中越来越普遍,也是 Android 和 iOS 的一个区别。然而,很少使用的应用设置仍然可以通过硬件按钮找到。
图 2-4 。动作栏
图 2-5 显示了与标签结合使用的动作栏,这对于更复杂的菜单结构很有用。
图 2-5 。带标签的动作栏
图 2-6 显示了导航抽屉或滑动菜单,它们可以作为动作栏的替代模式。
图 2-6 。导航抽屉
图 2-7 显示了我们的 TodoList 应用,增加了一个动作栏。
图 2-7 。带动作栏的 TodoList
Android 的用户界面设计模式与 iOS 有很大的不同,这经常会给不熟悉 Android 的人带来麻烦,尽管有一些相似之处,比如导航抽屉。没有必要在屏幕上的后退按钮或把标签在底部栏。跨平台的 HTML5 应用经常会遇到这个问题,因为它们经常混合了 iOS 和 Android 的设计模式。
要实现动作栏,在strings.xml中创建字符串,如清单 2-4 中的所示。
清单 2-4 。strings . XML
<?xml version=*"1.0"*encoding=*"utf-8"*?>
<resources>
<string name=*"app_name"*>ToDoList</string>
<string name=*"action_settings"*>Settings</string>
<string name=*"add_button_name"*>Add item</string>
<string-array name=*"action_bar_action_list"*>
<item>Select Filter</item>
<item>A-H</item>
<item>I-P</item>
<item>Q-Z</item>
</string-array>
</resources>
在清单 2-5 中,我们为动作栏设置了适配器代码,在本例中是一个动作栏微调器。
清单 2-5 。??【actionBarSpinnerAdapter】
this.actionBarSpinnerAdapter = ArrayAdapter.createFromResource(this, R.array.action_bar_action_list, android.R.layout.simple_spinner_dropdown_item);
final ActionBar myActionBar = getActionBar();
myActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
myActionBar.setListNavigationCallbacks(actionBarSpinnerAdapter, handleActionBarClick);
添加清单 2-6 中的所示的OnNavigationListener方法,以便在微调列表中选择菜单项时进行处理。
清单 2-6 。 动作栏监听器
private OnNavigationListener handleActionBarClick = new OnNavigationListener() {
@Override
public boolean onNavigationItemSelected(int position, long itemId) {
switch (position) {
case 0:
Log.d(APP_TAG, "Action Clear Filter selected");
TodoActivity.this.provider.clearFilter();
TodoActivity.this.renderTodos();
break;
case 1:
Log.d(APP_TAG, "Action A-H selected");
TodoActivity.this.provider.setFilter('A', 'H');
TodoActivity.this.renderTodos();
break;
case 2:
Log.d(APP_TAG, "Action I-P selected");
TodoActivity.this.provider.setFilter('I', 'P');
TodoActivity.this.renderTodos();
break;
case 3:
Log.d(APP_TAG, "Action Q-Z selected");
TodoActivity.this.provider.setFilter('Q', 'Z');
TodoActivity.this.renderTodos();
break;
default:
break;
}
return true;
}
};
不需要对renderTodos方法做任何修改,因为它已经被过滤了。
actionbar 夏洛克导航
现在 Action Bar 已经成为 Android 4.0 和更高版本的设计模式,那么早期版本的 Android,更具体地说是那些仍然运行 2.x 的人,又将何去何从呢?如果你发布的是消费者应用,你或你的商业利益相关者可能不想忽视这些客户。
一种选择是使用早期手机中的硬件按钮,这些按钮在很大程度上被基于 Android 版本或 API 级别的不同功能的动作栏模式和代码所取代。
更好的选择是使用 Jake Wharton 的名为 Action Bar 夏洛克的库,可以在http://actionbarsherlock.com/获得。
用杰克的话说,ActionBar 夏洛克是一个“通过单个 API 和主题,使用 Android 4.0+上的原生动作栏和 4.0 之前的自定义实现来实现动作栏设计模式的库。”它允许你为所有版本的 Android 编写一次代码,硬件按钮在很大程度上可以被忽略。图 2-8 显示了使用 ActionBarSherlock 的 ToDoList 应用。
在 Eclipse 中下载并安装这个库,并将这些项目添加到资源文件中,如清单 2-7 所示。
清单 2-7 。main.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/action_A_H"
android:title="A-H"
android:showAsAction="always"
android:orderInCategory="100">
</item>
<item
android:id="@+id/action_I_P"
android:title="I-P"
android:showAsAction="always">
</item>
<item
android:id="@+id/action_Q_Z"
android:title="Q-Z"
android:showAsAction="always">
</item>
</menu>
将onCreateOptionsMenu和onOptionsItemSelected代码添加到ToDoActivity中,如清单 2-8 所示。
清单 2-8 。OnCreateOptionsMenu 和 onOptionsItemSelected
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getSupportMenuInflater();
inflater.inflate(R.menu.activity_itemlist, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.action_A_H:
// filter & render
return true;
case R.id.action_I_P:
// filter & render
return true;
case R.id.action_Q_Z:
// filter & render
return true;
default:
return super.onOptionsItemSelected(item);
}
}
现在实现了动作栏,不考虑 Android OS 版本;图 2-8 显示它运行在 Android 2.1 上。
图 2-8 。在 Android 2.1 上使用 ActionBarSherlock 实现的动作栏
为不同设备设计
Android 允许你为不同的普通屏幕尺寸和屏幕像素密度提供图像和布局。为了在多种设备上创造良好的用户体验,您需要了解几个关键变量。最常见的屏幕尺寸有小、正常、大和超大(适用于平板电脑)。截至 2013 年 9 月 4 日,市场上几乎 80%的设备都是正常尺寸;参见表 2-1 。
表 2-1 。屏幕像素密度和屏幕尺寸
同样在表 2-1 中显示的是我们的第二个变量,显示器每平方英寸的像素数或屏幕像素密度。最常见的屏幕像素密度有 mdpi(中)、hdpi(高)、xhdpi(超高)和 xxhdpi(超高)密度。根据设备屏幕的屏幕密度或像素数,图像或布局的大小会有所不同。
该表的最新版本可在http://developer.android.com/about/dashboards/index.html找到。
图 2-9 显示了开源 Wordpress 应用的资源目录的布局。它包含了layout文件夹中的所有默认正常布局,以及小、大和 xlarge。对于某些但不是所有的屏幕尺寸,还为纵向和横向定义了进一步的资源。
图 2-9 。
但是什么是布局-sw720dp?在 Android 3.2 中,包含了新的布局定义来处理平板电脑;在本例中, sw 代表最小宽度,布局目标是 10 英寸平板电脑最小宽度为 720 密度像素的平板电脑。这些新的限定词还允许您以特定的宽度(w)和高度(h)为目标。
碎片
谷歌在 Android 3.0 中引入了片段,作为一种创建更加模块化的用户界面设计的方式,以便相同的片段可以在 Android 手机和 Android 平板电脑上以模块化的方式使用。
一个活动现在被分割成多个片段,允许基于设备的更复杂的布局。图 2-10 显示了手机上的一个带有相应任务细节的任务项。
图 2-10 。手机上的任务项和任务明细
图 2-11 显示了这在平板电脑上的外观,这里有更多的空间,可以在单个屏幕上查看任务项目和细节。
图 2-11 。平板电脑上的任务项目和任务详细信息
清单 2-8 显示了新片段布局的更新和注释的ToDoActivity.java代码。ToDoActivity现在扩展了FragmentActivity,我们创建一个TaskFragment和NoteFragment,根据设备布局换入换出。清单 2-9 中的代码检查布局中是否存在注释片段并显示出来。注释片段只存在于layout-large/main.xml资源中,而不存在于layout/main.xml文件中。
清单 2-8 。ToDoActivity.java 片段来源
public class TodoActivity extends FragmentActivity implements TaskFragment.OnTaskSelectedListener
{
@Override
public void onCreate(final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
this.setContentView(R.layout.main);
// Check whether the activity is using the layout version with
// the fragment_container FrameLayout. If so, we must add the first
// fragment
if (this.findViewById(R.id.fragment_container) != null)
{
// However, if we're being restored from a previous state,
// then we don't need to do anything and should return or else
// we could end up with overlapping fragments.
if (savedInstanceState != null)
{
return;
}
final TaskFragment taskFrag = new TaskFragment();
// In case this activity was started with special instructions
// from an Intent,
// pass the Intent's extras to the fragment as arguments
taskFrag.setArguments(this.getIntent().getExtras());
// Add the fragment to the 'fragment_container' FrameLayout
this.getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, taskFrag).commit();
}
}
/**
* User selected a task
*/
@Override
public void onTaskSelected(final int position)
{
// Capture the title fragment from the activity layout
final NoteFragment noteFrag = (NoteFragment) this.getSupportFragmentManager()
.findFragmentById(R.id.note_fragment);
if (noteFrag != null)
{
// If note frag is available, we're in two-pane layout…
noteFrag.updateNoteView(position);
}
else
{
// If the frag is not available, we're in the one-pane layout
// Create fragment and give it an argument for the selected task
final NoteFragment swapFrag = new NoteFragment();
final Bundle args = new Bundle();
args.putInt(NoteFragment.ARG_POSITION, position);
swapFrag.setArguments(args);
final FragmentTransaction fragTx = this.getSupportFragmentManager().beginTransaction();
// Replace whatever is in the fragment_container view
// and add the transaction to the back stack so the user can
// navigate back
fragTx.replace(R.id.fragment_container, swapFrag);
fragTx.addToBackStack(null);
// Commit the transaction
fragTx.commit();
}
}
}
清单 2-9 。 布局-large/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/tasks_fragment"
android:name="com.example.TaskFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<fragment
android:id="@+id/note_fragment"
android:name="com.example.NoteFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2" />
</LinearLayout>
建筑设计模式
所有类型的软件的一个基本问题可以归结为熵的概念,这表明有序的代码随着时间的推移自然会变得无序。或者换句话说,无论你如何努力,你的代码都会逐渐从一个有组织的状态变成一个无组织的状态,这就是所谓的高度耦合,或者更坦率地说,意大利面条式代码。
对于有一两个细心开发人员的较小的 Android 应用,这起初似乎不是问题。但是,随着新版本的发布和新人的加入,正如鲍伯·马丁所说,代码开始变得有味道,如果你想保持代码干净,就需要定期重组或重构。
对于更大的企业 Android 应用,组织代码的方式从一开始就是一个问题。不幸的是,经典的 Android 设计并不适合长期保持整洁。
在这一节中,我们将看看一些框架或软件设计模式,当你考虑你的应用的架构时,你可能要考虑这些模式。
如果你想在你的 Android 应用中有更少的耦合和更大的分离,你需要把你的逻辑移到主Activity类之外的类。我们从经典的 Android 设计开始,然后看看 MVC 和 MVVM,最后用依赖注入来帮助你了解如何使用这些框架来更好地组织你的代码。
经典安卓
在经典的 Android 设计中,用户界面是在 XML 布局文件中定义的。然后,活动使用这些 XML 文件来绘制屏幕,并为多种屏幕分辨率和硬件加载图像、大小信息和字符串。任何其他用户界面代码都是在主 UI 线程之外的其他类中编写的。
前面的清单 2-1 和 2-2 中显示的 TodoList 应用的代码适用于经典的 Android 设计。我们将在整本书中使用这个应用的许多不同版本。
MVC
MVC (Model-View-Controller) 是一种软件设计模式,它使用一个中介(Controller)将模型连接到视图,从而将用户界面(View)与业务规则和数据(Model)分离开来。
对我们来说,MVC 的主要好处是关注点的分离。MVC 的每个部分负责自己的工作,仅此而已:视图负责用户界面,模型负责数据,控制器在两者之间发送消息。
控制器为视图提供来自模型的数据,以绑定到 UI。对控制器的任何更改对视图都是透明的,UI 的更改不会影响业务逻辑,反之亦然。
设计模式有助于加强开发人员的结构,从而使代码变得更容易控制,更不容易损坏。MVC 的关注点分离使得如果我们想在以后的阶段添加单元测试变得更加容易。
有一种观点认为 Android 已经使用了 MVC 模式,XML 文件充当视图。然而,这并没有为我们提供任何分离关注点的实际可能性。
在下面的例子中,经典的 Android 代码被重构为 MVC 框架,如下所示。
模型
MVC 模型组件,如清单 2-10 所示,很大程度上取代了之前的ToDoProvider.java代码。
清单 2-10 。MVC 模型代码
final class TodoModel
{
private static final String DB_NAME = "tasks";
private static final String TABLE_NAME = "tasks";
private static final int DB_VERSION = 1;
private static final String DB_CREATE_QUERY = "CREATE TABLE " + TodoModel.TABLE_NAME +
" (id integer primary key autoincrement, title text not null);";
private final SQLiteDatabase storage;
private final SQLiteOpenHelper helper;
public TodoModel(final Context ctx)
{
this.helper = new SQLiteOpenHelper(ctx, TodoModel.DB_NAME, null, TodoModel.DB_VERSION)
{
@Override
public void onCreate(final SQLiteDatabase db)
{
db.execSQL(TodoModel.DB_CREATE_QUERY);
}
@Override
public void onUpgrade(final SQLiteDatabase db, final int oldVersion,
final int newVersion)
{
db.execSQL("DROP TABLE IF EXISTS " + TodoModel.TABLE_NAME);
this.onCreate(db);
}
};
this.storage = this.helper.getWritableDatabase();
}
public void addEntry(ContentValues data)
{
this.storage.insert(TodoModel.TABLE_NAME, null, data);
}
public void deleteEntry(final String field_params)
{
this.storage.delete(TodoModel.TABLE_NAME, field_params, null);
}
public Cursor findAll()
{
Log.d(TodoActivity.APP_TAG, "findAll triggered");
final Cursor c = this.storage.query(TodoModel.TABLE_NAME, new String[]
{ "title" }, null, null, null, null, null);
return c;
}
}
景色
MVC 中的视图代码,如清单 2-11 所示,是之前ToDoActivity.java代码的修改版本。任何 UI 更改现在都发生在这里,控制代码现在被移动到ToDoController.java文件中。
清单 2-11 。MVC 视图代码
public class TodoActivity extends Activity
{
public static final String APP_TAG = "com.example.mvc";
private ListView taskView;
private Button btNewTask;
private EditText etNewTask;
/*Controller changes are transparent to the View. UI changes won't
*affect logic, and vice-versa. See below: the TodoModel has
* been replaced with the TodoController, and the View persists
* without knowledge that the implementation has changed.
*/
private TodoController provider;
private final OnClickListener handleNewTaskEvent = new OnClickListener()
{
@Override
public void onClick(final View view)
{
Log.d(APP_TAG, "add task click received");
TodoActivity.this.provider.addTask(TodoActivity.this
.etNewTask
.getText()
.toString());
TodoActivity.this.renderTodos();
}
};
@Override
protected void onStop()
{
super.onStop();
}
@Override
protected void onStart()
{
super.onStart();
}
@Override
public void onCreate(final Bundle bundle)
{
super.onCreate(bundle);
this.setContentView(R.layout.main);
this.provider = new TodoController(this);
this.taskView = (ListView) this.findViewById(R.id.tasklist);
this.btNewTask = (Button) this.findViewById(R.id.btNewTask);
this.etNewTask = (EditText) this.findViewById(R.id.etNewTask);
this.btNewTask.setOnClickListener(this.handleNewTaskEvent);
this.renderTodos();
}
private void renderTodos()
{
final List<String> beans = this.provider.getTasks();
Log.d(TodoActivity.APP_TAG, String.format("%d beans found", beans.size()));
this.taskView.setAdapter(new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1,
beans.toArray(new String[]
{})));
this.taskView.setOnItemClickListener(new OnItemClickListener()
{
@Override
public void onItemClick(final AdapterView<?> parent, final View view, final int position, final long id)
{
Log.d(TodoActivity.APP_TAG, String.format("item with id: %d and position: %d", id, position));
final TextView v = (TextView) view;
TodoActivity.this.provider.deleteTask(v.getText().toString());
TodoActivity.this.renderTodos();
}
});
}
}
控制器
如清单 2-12 所示,控制器将 UI 绑定到数据,但也在上面的模型和视图代码之间创建了一个分离层。这两层之间的接口为代码的扩展提供了一个框架,也为新开发人员提供了一个框架,让他们能够遵循 MVC 模式来了解新代码的归属。
清单 2-12 。MVC 控制器代码
public class TodoController {
/*The Controller provides data from the Model for the View
*to bind to the UI.
*/
private TodoModel db_model;
private List<String> tasks;
public TodoController(Context app_context)
{
tasks = new ArrayList<String>();
db_model = new TodoModel(app_context);
}
public void addTask(final String title)
{
final ContentValues data = new ContentValues();
data.put("title", title);
db_model.addEntry(data);
}
//Overrides to handle View specifics and keep Model straightforward.
public void deleteTask(final String title)
{
db_model.deleteEntry("title='" + title + "'");
}
public void deleteTask(final long id)
{
db_model.deleteEntry("id='" + id + "'");
}
public void deleteAll()
{
db_model.deleteEntry(null);
}
public List<String> getTasks()
{
Cursor c = db_model.findAll();
tasks.clear();
if (c != null)
{
c.moveToFirst();
while (c.isAfterLast() == false)
{
tasks.add(c.getString(0));
c.moveToNext();
}
c.close();
}
return tasks;
}
}
MVVM
MVVM(模型-视图-视图模型)模式来自微软世界。它是 MVC 的一个特例,处理像 Silverlight 这样的 UI 开发平台。Net,它可能也适用于 Android。MVC 和 MVVM 的区别在于模型不应该包含特定于视图的逻辑——只包含为视图模型提供最小 API 所必需的逻辑。
模型只需要添加/删除,视图模型处理视图的具体需求。所有事件逻辑和委托都由视图模型处理,视图只处理 UI 设置。
在我们的例子中,模型组件基本保持不变,正如你在清单 2-13 中看到的。如清单 2-15 所示,视图模型充当 ToDoActivity(视图)和 ToDoProvider(模型)之间的代理。ViewModel 从视图接收引用,并使用它们来更新 UI。视图模型处理视图数据的渲染和更改,而视图,如清单 2-14 所示,只是提供了对其元素的引用。
模型
如清单 2-13 所示,该模型在 MVVM 很大程度上与 MVC 版本保持一致。
清单 2-13 。MVVM 模型代码
package com.example.mvvm;
import java.util.ArrayList;
import java.util.List;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
final class TodoModel
{
//The Model should contain no logic specific to the view - only
//logic necessary to provide a minimal API to the ViewModel.
private static final String DB_NAME = "tasks";
private static final String TABLE_NAME = "tasks";
private static final int DB_VERSION = 1;
private static final String DB_CREATE_QUERY = "CREATE TABLE " + TodoModel.TABLE_NAME + " (id integer primary key autoincrement, title text not null);";
private final SQLiteDatabase storage;
private final SQLiteOpenHelper helper;
public TodoModel(final Context ctx)
{
this.helper = new SQLiteOpenHelper(ctx, TodoModel.DB_NAME, null, TodoModel.DB_VERSION)
{
@Override
public void onCreate(final SQLiteDatabase db)
{
db.execSQL(TodoModel.DB_CREATE_QUERY);
}
@Override
public void onUpgrade(final SQLiteDatabase db, final int oldVersion,
final int newVersion)
{
db.execSQL("DROP TABLE IF EXISTS " + TodoModel.TABLE_NAME);
this.onCreate(db);
}
};
this.storage = this.helper.getWritableDatabase();
}
/*Overrides are now done in the ViewModel. The Model only needs
*to add/delete, and the ViewModel can handle the specific needs of the View.
*/
public void addEntry(ContentValues data)
{
this.storage.insert(TodoModel.TABLE_NAME, null, data);
}
public void deleteEntry(final String field_params)
{
this.storage.delete(TodoModel.TABLE_NAME, field_params, null);
}
public Cursor findAll()
{
//Model only needs to return an accessor. The ViewModel will handle
//any logic accordingly.
return this.storage.query(TodoModel.TABLE_NAME, new String[]
{ "title" }, null, null, null, null, null);
}
}
景色
MVVM 的视图,如清单 2-14 所示,只是提供了对其元素的引用。
清单 2-14 。MVVM 视图代码
package com.example.mvvm;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
public class TodoActivity extends Activity
{
public static final String APP_TAG = "com.logicdrop.todos";
private ListView taskView;
private Button btNewTask;
private EditText etNewTask;
private TaskListManager delegate;
/*The View handles UI setup only. All event logic and delegation
*is handled by the ViewModel.
*/
public static interface TaskListManager
{
//Through this interface the event logic is
//passed off to the ViewModel.
void registerTaskList(ListView list);
void registerTaskAdder(View button, EditText input);
}
@Override
protected void onStop()
{
super.onStop();
}
@Override
protected void onStart()
{
super.onStart();
}
@Override
public void onCreate(final Bundle bundle)
{
super.onCreate(bundle);
this.setContentView(R.layout.main);
this.delegate = new TodoViewModel(this);
this.taskView = (ListView) this.findViewById(R.id.tasklist);
this.btNewTask = (Button) this.findViewById(R.id.btNewTask);
this.etNewTask = (EditText) this.findViewById(R.id.etNewTask);
this.delegate.registerTaskList(taskView);
this.delegate.registerTaskAdder(btNewTask, etNewTask);
}
}
视图模型
如清单 2-15 所示,ViewModel 组件充当ToDoActivity(视图)和ToDoProvider(模型)之间的代理。ViewModel 处理视图数据的呈现和更改;它从视图中接收引用,并使用它们来更新 UI。
清单 2-15 。MVVM 视图-模型代码
package com.example.mvvm;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
public class TodoViewModel implements TodoActivity.TaskListManager
{
/*The ViewModel acts as a delegate between the ToDoActivity (View)
*and the ToDoProvider (Model).
* The ViewModel receives references from the View and uses them
* to update the UI.
*/
private TodoModel db_model;
private List<String> tasks;
private Context main_activity;
private ListView taskView;
private EditText newTask;
public TodoViewModel(Context app_context)
{
tasks = new ArrayList<String>();
main_activity = app_context;
db_model = new TodoModel(app_context);
}
//Overrides to handle View specifics and keep Model straightforward.
private void deleteTask(View view)
{
db_model.deleteEntry("title='" + ((TextView)view).getText().toString() + "'");
}
private void addTask(View view)
{
final ContentValues data = new ContentValues();
data.put("title", ((TextView)view).getText().toString());
db_model.addEntry(data);
}
private void deleteAll()
{
db_model.deleteEntry(null);
}
private List<String> getTasks()
{
final Cursor c = db_model.findAll();
tasks.clear();
if (c != null)
{
c.moveToFirst();
while (c.isAfterLast() == false)
{
tasks.add(c.getString(0));
c.moveToNext();
}
c.close();
}
return tasks;
}
private void renderTodos()
{
//The ViewModel handles rendering and changes to the view's
//data. The View simply provides a reference to its
//elements.
taskView.setAdapter(new ArrayAdapter<String>(main_activity,
android.R.layout.simple_list_item_1,
getTasks().toArray(new String[]
{})));
}
public void registerTaskList(ListView list)
{
this.taskView = list; //Keep reference for rendering later
if (list.getAdapter() == null) //Show items at startup
{
renderTodos();
}
list.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(final AdapterView<?> parent, final View view, final int position, final long id)
{ //Tapping on any item in the list will delete that item from the database and re-render the list
deleteTask(view);
renderTodos();
}
});
}
public void registerTaskAdder(View button, EditText input)
{
this.newTask = input;
button.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(final View view)
{ //Add task to database, re-render list, and clear the input
addTask(newTask);
renderTodos();
newTask.setText("");
}
});
}
}
依赖注入
如果我们的目标是远离高度耦合的代码,那么依赖注入模式可能比 MVC 或 MVVM 允许更大程度的跨应用的分离。它消除了类之间任何硬编码的依赖性,并允许您在编译时插入不同的类。这对于团队中的多个开发人员非常有用,因为它可以强制执行一个更严格的框架。
同样重要的是,依赖注入也促进了可测试代码的编写,我们将在敏捷 Android 的第四章中看到更多。
依赖注入(DI)在 Java 开发中已经存在很多年了。它通常有两种风格,编译时 DI(如 Guice)或运行时 DI(如 Spring)。在编译时 DI 中,注入在编译时是已知的,并且由映射文件控制。运行时 DI 更多地采用面向方面的编程方法,在应用运行时注入类。
Android 中有许多可用的 DI 框架,如 Roboelectric 和 Dagger,它们都是编译时 DI。
在下面的例子中,我们将看看如何使用 Dagger 来模拟一个数据库连接。通常你想测试应用而不是数据库。
在这个例子中,我们需要将四个部分连接在一起。ToDoModule.java包含注入映射,它告诉应用是使用连接到数据库的ToDoProvider存根文件还是ToDoProvider2文件。ToDoProvider.java包含返回假任务列表的存根文件,ToDoProvider2.java包含真实的数据库连接,ToDoApplication.java包含一个currentChoice布尔标志,告诉应用是使用存根还是真实的连接。
ToDoModule
清单 2-16 显示了ToDoModule如何连接两个数据库提供者;第一个是真正的数据库,第二个是存根函数。
清单 2-16 。匕首 ToDoModule.java
import dagger.Module;
import dagger.Provides;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
@Module(complete = true, injects = { TodoActivity.class })
public class TodoModule {
static final String DB_NAME = "tasks";
static final String TABLE_NAME = "tasks";
static final int DB_VERSION = 1;
static final String DB_CREATE_QUERY = "CREATE TABLE "
+ TodoModule.TABLE_NAME
+ " (id integer primary key autoincrement, title text not null);";
private final Context appContext;
public static boolean sourceToggle = false;
private TodoApplication parent;
/** Constructs this module with the application context. */
public TodoModule(TodoApplication app) {
this.parent = app;
this.appContext = app.getApplicationContext();
}
@Provides
public Context provideContext() {
return appContext;
}
/**
* Needed because we need to provide an implementation to an interface, not a
* class.
*
* @return
*/
@Provides
IDataProvider provideDataProvider(final SQLiteDatabase db) {
//Here we obtain the boolean value for which provider to use
boolean currentChoice = parent.getCurrentSource();
if(currentChoice == true){
//Here is a log message to know which provider has been chosen
Log.d(TodoActivity.APP_TAG, "Provider2");
return new TodoProvider2(db);
}else{
Log.d(TodoActivity.APP_TAG, "Provider");
return new TodoProvider(db);
}
}
/**
* Needed because we need to configure the helper before injecting it.
*
* @return
*/
@Provides
SQLiteOpenHelper provideSqlHelper() {
final SQLiteOpenHelper helper = new SQLiteOpenHelper(this.appContext,
TodoModule.DB_NAME, null, TodoModule.DB_VERSION) {
@Override
public void onCreate(final SQLiteDatabase db) {
db.execSQL(TodoModule.DB_CREATE_QUERY);
}
@Override
public void onUpgrade(final SQLiteDatabase db,
final int oldVersion, final int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TodoModule.TABLE_NAME);
this.onCreate(db);
}
};
return helper;
}
@Provides
SQLiteDatabase provideDatabase(SQLiteOpenHelper helper) {
return helper.getWritableDatabase();
}
}
数据库供应器
布尔值currentChoice告诉代码使用哪个数据库提供者;我们可以连接到真实的数据库ToDoProvider2,如清单 2-17 中的所示,或者连接到存根ToDoProvider,如清单 2-18 中的所示。
清单 2-17 。匕首 ToDoProvider2.java
package com.example.dagger;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
class TodoProvider2 implements IDataProvider {
private final SQLiteDatabase storage;
@Inject
public TodoProvider2(SQLiteDatabase db)
{
this.storage = db;
}
@Override
public void addTask(final String title) {
final ContentValues data = new ContentValues();
data.put("title", title);
this.storage.insert(TodoModule.TABLE_NAME, null, data);
}
@Override
public void deleteAll() {
this.storage.delete(TodoModule.TABLE_NAME, null, null);
}
@Override
public void deleteTask(final long id) {
this.storage.delete(TodoModule.TABLE_NAME, "id=" + id, null);
}
@Override
public void deleteTask(final String title) {
this.storage.delete(TodoModule.TABLE_NAME, "title='" + title + "'",
null);
}
@Override
public List<String> findAll() {
Log.d(TodoActivity.APP_TAG, "findAll triggered");
final List<String> tasks = new ArrayList<String>();
final Cursor c = this.storage.query(TodoModule.TABLE_NAME,
new String[] { "title" }, null, null, null, null, null);
if (c != null) {
c.moveToFirst();
while (c.isAfterLast() == false) {
tasks.add(c.getString(0));
c.moveToNext();
}
c.close();
}
return tasks;
}
}
存根提供者
清单 2-18 显示了伪造的或被删除的数据库;我们这样做是为了确保我们只测试我们的代码,而不是数据库连接。
清单 2-18 。ToDoProvider.java
package com.example.dagger;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
class TodoProvider implements IDataProvider {
private final SQLiteDatabase storage;
@Inject
public TodoProvider(SQLiteDatabase db)
{
this.storage = db;
}
@Override
public void addTask(final String title) {
final ContentValues data = new ContentValues();
data.put("title", title);
this.storage.insert(TodoModule.TABLE_NAME, null, data);
}
@Override
public void deleteAll() {
this.storage.delete(TodoModule.TABLE_NAME, null, null);
}
@Override
public void deleteTask(final long id) {
this.storage.delete(TodoModule.TABLE_NAME, "id=" + id, null);
}
@Override
public void deleteTask(final String title) {
this.storage.delete(TodoModule.TABLE_NAME, "title='" + title + "'",
null);
}
@Override
public List<String> findAll() {
Log.d(TodoActivity.APP_TAG, "findAll triggered");
final List<String> tasks = new ArrayList<String>();
final Cursor c = this.storage.query(TodoModule.TABLE_NAME,
new String[] { "title" }, null, null, null, null, null);
if (c != null) {
c.moveToFirst();
while (c.isAfterLast() == false) {
tasks.add(c.getString(0));
c.moveToNext();
}
c.close();
}
return tasks;
}
}
全部应用
最后,我们需要告诉代码要注入什么代码。我们在ToDoApplcation.java的getCurrentSource方法中这样做,如清单 2-19 所示。理想情况下,我们希望将它设置在一个配置文件中的某个位置,但是这里它是硬编码在一个文件中的。
清单 2-19 。所有 Application.java
package com.example.dagger;
import android.app.Application;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import dagger.ObjectGraph;
public class TodoApplication extends Application {
private ObjectGraph objectGraph;
SharedPreferences settings;
@Override
public void onCreate()
{
super.onCreate();
//Initializes the settings variable
this.settings = getSharedPreferences("Settings", MODE_PRIVATE);
Object[] modules = new Object[] {
new TodoModule(this)
};
objectGraph = ObjectGraph.create(modules);
}
public ObjectGraph getObjectGraph() {
return this.objectGraph;
}
//Method to update the settings
public void updateSetting(boolean newChoice){
Editor editor = this.settings.edit();
editor.putBoolean("CurrentChoice", TodoModule.sourceToggle);
editor.commit();
}
//Method to obtain the value of the provider setting
public boolean getCurrentSource(){
return this.settings.getBoolean("CurrentChoice", false);
}
}
摘要
在这一章中,我们研究了赫萝图形用户界面设计模式,以了解图形用户界面的最佳实践,以及使用 Dagger 的 MVC、MVVM 和 DI 架构设计模式,以了解如何最好地组织或分离您的代码,以便获得一些增长空间。我们将在第四章中回到 Dagger,关于敏捷 Android,展示我们如何使用 DI 进行模拟测试。如果你想进一步研究,本章和书中所有例子的代码都可以在 Apress 网站上找到。
三、性能
迈克尔·杰克逊提出了著名的第一和第二条程序优化规则:
- 规则 1 。别这么做。
- 规则二。(仅限专家!):先别做。
这可以有多种解释。对我来说,它真正的意思是“保持你的代码干净,不要担心优化它的流程。”避免使代码过于复杂。它还指出了一个事实,随着计算机和 JIT 编译器变得更加先进,你可能在六个月内不必担心它,因为硬件将超越你可以应用的任何最小优化。
这并不是说,如果你的移动应用需要 10 秒或更多的时间来加载一个活动,并且是一个糟糕的用户体验,你就什么都不做。请记住,无论你认为在网上可以接受的时间,在手机上肯定是不可接受的,无论是在 Android 还是 iOS 上。
而且情况变得更糟,因为如果你的应用花费太长时间,那么 Android 会显示可怕的 Android 无响应图像(见图 3-1 ),你的用户很可能会离开应用。这种情况更可能发生在内存和功耗更低的旧设备上;与程序优化第二定律相反,姜饼时代的许多旧 Android 设备仍在该领域徘徊,预计不会很快消失。
图 3-1 。Android“无响应”弹出窗口
为了便于比较,我们将讨论一下性能调优在 Web 上是如何工作的。优化安卓应用还是有点黑的艺术;虽然对于如何优化你的网络服务器有一个普遍的共识,但是在 Android 平台上却没有这么干净整洁。毕竟,市场上各种各样的 Android 设备使得优化你的应用成为一个移动的目标。
我们还会花一些时间在 Android 性能方面的技巧,这些技巧真的会对你的代码产生影响。最后,我们将看看 Android SDK 附带的一些工具,如 dal vik Debug Monitor Server(DDMS)和 Traceview,它们可以帮助识别瓶颈,并有望为创建更好的应用指明道路。
历史
早在 2000 年,性能优化都是关于如何优化通常位于 IIS 或 Apache web 服务器上的 web 应用,许多相同的点适用于我们在本章中尝试做的事情。不幸的是,测量 Android 的性能不像在 web 服务器上那么容易。
Web 服务器通常的目标是 95%的页面应该在一秒或更短的时间内返回。原始统计数据,例如页面点击次数和页面计时(使用时间标记,如图 3-2 所示),都可以在日志文件中看到。诀窍是优化速度最慢、访问量最大的页面,这给人一种速度更快的 web 服务器的感觉;说到表现,感知就是现实。在移动设备上也是如此。
图 3-2 。带有耗时令牌的 Web 服务器日志文件
通过在数据库上添加索引、修复 SELECT 语句以限制返回的数据量,或者修复编程控制流逻辑的问题,通常可以在性能最差的页面上大幅提高页面速度。使用这种“清洗、冲洗、重复”的方法,在一段时间内反复修复访问量最大、性能最差的页面可以改变 web 服务器的速度。
另一方面,Android 就没那么简单了。在 Android 中,没有类似“95%的页面应该在一秒或更短的时间内返回”这样的标准。对于一个应用需要有多快响应速度,人们还没有达成共识。并且度量也可能因设备而异。衡量每项活动需要多长时间也要困难得多,因为没有带有方便的耗时令牌的日志文件可供您轻松使用。
然而,这也不全是坏消息,因为 Android SDK 确实附带了许多工具,如 DDMS 和 Traceview,这些工具确实有助于调试性能问题,但它们可以衡量 Android 应用性能的不同方面。
理想情况下,您需要一个好的负载测试工具,带有某种可靠的时间度量。如果可能的话,它应该作为构建的一部分在持续集成服务器上运行,这样您就可以看到回归测试报告;通过查看应用运行过程中同样的动作花费了多长时间,你将能够确定某件事情是否突然比过去花费了更多时间。
当我们试图优化 Web 服务时,我们将需要查看 Web 服务器的统计数据,我们将在本书的后面回到这个问题。
性能提示
让我们来看看一些 Android、Java、Web 服务和 SQL 技巧,如果您的应用没有正确响应,您可能想尝试一下。
Android 性能
谷歌发布了一份优秀的性能提示列表(见http://developer.android.com/training/articles/perf-tips.html),以下内容大部分摘自该列表并对其进行了扩展。其中一些优化采用非常宏观的方法,和一些非常微观的方法进行优化,将只从 APK 中生成的classes.dex中删除一两行字节码。这些微优化可能会由未来的即时 DVM 优化来处理,或者提前由新的 ART 或 Android 运行时虚拟机来处理,这是 DVM 的替代品。然而,在撰写本文时,ART 仅在 Android KitKat 上可用,这些自动化优化变得司空见惯可能还需要一段时间。
-
避免创建不必要的对象或内存分配。编写高效代码有两个基本规则:
-
不要做你不需要做的工作。
-
能避免就不要分配内存。
-
移动开发目前相对简单;我们没有随着技术的成熟而出现的一层又一层的复杂性,比如 EJB。
-
但在 Android 上这是迟早要发生的事情,这是必然的。人们已经在他们的 Android 应用中加入 ORM,所以试着转移到更多的 TDD(测试驱动开发)模型,想想你正在引入什么。您真的需要重新发明某种缓存机制来满足您正在实现的特性吗?如果你仍然担心,那么应用 YAGNI 概念——你不需要它,因为你真的不需要它。
-
**避免内部 getter/setter。**虚方法调用比实例字段查找更昂贵。遵循常见的面向对象编程实践并在公共接口中使用 getters 和 setters 是合理的,但是在一个类中,您应该总是直接访问字段。这是一个微优化的例子,它从 APK 中生成的
classes.dex中删除了一两行字节码。 -
**在适当的地方使用静态/最终。**由于代码被编译成 Davlik 字节码的方式,任何引用 intVal 的代码如果使用 static final 都将直接使用整数值 42,对 strVal 的访问将使用相对廉价的“字符串常量”指令,而不是字段查找。
-
明智地使用浮动。浮点计算非常昂贵,在 Android 设备上通常需要两倍于整数计算的时间。
-
NDK 称,要少花钱,多办事。使用 JNI 或 NDK 在 Java 和 C++ 之间进行上下文切换可能会很昂贵。也没有 JIT 优化。
-
但是,如果应用使用一些核心算法或功能,不需要以任何重要的方式绑定到用户界面,它可能应该在本机运行。即使使用 JIT 编译器,本机运行几乎总是比 Java 快。NDK 还带来了一些重要的安全优势,因为对 C++ 代码进行逆向工程要困难得多。
-
**仅在需要时放大视图。**基本上,这里的想法是,你只需要将视图放大最少的次数,或者更好的是延迟显示视图,因为放大视图是非常昂贵的。
-
**使用标准库和增强功能。**使用库,而不是滚动自己的代码。Android 有时也会用优化的手工编码汇编程序代替库方法。例如,使用
String.indexOf()和System.arraycopy()方法比手工编码的循环快 9 倍。 -
Use StrictMode. To limit the chance of an Android Not Responsive (ANR) error, it helps to not include any slow network or disk access in the applications main thread. Android provides a utility called
StrictMode, which is typically used to detect if there are any unwanted disk or network accesses introduced into your application during the development process. AddStrictModeto youronCreate()method as shown in Listing 3-1.StrictModecalls are also pretty expensive, so make sure the code isn’t shipped as production code.清单 3-1 。使用 Strictmode 实用程序
public void onCreate() { // remove from production code if (BuildConfig.DEBUG){ StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder(), .detectDiskReads() .detectDiskWrites() .detectNetwork() .penaltyLog() .build()); { super.onCreate(); } -
**优化 onSomething()类。**之前我们谈到了感知是 web 应用的现实;在 Android 世界中,如果你的
onStart()、onCreate()和onResume()类快如闪电,那么这个应用会被认为是一个更快的 Android 应用。因此,如果你有任何代码可以放在其他地方,或者你可能想要应用优化,那么花时间在这些类上会带来回报。尽可能长时间等待,以扩大任何观点。使用android.view.ViewStub将允许在需要时创建对象,这种技术被称为延迟膨胀视图。 -
使用 Relativelayouts 而不是 Linearlayouts。新的 Android 开发者倾向于创建一个过度使用的 UI。随着应用变得越来越复杂,这些线性布局经常会变得非常嵌套。用一个
RelativeLayout代替这些LinearLayouts将会提高你的 UI 加载速度。Lint 和 Hierarchy Viewer 将帮助您识别深度嵌套的LinearLayouts。
Java 性能
有写 Java 性能的书和书籍,Android 也可以从一些写得很好的 Java 代码中受益。Java 性能调优页面(http://www.javaperformancetuning.com/tips/rawtips.shtml)是一个链接页面,其中链接了关于 Java 优化的文章,以及这些页面中每一页优化技巧的总结和回顾。
最常见的优化如下:
- 使用+连接两个字符串;使用
Stringbuffer连接更多的字符串。
- 除非需要同步,否则不要同步代码。
- 完成后,关闭所有资源,如连接和会话对象。
- 不会被重定义的类和方法应该被声明为 final。
- 访问数组比访问向量、字符串和字符串缓冲区快得多。
SQLite 性能
网站效率低下通常可以总结为“是数据库的问题,笨蛋。”虽然在 Android 上这不是一个问题,因为 SQLite 更多地用于客户端信息缓存,但是没有理由解释计划在性能调优中仍然非常有用。不要忘记,如果你需要,你也可以在 SQLite 上创建索引(见图 3-3 )。
图 3-3 。SQLite 索引
学习 SQLite Android 库,使用DatabaseUtils.InsertHelper命令插入大量数据,或者在适当的时候使用compileStatement。不要将数据库存储在 SD 卡上。最后,不要在 SELECT 语句中返回整个数据表;始终使用精心设计的 SQL 语句返回最少的行数。
网络服务性能
对于网络服务来说,这是一个“一切旧的都是新的”的例子我们又回到了我之前提到的网站优化技术。使用服务器日志,如前面的图 3-2 所示,查看每个调用花费的时间,并优化最慢的、最常用的 Web 服务。一些常见的 Web 服务优化如下:
- 最小化 Web 服务信封的大小;尽可能选择 REST 而不是 SOAP,JSON 而不是 XML。
- 减少往返次数,避开喋喋不休的 Web 服务调用,并将 Web 服务事务的数量保持在最低水平。
- 删除任何重复的呼叫,它们并不像看起来那样不常见。
- 与数据库 SELECT * FROM TABLE 语句类似,仔细选择查询参数可以极大地限制通过 Web 服务返回的数据量。
- 避免跨调用维护状态;最具伸缩性的 Web 服务不维护任何状态。
- 压缩数据。
像 Charles Proxy ( http://www.charlesproxy.com/)这样的 Web 代理工具是一种很好的方式来查看你的应用是如何与 Web 服务交互的。
Web 服务的主题在第八章中有更详细的介绍。
优化代码
在接下来的几页中,您将看到这些优化是如何在 ToDo List 应用中使用的。首先,清单 3-2 展示了 Splash.java,它有一个基本的 onCreate()方法。
清单 3-2 。待办事项应用的 Splash.java 页面
package com.logicdrop.todos;
import android.app.Activity;
import android.os.Bundle;
import android.content.Intent;
public class Splash extends Activity {
public void onCreate(Bundle savedInstanceState) {
// TIP: Optimized the onSomething() classes, especially onCreate()
super.onCreate(savedInstanceState);
// TIP: View - inflate the views a minimum number of times
// inflating views are expensive
/*for (int i=0; i<10000; i++)
setContentView(R.layout.splash);*/
// TIP: Splashscreen optional (DONE)
setContentView(R.layout.splash);
Thread timer = new Thread() {
public void run() {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
Intent openStartingPoint = new Intent("com.logicdrop.todos.TodoActivity");
startActivity(openStartingPoint);
}
}
};
timer.start();
}
}
清单 3-3 中显示的ToDoActivity.java、,有很多本章提到的 Android 和 Java 优化;有关更多信息,请参见代码中的注释。它还展示了如何使用 Traceview API 停止和启动分析。
清单 3-3 。待办事项列表应用的 ToDoActivity.java 页面
package com.logicdrop.todos;
import java.util.List;
import java.util.ArrayList;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import android.os.StrictMode;
import com.logicdrop.todos.R;
public class TodoActivity extends Activity
{
public static final String APP_TAG = "com.logicdrop.todos";
private ListView taskView;
private Button btNewTask;
private EditText etNewTask;
private TodoProvider provider;
// TIP: Use static/final where appropriate
private final OnClickListener handleNewTaskEvent = new OnClickListener()
{
@Override
public void onClick(final View view)
{
Log.d(APP_TAG, "add task click received");
TodoActivity.this.provider.addTask(TodoActivity.this
.etNewTask
.getText()
.toString());
TodoActivity.this.renderTodos();
}
};
// TIP: Traceview
@Override
protected void onStop()
{
super.onStop();
// Debug.stopMethodTracing();
}
@Override
protected void onStart()
{
// Debug.startMethodTracing("ToDo");
super.onStart();
}
// TIP: Use floats judiciously
@SuppressWarnings("unused")
private void showFloatVsIntegerDifference()
{
int max = 1000;
float f = 0;
int i = 0;
long startTime, elapsedTime;
// Compute time for floats
startTime = System.nanoTime();
for (float x = 0; x < max; x++)
{
f += x;
}
elapsedTime = System.nanoTime() - startTime;
Log.v(APP_TAG, "Floating Point Loop: " + elapsedTime);
// Compute time for ints
startTime = System.nanoTime();
for (int x = 0; x < max; x++)
{
i += x;
}
elapsedTime = System.nanoTime() - startTime;
Log.v(APP_TAG, "Integer Point Loop: " + elapsedTime);
}
// TIP: Avoid creating unnecessary objects or memory allocation
private void createPlaceholders()
{
// TIP: Avoid internal getters/setters
provider.deleteAll();
if (provider.findAll().isEmpty())
{
// TIP: Arrays are faster than vectors
List<String> beans = new ArrayList<String>();
// TIP: Use enhanced for loop (DONE)
// This is example of the enhanced loop but don't allocate objects if not necessary
/*for (String task : beans) {
String title = "Placeholder ";
this.provider.addTask(title);
beans.add(title);
}*/
/*for (int i = 0; i < 10; i++)
{
String title = "Placeholder " + i;
this.getProvider().addTask(title);
beans.add(title);
}*/
}
}
// TIP: Avoid private getters/setters - consider using package (DONE)
/*EditText getEditText()
{
return this.etNewTask;
}*/
/*private TodoProvider getProvider()
{
return this.provider;
}*/
/*private ListView getTaskView()
{
return this.taskView;
}*/
@Override
public void onCreate(final Bundle bundle)
{
// TIP: Use Strictmode to detect unwanted disk or network access
// Remove from production code (DONE)
//StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
// .detectDiskReads()
// .detectDiskWrites()
// .detectNetwork()
// .penaltyLog()
// .build());
super.onCreate(bundle);
// TIP: Do not overuse Linearlayouts, as they become more complex (DONE)
// Replace them with Relativelayouts, increasing UI loading speed
this.setContentView(R.layout.main);
this.provider = new TodoProvider(this);
this.taskView = (ListView) this.findViewById(R.id.tasklist);
this.btNewTask = (Button) this.findViewById(R.id.btNewTask);
this.etNewTask = (EditText) this.findViewById(R.id.etNewTask);
this.btNewTask.setOnClickListener(this.handleNewTaskEvent);
this.renderTodos();
// TIP: Again, don't allocate unnecessary objects that expand the heap size to significant proportions (DONE)
// Once GC occurs, a large amount of the heap memory is dumped, especially with
// local data structures, which renders a large portion of the heap unused.
// SEE: optimizedHeap.png, deoptimizedHeap.png, heap-before.tiff, heap-after.tiff
/*ArrayList<uselessClass> uselessObject = new ArrayList<uselessClass>();
for (int i=0; i<180000; i++)
uselessObject.add(new uselessClass());*/
}
private void renderTodos()
{
final List<String> beans = this.provider.findAll();
Log.d(TodoActivity.APP_TAG, String.format("%d beans found", beans.size()));
this.taskView.setAdapter(new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1,
beans.toArray(new String[]
{})));
this.taskView.setOnItemClickListener(new OnItemClickListener()
{
@Override
public void onItemClick(final AdapterView<?> parent, final View view, final int position, final long id)
{
Log.d(TodoActivity.APP_TAG, String.format("item with id: %d and position: %d", id, position));
final TextView v = (TextView) view;
TodoActivity.this.provider.deleteTask(v.getText().toString());
TodoActivity.this.renderTodos();
}
});
}
// Class with 26 double data members used to expand heap size in example
/*private class uselessClass {
double a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z;
}*/
}
最后,清单 3-4 中显示的ToDoProvider.java、有一些剩余优化的例子,比如总是关闭资源和只使用 SELECT 语句返回最少的数据。
清单 3-4 。待办事项列表应用的 ToDoProvider.java 页面
package com.logicdrop.todos;
import java.util.ArrayList;
import java.util.List;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
final class TodoProvider
{
private static final String DB_NAME = "tasks";
private static final String TABLE_NAME = "tasks";
private static final int DB_VERSION = 1;
private static final String DB_CREATE_QUERY = "CREATE TABLE " + TodoProvider.TABLE_NAME + " (id integer primary key autoincrement, title text not null);";
// TIP: Use final wherever possible (DONE)
private final SQLiteDatabase storage;
private final SQLiteOpenHelper helper;
public TodoProvider(final Context ctx)
{
this.helper = new SQLiteOpenHelper(ctx, TodoProvider.DB_NAME, null, TodoProvider.DB_VERSION)
{
@Override
public void onCreate(final SQLiteDatabase db)
{
db.execSQL(TodoProvider.DB_CREATE_QUERY);
}
@Override
public void onUpgrade(final SQLiteDatabase db, final int oldVersion,
final int newVersion)
{
db.execSQL("DROP TABLE IF EXISTS " + TodoProvider.TABLE_NAME);
this.onCreate(db);
}
};
this.storage = this.helper.getWritableDatabase();
}
// TIP: Avoid synchronization (DONE)
public void addTask(final String title)
{
final ContentValues data = new ContentValues();
data.put("title", title);
this.storage.insert(TodoProvider.TABLE_NAME, null, data);
}
public void deleteAll()
{
this.storage.delete(TodoProvider.TABLE_NAME, null, null);
}
public void deleteTask(final long id)
{
this.storage.delete(TodoProvider.TABLE_NAME, "id=" + id, null);
}
public void deleteTask(final String title)
{
this.storage.delete(TodoProvider.TABLE_NAME, "title='" + title + "'", null);
}
// TIP: Don't return the entire table of data. (DONE)
// Unused
public List<String> findAll()
{
Log.d(TodoActivity.APP_TAG, "findAll triggered");
final List<String> tasks = new ArrayList<String>();
final Cursor c = this.storage.query(TodoProvider.TABLE_NAME, new String[]
{ "title" }, null, null, null, null, null);
if (c != null)
{
c.moveToFirst();
while (c.isAfterLast() == false)
{
tasks.add(c.getString(0));
c.moveToNext();
}
// TIP: Close resources (DONE)
c.close();
}
return tasks;
}
}
工具
在这一节中,我们将研究两种有助于发现性能瓶颈的工具 Android SDK 附带的工具和 Unix 命令行工具。
Android SDK 附带了以下工具来帮助我们识别任何性能问题:
- 启动
- 特蕾西
- 线头
- 层次结构查看器
- 电视观众
Dalvik Debug Monitor Server (DDMS)是一个 Android SDK 应用,可以作为独立工具或 Eclipse 插件使用。DDMS 做了很多事情,包括设备屏幕捕获和提供一个查找日志输出的地方。但是它也提供堆分析、方法分配和线程监控信息。Android SDK 也有 Traceview 工具用于方法分析,layoutopt用于优化 XML 布局,以及 Hierarchy Viewer 用于优化 UI。
由于 Android 基本上是一个 Linux 外壳,我们可以利用以下许多命令行 Unix 工具进行性能测试:
TopDumpsysVmstatProcstats
在这一节中,我们将看看如何使用这些工具来快速了解您的应用在哪里花费了大部分时间。
DDMS
在这一节中,我们将讨论系统性能、堆使用、线程和 Traceview 工具,所有这些都是 DDMS 的一部分。我们还将查看内存分析器工具(MAT) ,它可以作为 Eclipse 工具的一部分下载,并用于报告如何在堆中管理内存。
系统性能
DDMS 套件中最基本的工具是系统性能,它给出了当前 CPU 负载、内存使用和帧渲染时间的快速概览,如图图 3-4 所示。当你的应用消耗了太多的 CPU 或内存时,你的应用表现不佳的第一个迹象。
图 3-4 。系统性能工具显示 CallCenterApp 的 CPU 负载
堆使用
DDMS 也提供了一个堆使用工具。采取以下步骤来查看内存堆,从中可以看到正在创建哪些对象,以及它们是否被垃圾收集正确地销毁了。(参见图 3-5 。)
- 在“设备”选项卡中,选择要查看堆的进程。
- 单击“更新堆”按钮,为进程启用堆信息。
- 单击 Heap 选项卡中的 Cause GC 调用垃圾收集,这将启用堆数据收集。
- 当垃圾收集完成时,您将看到一组对象类型和为每种类型分配的内存。
- 单击列表中的对象类型,查看以字节为单位显示为特定内存大小分配的对象数量的条形图。
- 再次单击“导致 GC”以刷新数据。给出了堆的详细信息以及特定分配类型的分配大小图表。观察堆大小的总体趋势,确保它不会在应用运行期间持续增长。
图 3-5 。查看 DDMS 堆
Eclipse 内存分析器
Eclipse 有一个集成的内存分析器工具(MAT) 插件,您可以从http://www.eclipse.org/mat/downloads.php下载并安装它。MAT 可以帮助您理解堆输出。现在,当你转储堆配置文件或 hprof 文件时(见图 3-6 ,它将被自动分析,这样你就可以对堆文件有所了解。
图 3-6 。转储 hprof 文件
MAT 提供了许多报告,包括最大类的支配者树、顶级消费者报告和泄漏嫌疑报告。图 3-7 显示了保留尺寸最大的物体。
图 3-7 。内存分析器工具概述
存储器分配
关于分配的下一层细节显示在分配跟踪器视图(图 3-8 )中。要显示它,请单击开始跟踪,在应用中执行操作,然后单击获取分配。列表按分配顺序显示,最新的内存分配显示在最前面。突出显示它会给出一个堆栈跟踪,显示该分配是如何创建的。
图 3-8 。分配跟踪器
线
DDMS 中的线程监视器和分析视图对于管理大量线程的应用非常有用。要启用它,点击更新线程图标,如图 3-9 所示。
图 3-9 。DDMS 螺纹
一个线程运行用户代码(utime)和系统代码(stime)所花费的总时间以 jiffies 来衡量。瞬间原本是光传播 1 厘米所需的时间,但对于 Android 设备来说,它是系统定时器中断一次的持续时间。它因设备而异,但通常被接受为大约 10ms。星号表示守护线程,状态 Native 表示线程正在执行本机代码。
查看图 3-9 中的样本数据,很明显 GC 花费了不寻常的时间。仔细观察应用如何处理对象创建可能是提高性能的一个好主意。
方法剖析
方法概要分析是 DDMS 选择的工具,用于快速了解应用中真正花费时间的地方,并且是找出花费太多时间的方法的第一步。当您的应用正在运行并理想地执行一些有趣的任务(您希望获得更多的性能数据)时,采取以下步骤来使用方法分析:
-
单击开始方法分析。
-
几秒钟后,再次单击该图标停止收集。
-
IDE 将自动启动 Traceview 窗口,并允许您直接在 IDE 中分析结果。
-
Click a method call in the bottom pane to create a hierarchy, showing you the current method, the parent(s) that call this method, and then the children methods called from within the selected method (Figure 3-10).
图 3-10 。使用 Traceview 在 DDMS 进行方法分析
-
确定花费时间最多的方法,这样您就可以通过创建 Traceview 文件来更仔细地查看它们,我们将在本节的稍后部分探讨这些文件。
每个方法都有其父方法和子方法,列如下:
- Inc % 方法加上任何被调用的方法所花费的总时间的百分比
- 包含在方法中花费的时间加上在任何被调用的方法中花费的时间
- Excl % 该方法花费的时间占总时间的百分比
- 独占该方法花费的时间
- 调用+递归调用该方法的次数加上任何递归调用
- 每次通话时间每次通话的平均时间
特蕾西
一旦您确定了要仔细研究的方法,您就可以使用 Traceview 的命令行版本和跟踪 API 进行更精确的测量。在您想要分析的代码周围添加 Debug.startMethodTracing 和 Debug.stopMethodTracing,如清单 3-5 所示。再次编译您的代码,并将 APK 推送到您的设备。
清单 3-5 。startMethodTracing 和 stopMethodTracing
public class ScoresActivity extends ListActivity {
public void onStart() {
// start tracing to "/sdcard/scores.trace"
Debug.startMethodTracing("scores");
super.onStart();
// other start up code here
}
public void onStop() {
super.onStop();
// other shutdown code here
Debug.stopMethodTracing();
}
// Other implementation code
}
现在可以使用以下命令将跟踪文件从设备中取出并显示在 Traceview 中:
adb pull /sdcard/scores.trace scores.before.trace
图 3-11 显示了代码优化前的结果。
图 3-11 。优化前的跟踪文件
使用本章前面的一些建议优化代码,并再次测量,这一次使用以下命令:
adb pull /sdcard/scores.trace scores.after.trace
图 3-12 显示了优化后的结果;区别很明显。
图 3-12 。优化后的跟踪文件
功能区
Lint 和它最初的 Unix 同名,是一个静态代码分析工具。它取代了 layoutopt 工具,该工具用于分析布局文件并指出潜在的性能问题,以通过重组 UI 布局来快速获得性能提升。它现在做得更多,包括以下错误检查类别:
- 正确性
- 正确性:消息
- 安全性
- 性能
- 可用性:排版
- 可用性:图标
- 可用性
- 易接近
- 国际化
如果您运行命令lint --list Performance ,它会告诉您 Lint 会进行以下性能检查,其中许多我们已经在 Android 提示部分看到过:
FloatMath:建议将android.util.FloatMath通话换成java.lang.Math。- 建议在一个类中用直接字段访问代替 getters 的使用。
InefficientWeight:在LinearLayouts中寻找无效的重量声明。NestedWeights:寻找嵌套布局权重,代价很高。DisableBaselineAlignment:寻找LinearLayouts,?? 应该设置android:baselineAligned=false。ObsoleteLayoutParam:查找对给定父布局无效的布局参数。MergeRootFrame:检查一个根 <框架布局> 是否可以被一个<merge>标签替换。UseCompoundDrawables:检查当前节点是否可以被使用复合 drawables 的TextView替换。UselessParent:检查是否可以删除父布局。UselessLeaf:检查是否可以删除叶布局。TooManyViews:检查布局是否有太多视图。TooDeepLayout:检查布局层次是否太深。ViewTag:使用View.setTag时发现潜在泄漏。HandlerLeak:确保处理程序类不会保留对外部类的引用。UnusedResources:寻找未使用的资源。UnusedIds:寻找未使用的 id。SecureRandom:查找SecureRandom类的可疑用法。Overdraw:查找过度绘制问题(绘制视图只是为了完全覆盖)。UnusedNamespace:查找 XML 文档中未使用的名称空间。DrawAllocation:查找绘图代码内的内存分配。UseValueOf:查找包装类的“new”实例,它应该使用valueOf来代替。UseSparseArrays:寻找机会用更高效的SparseArray取代HashMaps。Wakelock:查找wakelock用法的问题。Recycle:寻找资源上丢失的recycle()调用。
Lint 可以从 Eclipse 内部运行,也可以在命令行上运行。如果您只想对您的项目运行性能检查,请在命令行中键入lint --check Performance``<project name>。清单 3-6 显示了样例应用的这个命令的输出,显示了一些需要更好组织的布局。
清单 3-6 。CallCenterApp 项目的 Lint 性能输出
Scanning CallCenterV3: ...................................................
Scanning CallCenterV3 (Phase 2): ......................
res\layout\custom_titlebar.xml:6: Warning: Possible overdraw: Root element paints background #004A82 with a theme that also paints a background (inferred theme is @style/CustomTheme) [Overdraw]
android:background="#004A82"
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
res\layout\custom_titlebar_with_logout.xml:6: Warning: Possible overdraw: Root element paints background #004A82 with a theme that also paints a background (inferred theme is @style/CustomTheme) [Overdraw]
android:background="#004A82"
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
res\layout\custom_titlebar_with_settings.xml:6: Warning: Possible overdraw: Root element paints background #004A82 with a theme that also paints a background (inferred theme is @style/CustomTheme) [Overdraw]
android:background="#004A82"
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
res\layout\login_screen.xml:5: Warning: Possible overdraw: Root element paints background @drawable/bg_app with a theme that also paints a background (inferred theme is @style/CustomTheme) [Overdraw]
android:background="@drawable/bg_app"
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
res\layout\queues_screen.xml:5: Warning: Possible overdraw: Root element paints background @drawable/bg_app with a theme that also paints a background (inferred theme is @style/CustomTheme) [Overdraw]
android:background="@drawable/bg_app"
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
res\layout\settings_screen.xml:5: Warning: Possible overdraw: Root element paints background #1D1D1D with a theme that also paints a background (inferred theme is @style/CustomTheme) [Overdraw]
android:background="#1D1D1D"
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
res\drawable-hdpi\bg_login.9.png: Warning: The resource R.drawable.bg_login appears to be unused [UnusedResources]
res\drawable-hdpi\btn_ok_xlarge.png: Warning: The resource R.drawable.btn_ok_xlarge appears to be unused [UnusedResources]
res\drawable-hdpi\no_xlarge.png: Warning: The resource R.drawable.no_xlarge appears to be unused [UnusedResources]
res\menu\settings_menu.xml: Warning: The resource R.menu.settings_menu appears to be unused [UnusedResources]
res\values\strings.xml:7: Warning: The resource R.string.loginMessage appears to be unused [UnusedResources]
<string name="loginMessage">Enter Your Login Credentials</string>
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
res\values\strings.xml:8: Warning: The resource R.string.CSQ_default appears to be unused [UnusedResources]
<string name="CSQ_default">Log In</string>
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
res\values\strings.xml:11: Warning: The resource R.string.default_time appears to be unused [UnusedResources]
<string name="default_time">00:00:00</string>
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
res\values\strings.xml:12: Warning: The resource R.string.oldest_in_queue appears to be unused [UnusedResources]
<string name="oldest_in_queue">Oldest Call In Queue: </string>
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
res\values\strings.xml:16: Warning: The resource R.string.add_to_queue appears to be unused [UnusedResources]
<string name="add_to_queue">Add To Queue</string>
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
res\layout\login_screen.xml:9: Warning: This LinearLayout view is useless (no children, no background, no id, no style) [UselessLeaf]
<LinearLayout
^
res\layout\custom_titlebar.xml:10: Warning: This RelativeLayout layout or its LinearLayout parent is useless; transfer the background attribute to the other view [UselessParent]
<RelativeLayout
^
res\layout\custom_titlebar_with_logout.xml:10: Warning: This RelativeLayout layout or its LinearLayout parent is useless; transfer the background attribute to the other view [UselessParent]
<RelativeLayout
^
res\layout\custom_titlebar_with_settings.xml:10: Warning: This RelativeLayout layout or its LinearLayout parent is useless; transfer the background attribute to the other view [UselessParent]
<RelativeLayout
^
res\layout\queue_list_item.xml:13: Warning: This TableRow layout or its TableLayout parent is possibly useless [UselessParent]
<TableRow
^
res\layout\queue_list_item.xml:45: Warning: This TableRow layout or its TableLayout parent is possibly useless [UselessParent]
<TableRow
^
res\layout\custom_titlebar.xml:3: Warning: The resource R.id.photo_titlebar appears to be unused [UnusedIds]
android:id="@+id/photo_titlebar"
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
res\layout\queue_list_item.xml:7: Warning: The resource R.id.nameTable appears to be unused [UnusedIds]
android:id="@+id/nameTable"
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
res\layout\queue_list_item.xml:14: Warning: The resource R.id.tableRow1 appears to be unused [UnusedIds]
android:id="@+id/tableRow1"
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
res\layout\queue_list_item.xml:19: Warning: The resource R.id.activeIndicatorDummy appears to be unused [UnusedIds]
android:id="@+id/activeIndicatorDummy"
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
res\layout\queue_list_item.xml:46: Warning: The resource R.id.tableRow2 appears to be unused [UnusedIds]
android:id="@+id/tableRow2"
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
res\layout\queue_list_item.xml:62: Warning: The resource R.id.callsInQueueLabel appears to be unused [UnusedIds]
android:id="@+id/callsInQueueLabel"
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
0 errors, 27 warnings
res\layout\queue_list_item.xml:7: Warning: The resource R.id.nameTable appears to be unused [UnusedIds]
android:id="@+id/nameTable"
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
res\layout\queue_list_item.xml:14: Warning: The resource R.id.tableRow1 appears to be unused [UnusedIds]
android:id="@+id/tableRow1"
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
res\layout\queue_list_item.xml:19: Warning: The resource R.id.activeIndicatorDummy appears to be unused [UnusedIds]
android:id="@+id/activeIndicatorDummy"
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
res\layout\queue_list_item.xml:46: Warning: The resource R.id.tableRow2 appears to be unused [UnusedIds]
android:id="@+id/tableRow2"
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
res\layout\queue_list_item.xml:62: Warning: The resource R.id.callsInQueueLabel appears to be unused [UnusedIds]
android:id="@+id/callsInQueueLabel"
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
0 errors, 27 warnings
层级查看器
调试性能问题的另一个有用的工具,特别是对于布局,是层次查看器。最基本的是,它会告诉你需要多长时间来展开布局。通过添加透视图,您可以从 Eclipse 中启动层次结构查看器;这类似于如果 DDMS 消失了,你会把它加回去。
层次结构查看器首先显示设备和模拟器的列表;从列表中单击应用的名称,然后单击加载视图等级。树形视图、树形总览和树形布局将打开,如图图 3-13 所示。树状视图显示了您在 XML 文件中定义的所有布局。我们在本章早些时候讨论了嵌套布局如何影响性能,而树形总览是一个很好的方式来查看你的布局的嵌套程度,并判断是否是时候将它们合并成一个RelativeLayout。树状视图显示了每个布局显示的时间,因此您可以确定需要调试和优化哪些视图来加快您的 UI。
图 3-13 。CallCenterApp 登录屏幕的层次结构查看器
在图 3-13 中,我们可以看到我们的登录视图花了将近 33 毫秒才显示出来。它还显示了哪些布局是登录视图的一部分,通过将鼠标悬停在特定视图上,您可以看到每个视图显示了多长时间。
层次查看器还包括一个完美的像素设计工具。我们不会在本书中涉及这一点。
Unix 工具
因为 Android 是在 Linux 上构建的,所以我们可以利用许多与 Linux 相同的 shell 命令工具来进行性能测试。主要工具关注总进程负载、单个进程细节和内存利用率。
顶端
顶部的命令会让你知道你的应用相对于设备上所有其他进程的位置。列表位置越高,消耗的资源就越多。您可以使用 adb shell 命令登录到电话,也可以从命令行使用 adb shell top 远程运行该命令。图 3-14 显示了结果。
图 3-14 。top 命令的输出
倾印系统〔??〕
Top 还会获取应用的进程 ID 或 PID,然后您可以将它用于 dumpsys 命令,如下所示:
adb shell dumpsys meminfo 1599
Dumpsys 将为您提供关于您的应用正在使用的内存和堆的信息;参见图 3-15 。
图 3-15 。dumpsys meminfo
本节中提到的所有 Unix 工具都在某个时间点进行测量。Procstats在 Android 4.4 或 KitKat 中引入,用于显示后台运行的应用将消耗多少内存和 CPU。使用命令查看procstats输出:
adb shell dumpsys procstats
结果如图 3-16 所示。
图 3-16 。倾印系统 procstats〔??〕
vmstat〔??〕
Vmstat 允许您查看设备上的虚拟内存级别;参见图 3-17 。这是一个简单的 Linux 命令,报告进程、内存、分页、块 IO、陷阱和 CPU 活动。“b”列显示哪些进程被阻止。使用如下命令:adb shell vmstat。
图 3-17 。dumpsys meminfo
摘要
在这一章中,我们已经看到了一些工具,它们首先发现您是否有性能问题,然后确定需要修复的调用;我们还看到了一些可以用来优化应用的技术。由于 Android SDK 和 Android 平台与 Unix 的密切关系,它们提供了大量工具来帮助您识别问题。