如何在Android上编写高效的Java代码

2024-11-25 22:44:29
推荐回答(1个)
回答1:

Java平台一般有三个版本:Java ME(微型版,用于某些手机)、Java SE(标准版,用于台式电脑)、Java EE(企业版,用于服务器端应用)。在谈到Java时,我们通常是指Java SE,因为只有这个版本包含虚拟机和编译器。

首先,Java代码会被编译成称为字节码的中间格式。当字节码在目标电脑上运行时,虚拟机会快速将它解析成目标电脑硬件和操作系统所需要的本机格式。

除了为开发者提供“一次编写,到处运行”的优势,Java还能通过垃圾回收器(GC)实现自动内存管理,开发者可免去手动在代码中释放无用对象的内存。虽然这个功能非常有用,且大大降低了在代码中引入内存问题的风险,但是它会增加运行时的开销,因为需要不停地执行垃圾回收进程。

本文开头将比较Java SE和用于Android开发的Java之间的差异。首先我会介绍开发者习惯的Java
SE语言结构以及它们是如何在Android上运行的。其次,我会介绍如何优化Android中的Java代码,如何优化内存分配,以及如何恰当地处理多线程。

比较Android上的Dalvik Java和Java SE

虽然远在Android出现之前,开发者就能用Java编程语言为移动设备编写应用程序,但它只是Java中功能极为有限的一个版本,称为Java
ME(微型版)。不同的移动设备还需编写不同的代码,因此,写一个应用程序就能在支持Java
ME的任何手机上运行是几乎不可能的。此外,由于当时不存在很好的在线商店,应用发布过程极其复杂。

Android的问世为开发者提供了构建智能手机强大应用的机会,开发者只需用Java编程语言以及他们熟知的标准Java
API编写代码。然而,尽管Android开发者仍使用Java SE编译器来编译应用程序,你会发现,James
Gosling开发的Java和Android设备上的Java存在许多不同之处。

在Android设备上运行的VM(虚拟机)称为Dalvik。它最初由谷歌的Dan
Bornstein开发,适用于CPU和内存受限的移动设备。Java SE和Dalvik Java存在一些差异,主要体现在虚拟机上。Java
SE使用了栈机设计,而Dalvik被设计成了基于寄存器的机器。Android SDK中有一个dx工具,它会把Java
SE栈机器的字节码转换成基于寄存器的Dalvik机器字节码,该转换步骤由IDE自动完成。

基于栈的虚拟机和基于寄存器的虚拟机的定义以及差异将不列入我们的讨论范围。由于历史原因,Android使用基于寄存器的虚拟机。虽然基于寄存器的虚拟机最多可以比基于栈的虚拟机快32%,但这只限于执行时解释字节码的虚拟机(也就是说,解释型虚拟机)。在Android
2.2版本(也称为Froyo)之前,Dalvik虚拟机都是纯解释型的。Froyo版本引入了JIT编译器(即时编译),这是Java
SE很早就有的一个优势。

JIT编译,也称为动态翻译。它在执行前把字节码翻译成本机代码(如图1所示),这样主要有两个好处。首先,它消除了那些纯解释型虚拟机的开销;其次,它能对本机代码执行优化,这通常是静态编译代码无法做到的。例如,JIT编译器可以在它运行的CPU上选择最合适的优化,也可以根据应用程序的输入来分析代码是如何运行的,以便进行下一步的优化。

图1 Android Java和Java SE翻译步骤

虽然Android的Dalvik JIT编译器有很大的发展前景,但要达到如Java SE的JIT编译器般稳定、成熟度尚需很长一段时间。不过,Dalvik JIT的出现为Android提供了巨大的性能优势,而且它也在不断得以改善。

JAVA
SE虚拟机和Dalvik虚拟机的另一个区别是,后者进行了优化,可运行在同一个机器上的多个实例中。它在开机时会启动一个叫做zygote的进程,该进程会创建第一个Dalvik实例,由这个实例创建所有其他的实例。当应用程序启动时,zygote进程会收到一个创建新虚拟机实例的请求,并给该应用程序创建一个新进程(如图2所示)。如果开发者已习惯于Java

SE开发,这样的设计可能看起来不切实际,但它有一个很大的优势,可以避免由一个应用程序运行失败导致Dalvik虚拟机崩溃,继而引发多应用程序崩溃。

图2 在Android中启动新Dalvik虚拟机实例

Android和Java
SE除了运行的虚拟机不同之外,它们实现API的方式也不一样。Android中属于java和javax包中的API都来自Apache
Harmony(这是一个开源项目,旨在重新实现Java SE软件栈,该项目从2011年11月不再维护)。在开发方面,这些API和Java
SE包中的类似,但也存在一些差别。例如,谷歌对HttpUrlConnection类进行了Java SE版本中所没有的重大升级。

此外,Android平台移除了Java
SE中无关的API。例如,Swing/AWT包被完全移除,因为Android使用不同的UI框架。其他被移除的API还有RMI、CORBA、ImageIO和JMX。它们或者被替换为特定的Android版本(在android包空间内),或者因为一些实际原因根本不存在。

优化Android上的Java代码

经过多年的改进,Java
SE具备了一些简化编写复杂代码结构的新特性。其中的一些特性会让整个流程变得更简单,但开发者需要了解何时以及如何正确地使用它们。另外,由于Java

SE大多用于服务器端开发(使用Java企业版的API),因而开发人员专门对服务器端Java代码进行了优化。注解和Java虚拟机对脚本语言的支持就是对服务器端开发进行优化的例证。虽然这些工具在构建后端开发时很强大,但在开发Android客户端代码时,这些特性的作用很小,甚至起反作用。Java开发者已经习惯于无限量的RAM和CPU,而Android开发需要密切关注性能和内存分配。简单地说,开发者需要使用稍微不同的方法对待Android和后端的开发。

然而,随着Android的首次发布,情况有所改变。曾经一些在Android上尽量不用的Java规范重新被推荐,这主要因为Android目前的JIT编译器解决了这些规范导致的性能问题。

本文将讨论编写Android应用程序需要了解的Java代码。我们不会深究Java编程语言的细节,而是重点关注对Android开发重要的东西。不过,开发者仍需了解,大多数适用于Java SE的规则和建议同样适用于Android和Dalvik虚拟机。

Android上的类型安全枚举

Java SE 5.0新增了许多方便开发者的新特性。其中最值得期待的是引入了类型安全枚举。枚举在代码中用来表示属于某一组的几个选择。在早期版本的Java中,可以用多个整型常量解决这个问题。虽然这在技术上可行,但是很容易出错。请看下面的代码:
public class Machine {
public static final int STOPPED = 10;
public static final int INITIALIZING = 20;
public static final int STARTING = 30;
public static final int RUNNING = 40;
public static final int STOPPING = 50;
public static final int CRASHED = 60;
private int mState;

public Machine() {
mState = STOPPED;
}

public int getState() {
return mState;
}

public void setState(int state) {
mState = state;
}
}

问题是,虽然这些常量是期望的,但是没有机制保证setState()方法接收不同的值。如果要在设置方法中添加检查,那么一旦得到的是非预期值,开发者就需要处理错误。开发者所需要的是在编译时检查非法赋值。类型安全的枚举解决了这个问题,如下所示:
public class Machine {
public enum State {
STOPPED, INITIALIZING, STARTING, RUNNING, STOPPING, CRASHED
}
private State mState;

public Machine() {
mState = State.STOPPED;
}

public State getState() {
return mState;
}

public void setState(State state) {
mState = state;
}
}

注意在声明不同类型安全值的地方新加的内部枚举类。这在编译时就会解决非法赋值的问题,所以代码更不容易出错。

如果Dalvik虚拟机还没有JIT编译器优化代码,不建议在Android平台上使用枚举类型,因为和使用整型常量相比,这种设计带来的内存和性能损失更大。这就是为什么在一些老版本的Android

API中还存在如此多的整型常量的原因。如今有了更强的JIT编译器以及一个不断改进的Dalvik虚拟机,开发者不必再担心这个问题,放心大胆地使用类型安全枚举即可。

然而,仍然存在一些情况使用整型常量是更好的选择。像int这样的Java基本类型,不会增加GC的开销。此外,Android SDK中许多已有的API仍然依赖基本类型,比如Handler类——在这种情况下,你没有太多的选择。

Android中增强版的for循环

Java SE 5.0还引入了增强版的for循环,提供了一个通用的缩写表达式来遍历集合和数组。首先,比较以下五种方法:
void loopOne(String[] names) {
int size = names.length;
for (int i = 0; i < size; i++) {
printName(names[i]);
}
}

void loopTwo(String[] names) {
for (String name : names) {
printName(name);
}
}

void loopThree(Collection names) {
for (String name : names) {
printName(name);
}
}

void loopFour(Collection names) {
Iterator iterator = names.iterator();
while (iterator.hasNext()) {
printName(iterator.next());
}
}

// 不要在ArrayList上使用增强版的for循环
void loopFive(ArrayList names) {
int size = names.size();
for (int i = 0; i < size; i++) {
printName(names.get(i));
}
}

上面显示了四种不同遍历集合和数组的方式。前面两种有着相同的性能,所以如果只是读取元素的话,可以放心地对数组使用增强版for循环。对Collection对象来说,增强版for循环和使用迭代器遍历元素有着相同的性能。ArrayList对象应避免使用增强版for循环。

如果不仅需要遍历元素,而且需要元素的位置,就一定要使用数组或者ArrayList,因为所有其他Collection类在这些情况下会更慢。

一般情况下,如果在读取元素几乎不变的数据集时对性能要求很高,建议使用常规数组。然而,数组的大小固定,添加数据会影响性能,所以编写代码时要考虑所有因素。

队列、同步和锁

通常情况下,应用程序会在一个线程中生产数据,在另一个线程中使用它们。常见的例子是在一个线程中获取网络上的数据,在另一个线程(操作UI的主线程)中把这些数据展现给用户。这种模式称为生产者/消费者模式,在面向对象编程课程中,开发者用算法来实现该模式可能要花上几个小时。下面会介绍一些简化生产者/消费者模式实现的现成类。

1. 更智能的队列

虽然已有现成的类并能用更少的代码实现该功能,但许多Java开发者仍然选择使用LinkedList以及同步块实现队列功能。开发者可在java.util.concurrent包中找到同步相关的类。此外,本包还包含信号量、锁以及对单个变量进行原子操作的类。考虑下面使用标准的LinkedList实现线程安全队列的代码。
public class ThreadSafeQueue {
private LinkedList mList = new LinkedList();
private final Object mLock = new Object();

public void offer(String value) {
synchronized (mLock) {
mList.offer(value);
mLock.notifyAll();
}
}

public synchronized String poll() {
synchronized (mLock) {
while (mList.isEmpty()) {
try {
mLock.wait();
} catch (InterruptedException e) {
//简洁起见忽略异常处理
}
}
return mList.poll();
}
}
}

虽然这段代码是正确的,并有可能在考试中得满分,但实现和测试这样一段代码只是在浪费时间。实际上,所有前面的代码可用下面一行代替。
LinkedBlockingQueue blockingQueue =
new LinkedBlockingQueue();

上面的一行代码能像前面的例子一样提供相同类型的阻塞队列,甚至能提供额外的线程安全操作。java.util.concurrent包含许多可选的队列以及并发映射类,所以,一般情况下,建议使用它们,而不是像之前的示例那样使用更多代码。

2. 更智能的锁

Java提供的synchronized关键字允许开发者创建线程安全的方法和代码块。synchronized关键字易于使用,也很容易滥用,对性能造成负面影响。当需要区分读数据和写数据时,synchronized关键字并不是最有效的。幸好,java.util.concurrent.locks包中的工具类对这种情况提供了很好的支持。
public class ReadWriteLockDemo {
private final ReentrantReadWriteLock mLock;
private String mName;
private int mAge;
private String mAddress;

public ReadWriteLockDemo() {
mLock = new ReentrantReadWriteLock();
}

public void setPersonData(String name, int age, String address) {
ReentrantReadWriteLock.WriteLock writeLock = mLock.writeLock();
try {
writeLock.lock();
mName = name;
mAge = age;
mAddress = address;
} finally {
writeLock.unlock();
}
}

public String getName() {
ReentrantReadWriteLock.ReadLock readLock = mLock.readLock();
try {
readLock.lock();
return mName;
} finally {
readLock.unlock();
}
}

// 重复代码不再赘述
}

上面的代码展示了在什么地方使用ReentrantReadWriteLock,它允许多个并发线程对数据进行只读访问,并确保同一时间只有一个线程写入相同的数据。

在代码中使用synchronized关键字仍然是处理锁问题的有效方法,但无论何种情况下,都要考虑ReentrantReadWriteLock是否是