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

转自:http://www.ituring.com.cn/article/177180

作者/ Erik Hellman

Factor10咨询公司资深移动开发顾问,曾任索尼公司Android团队首席架构师,主导Xperia系列产品开发;精通移动应用、Web技术、云计算和三维图形,定期在DroidCon、JFokus、JavaOne和其他专业开发人员大会上发表演讲。关于Erik的更多信息,可访问他的博客http://blog.hellsoft.se

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<String> names) {
    for (String name : names) {
        printName(name);
    }
}

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

// 不要在ArrayList上使用增强版的for循环
void loopFive(ArrayList<String> 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<String> mList = new LinkedList<String>();
    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<String> blockingQueue =
       new LinkedBlockingQueue<String>();

上面的一行代码能像前面的例子一样提供相同类型的阻塞队列,甚至能提供额外的线程安全操作。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是否是更有效的解决方案。

理和分配内存

Java中的自动内存管理有效消除了软件开发过程中许多最常见的问题。当不再需要记住为每个新建的对象释放内存时,开发者可以用省下的时间改善功能以及软件的整体质量。

但需要为这个功能付出代价,因为自动垃圾回收器会和应用程序并行运行。垃圾回收器会一直运行,并检查是否有可以回收的内存。这种行为意味着应用程序进程会和垃圾回收器竞争CPU时间,所以至关重要的一点是,确保垃圾回收器不管何时运行都不会占用太长时间。

此外,自动内存管理并不能保证不会有内存泄漏。如果引用了不再需要的对象,垃圾回收器不会收集它们,这将导致内存的浪费。如果一直分配对象,但从不释放,最终会导致OutOfMemory异常,应用程序也会崩溃。所以,要尽量避免在Android的主要组件中引用对象,否则,这些对象在应用程序的生命周期中可能永远不会被“垃圾回收”。

少对象分配

Java和Android中,自动内存管理最常见的问题是分配了无用的对象,导致垃圾回收器一直运行。考虑一种情况,一个代表一对整数的简单类:

public final class Pair {
   public int firstValue;
   public int secondValue;

   public Pair(int firstValue, int secondValue) {
       this.firstValue = firstValue;
       this.secondValue = secondValue;
   }
}

现在,假如在应用程序中接收了一个整数数组,把它们进行分组,然后使用sendPair方法。下面是一个内存分配做得很差的例子:

public void badObjectAllocationExample(int[] pairs) {
   if(pairs.length % 2 != 0) {
       throw new IllegalArgumentException(“Bad array size!”);
   }
   for(int i = 0; i < pairs.length; i+=2) {
       Pair pair = new Pair(pairs[i], pairs[i+1]);
       sendPair(pair);
} }

虽然这是个展示如何生成Pair对象的简单粗糙的例子(如果数组的大小是奇数的话可能会引起崩溃),但它说明了一个很常见的错误:在循环中分配对象。在上面的循环中,垃圾回收器将会做很多工作,并很可能耗尽CPU从而导致应用程序用户界面卡顿。如果开发者知道sendPair方法返回时并不会持有Pair对象的引用,那么解决方案很简单,在循环外创建Pair对象并重用,如下所示:

public void betterObjectAllocationExample(int[] pairs) {
   if(pairs.length % 2 != 0) {
       throw new IllegalArgumentException ("Bad array size!");
   }
   Point thePair = new Point(0,0);
   for (int i = 0; i < pairs.length; i+=2) {
       thePair.set(pairs [i], pairs [i+1]);
       sendPair(thePair);
   }
}

新版的方法确保了在整个运行过程中一直重用该对象。当方法返回时,只会有一次垃圾回收。请记住,应尽可能避免在循环中分配对象。

然而有时候无法避免在循环中创建对象,所以还需要采用某种方法处理这种情况。我们的解决方案是使用一个静态工厂方法按需分配对象,Joshua Bloch在《Effective Java中文版》一书的第一条中详细地描述了该方法。

这种方法在Android框架和API中很常见,它允许开发者使用一个按需填充的对象缓存。唯一的缺点是需要手动回收这些对象,否则缓存会一直是空的。

基于前面的例子,通过重构Pair类来使用一个简单的对象池重用对象。

public final class Pair {
    public int firstValue;
    public int secondValue;

    // 引用对象池中的下一个对象
    private Pair next;

    // 同步锁
    private static final Object sPoolSync = new Object();
    // 对象池中第一个可用的对象
    private static Pair sPool;

    private static int sPoolSize = 0;
    private static final int MAX_POOL_SIZE = 50;

    /**
     * 只能用obtain()方法获取对象
     */
    private Pair() {
    }

    /**
     * 返回回收的对象或者当对象池为空时创建一个新对象
     */
    public static Pair obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Pair m = sPool;
                sPool = m.next;
                m.next = null;
                sPoolSize--;
                return m;
            }
        }
        return new Pair();
    }

    /**
     * 回收该对象。调用该方法后需要释放所有对该实例的引用
     */
    public void recycle() {
        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }
}

注意,本例增加了多个字段,有静态的也有非静态的。可使用这些字段实现传统的Pair对象链表。只能通过obtain方法创建该类的对象。通过使用私有构造函数来防止在类外面创建对象。obtain方法首先会检查对象池中是否包含任何存在的对象,并删除列表中的第一个元素然后返回它。如果对象池为空,obtain方法会创建一个新的对象。要把对象重新放回池中,需要在使用完该对象时,对它调用recycle方法。这时,不能再有对该对象的引用。

修改Pair类后,之前的循环方法也需要修改。

public void bestObjectAllocationExample(int[] pairs) {
    if (pairs.length % 2 != 0) throw new IllegalArgumentException("Bad array size!");

    for (int i = 0; i < pairs.length; i += 2) {
        Pair pair = Pair.obtain();
        pair.firstValue = pairs[i];
        pair.secondValue = pairs[i + 1];
        sendPair(pair);
        pair.recycle();
    }
}

第一次运行这个方法会创建一个新的Pair实例,接下来的每次迭代会重用改对象。不过,下次再运行该方法时,不会再创建新的对象。另外,由于obtainrecycle是线程安全的,可以在多个并发线程中安全地使用这两个方法。唯一的缺点是,必须记住要手动调用recycle方法,不过这是一个很小的代价。这样就只会在应用退出时才会对Pair类进行垃圾回收。

Pair类的例子很琐碎,但是它描述了一个模式能明显减少类的创建。这个设计可能看起来很熟悉,因为它出现在Android源代码和API的多个地方。一些经常使用的类,比如MessageMotionEvent以及Parcel都通过实现这个模式来减少不必要的垃圾回收。之前的Pair类基本上就是复制Message类的实现。使用这种方法时记得在使用完对象后调用recycle方法,否则对象池将一直是空的。

Android中的多线程

编程中最难的部分之一编写在多个线程中执行的代码。这是对当今应用的一个要求,因为不可能只在一个线程中按顺序执行所有代码。Android应用程序从主线程开始运行,也称为UI线程(这里UI线程和主线程含义相同)。除非启动另一个线程或者通过隐式调用函数来启动一个线程,否则所有在Android应用中的操作都会运行在主线程中。这意味着,如果在主线程执行很耗时的操作(比如在onResume中运行代码),所有的绘制以及输入事件将被阻塞,直到该操作完成。所以,编写代码时首选需要牢记的是:确保永远不要阻塞主线程。

但是怎样才能知道一个方法是否在主线程中执行?Android官方文档指出:“默认情况下,应用的所有组件都运行在同一个进程和线程中(主线程)。”更具体点儿,Android组件(ActivityBroadcastReceiverService以及Application)的所有回调(基本上是所有的onX方法)都运行在主线程里。因此,ServiceonStartCommand方法和ActivityonResume也运行在同一个线程里。需要记住的时,阻塞上面任意一个方法,都可能导致系统“杀死”应用程序。

只要应用程序进程还在执行,主线程会一直运行。通过使用Looper类,主线程会在应用程序的生命周期中一直执行。Looper类会在当前线程中一直查询消息队列(使用MessageQueue类)。对该队列的查询会被阻塞直到有新的消息进入,这能确保空闲时线程进入休眠状态。所有对主线程的操作都是通过直接使用Handler对象或者间接使用部分Android API(比如,runOnUiThread方法)往队列里发送消息完成的。可以通过Context.getMainLooper()来查询应用程序主线程的Looper对象。

什么样的代码在主线程中执行才是安全的?什么样的代码需要放到其他线程中?严格地讲,只有那些必须在主线程执行的方法才能放在主线程中。其他一切操作都应放在另一个单独的线程中执行。实际情况下,那些不会耗时的操作也可以放在主线程中。如果能确保在另一个单独的线程中执行文件、数据库或者网络操作,通常主线程会是安全的。另外,对于某些应用或者游戏,开发人员可能会不定期执行一些与UI无关的计算,这些操作也应该放在一个单独的线程中执行。然而,也要确保同一时间不会运行太多线程,原因是CPU切换线程也会造成性能损失。

在编写Android代码时如何声明和管理各种线程?

Thread类

Thread类是Android中所有线程的基类,Java SE中也包含它。如果要在线程中执行代码,既可以创建一个具体的类(即继承自Thread的新类),也可以把实现Runnable接口的类对象传给Thread的构造函数。

本例需要遍历Objects的数组,从而把数据“上传”到服务器(用于上传的代码不是本例的一部分)。需要在一个单独的线程中执行此操作,否则会阻塞用户界面。此外,需要通过增加ProgressBar来更新上传的进度。下面的代码显示了通过实现Runnable接口来解决这个问题:

public class MyThread implements Runnable {
    private Object[] mInput;
    private Activity mActivity;
    private int mProgress = 0;

    public MyThread(Activity activity, Object... input) {
        mActivity = activity;
        mInput = input;
    }

    @Override
    public void run() {
        mProgress = 0;
        Runnable runnable = new Runnable() {
            public void run() {
                mActivity.findViewById(R.id.progressBar).
                    setVisibility(View.VISIBLE);
                ((ProgressBar) mActivity.
                    findViewById(R.id.progressBar)).setProgress(0);
            }
        };
        mActivity.runOnUiThread(runnable);

        // 循环并处理输入
        for (Object input : mInput) {
            // 上传到服务器 (用睡眠模拟)
               SystemClock.sleep(50);

            runnable = new Runnable() {
                public void run() {
                    ((ProgressBar) mActivity.
                            findViewById(R.id.progressBar)).
                            setMax(++mProgress);
                    ((ProgressBar) mActivity.
                            findViewById(R.id.progressBar)).
                            setProgress(mInput.length);
                }
            };
            mActivity.runOnUiThread(runnable);
        }

        runnable = new Runnable() {
            public void run() {
                mActivity.findViewById(R.id.progressBar).
                    setVisibility(View.INVISIBLE);
            }
        };
        mActivity.runOnUiThread(runnable);
    }
}

从上面的例子可以看出,每次更新UI都需要创建一个新的Runnable对象。这使得代码变得很乱,而且垃圾回收器还会进行不必要的对象回收,这些都是开发者要避免的。为了在主线程中使用runOnUiThread方法更新UI,必须使用Runnable

这种方案还有一个问题:因为只能对Thread实例调用一次start方法,所以每次执行操作都需要创建一个新的Thread对象。不断创建新的线程是非常昂贵的,本例还有改进的空间。总之,这不是一个非常灵活的方法,开发者应避免直接使用Thread类。

AsyncTask

AsyncTask是Android中比较流行的几个类中的一个,因为它很容易使用。它允许开发者定义一个运行在单独线程中的任务,还能在任务的不同阶段提供回调函数。这些回调函数被设计成无需使用runOnUiThread方法即可更新UI,这非常适合表示长时间运行的操作的进度。下面的示例使用AsyncTask来完成Thread例子中的功能:

public class MyAsyncTask extends AsyncTask<String, Integer, Integer> {
    private Activity mActivity;

    public MyAsyncTask(Activity activity) {
        mActivity = activity;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        // 下面的代码会运行在主线程中
        mActivity.findViewById(R.id.progressBar).
                setVisibility(View.VISIBLE);
        ((ProgressBar) mActivity.findViewById(R.id.progressBar)).
                setProgress(0);
    }

    @Override
    protected Integer doInBackground(String... inputs) {
        // 下面的代码不会运行在主线程中
        int progress = 0;
        for (String input : inputs) {
            // 把输入上传到服务器(用睡眠代替)
            SystemClock.sleep(50);
            publishProgress(++progress, inputs.length);
        }
        return progress;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        // 下面的代码会运行在主线程中
        ((ProgressBar) mActivity.findViewById(R.id.progressBar)).
                setMax(values[1]);
        ((ProgressBar) mActivity.findViewById(R.id.progressBar)).
                setProgress(values[0]);
    }

    @Override
    protected void onPostExecute(Integer i) {
        super.onPostExecute(i);
        // 下面的代码会运行在主线程中
        mActivity.findViewById(R.id.progressBar).
                setVisibility(View.INVISIBLE);
    }
}

上面的例子实现了四个回调函数,并在代码注释中表明了它们会运行在哪个线程。可以看到,onPreExecuteonProgressUpdateonPostExecute方法都运行在主线程,所以可以安全地在这些线程中更新UI。每次触发onProgressUpdate回调函数都会调用publishProgress,这样可以更新进度条。

通过AsyncTask类,开发者可以很容易在其他线程中执行耗时的任务,也可以在需要时很方便地和主线程通信。使用AsyncTask唯一的问题是该类的实例只能使用一次,这意味着每次执行操作都要新建一个MyAsyncTask对象。虽然是个轻量级的类(实际的线程是由ExecutorService管理的),但它不适合那些频繁的操作,因为这会快速聚集需要垃圾回收的对象,并最终导致应用程序界面卡顿。

此外,AsyncTask不能对操作设置执行时间,也无法间隔一段时间执行操作。它适合文件下载,以及不会频繁发生或通过用户交互等类似情况的操作。然而,由于容易实现,AsyncTask很可能是开发时首选的类。

Handler类

当需要更细粒度地控制在一个单独的线程中执行操作时,Handler类会是一个很有用的工具。该类允许开发者准确地控制操作的执行时间,还可以重复多次使用它。执行操作的线程会一直运行,直到被显式地终止。Looper类会处理幕后的事情,但开发者很少需要直接和它打交道,相反可以通过包装类HandlerThread创建它。下面的例子展示了如何在Activity中创建一个Handler实例。

public class SampleActivity extends Activity implements Handler.Callback {
    private Handler mHandler;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        // 使用Looper开启一个新线程
        HandlerThread handlerThread
                = new HandlerThread(“BackgroundThread”);
        handlerThread.start();
        // 创建Handler对象
        mHandler = new Handler(handlerThread.getLooper(), this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 关闭Looper线程
        mHandler.getLooper().quit();
    }

    @Override
    public boolean handleMessage(Message message) {
        // 处理消息...
        // 回收消息对象
        message.recycle();
        return true;
    }
}

通过新建的Handler对象,开发者可以安全地精确安排操作的执行时间。使用Handler类最常见的方式是发送Message。当向后台线程传递数据和参数时,这些消息对象简单、易于创建,并且可以重用。Message对象通常是由它的公有整型成员变量what定义的,可以在handleMessage回调函数中将其作为switch-case语句的一个标志位来使用它。还有两个名为arg1arg2的整型成员变量,它们用于创建低开销的参数,以及obj成员变量(可以存储任意单个对象的引用)。如果需要的话,还可以用setData(Bundle obj)方法设置更复杂的数据。我们可以使用多种方法给Handler发送消息,下面列出最常见的三种:

public void sendMessageDemo(Object data) {
    // 创建一个带有data参数的Message,然后立刻把它发送到handler执行
    Message.obtain(mHandler, SYNC_DATA, data).sendToTarget();
    // 立刻给handler发送一个简单的空消息
    mHandler.sendEmptyMessage(SYNC_DATA);
    // 给handler发送一个简单的空消息,该消息会在30秒后执行
    mHandler.sendEmptyMessageAtTime(SYNC_DATA,
            THIRTY_SECONDS_IN_MILLISECONDS);
    // 给handler发送带有arguments和obj参数的消息,并在两分钟后执行
    int recipient = getRecipientId();
    int priority = 5;
    Message msg = mHandler.obtainMessage(SYNC_DATA, recipient,
            priority, data);
    mHandler.sendMessageDelayed(msg, TWO_MINUTES_IN_MILLISECONDS);
}

前面两个例子表明既可以用Message类也可以用Handler对象创建和发送消息。在第三个和第四个例子中,可以看到如何精确到毫秒来安排消息的处理。

循环线程会从消息队列中读取Message对象,然后把它发送到回调函数中。多个Handler对象可以共用一个回调函数,就像代理方法一样,处理应用程序消息会很有用。甚至可以在ActivityService之间共享回调函数。实现回调函数最有效的方式是在实现它的类中保持所有代表what值的常量,然后用标准的switch-case语句处理每种消息类型。前面的例子在Activity中实现了回调函数,但是使用一个单独的类并把应用程序的Context传给它通常会更有用,因为这样就可以在应用程序的各个部分中使用它。下面是一个典型的回调函数示例:

// 用于what成员变量的常量值
public static final int SYNC_DATA = 10;
public static final int PING_SERVER = 20;

@Override
public boolean handleMessage(Message message) {
    switch (message.what) {
        case SYNC_DATA:
            // 执行耗时的网络输入/输出操作
            syncDataWithServer(message.obj);
            break;
        case PING_SERVER:
            // ping服务器,应该定期执行
            pingServer();
            break;
    }
    // 回收消息对象以便节省内存
    message.recycle();
    return true;
}

本例中的handleMessage回调只实现了两个操作,SYNC_DATAPING_SERVER。第一个可能会被用户事件触发,比如,保存文件或者准备好将新数据上传到服务器。第二个应该每间隔一段时间执行一次。然而,Handler类并没有方法间隔地发送消息,所以开发者要自己实现这种行为。

1. 间隔地执行操作

假设Activity一启动,就每分钟ping一次服务器。退出Activity后停止执行ping操作。

接下来的例子在onResume()onPause()中增加了对Handler的调用(前面有如何创建Handler实例的例子),这样就能在Activity显示或者消失时有效地执行上面的操作。在onResume方法中,把是否需要ping服务器的布尔值设置成true,然后立刻发送一个PING_SERVER消息(第一个ping操作应尽快发生)。消息会到达前面例子所述的回调函数中,并在该回调函数中执行pingServer()方法。

public class SampleActivity extends Activity implements Handler.Callback {
    private static final String PING_URL = "http://www.server.com/ping”;
    private static final int SIXTY_SECONDS_IN_MILLISECONDS = 60 * 1000;
    public static final int SYNC_DATA = 10;
    public static final int PING_SERVER = 20;
    private Handler mHandler;
    private boolean mPingServer = false;
    private int mFailedPings = 0;

    简单起见,移除了前面例子的代码

    @Override
    protected void onResume() {
        super.onResume();
        mPingServer = true;
        mHandler.sendEmptyMessage(PING_SERVER);
    }

    @Override
    protected void onPause() {
        super.onPause();
        mPingServer = false;
        mHandler.removeMessages(PING_SERVER);
    }

    private void pingServer() {
        HttpURLConnection urlConnection;
        try {
            URL pingUrl = new URL(PING_URL);
            urlConnection = (HttpURLConnection) pingUrl.openConnection();
            urlConnection.setRequestMethod("GET");
            urlConnection.connect();
                if (urlConnection.getResponseCode() == 200) {
                    mFailedPings = 0;
      }// 这儿也需要处理网络失败的情况
        } catch (IOException e) {
                // 还需要处理网络错误
        } finally {
            if (urlConnection != null) urlConnection.disconnect();
        }
        if (mPingServer) {
            mHandler.sendEmptyMessageDelayed(PING_SERVER,
               SIXTY_SECONDS_IN_MILLISECONDS);
        }
    }
}

pingServer()方法中,通过发送一个简单的HTTP请求来看服务器是否还处在活动中。一旦请求完成,需要检查是否要继续ping服务器,如果是的话,60秒后再发送一个PING_SERVER消息。在onPause()方法中,把该布尔值设置成false,然后移除消息队列中所有的PING_SERVER消息。

2. 在Handler中使用MainLooper

因为在构造函数中传递Looper对象可以为Handler分配线程,所以我们可以创建一个处理主线程消息的Handler。如果想避免使用runOnUiThread()方法,这样做特别有用。经常使用runOnUiThread会导致代码丑陋且低效。笔者经常在应用程序中使用这种方式,这样就可以在主线程和后台线程之间简单地发送消息。

@Override
public boolean handleMessage(Message message) {
    switch (message.what) {
        case SYNC_DATA:
            syncDataWithServer(message.obj);
            break;
        case SET_PROGRESS:
            ProgressBar progressBar =
                    (ProgressBar) findViewById(R.id.progressBar);
            progressBar.setProgress(message.arg1);
            progressBar.setMax(message.arg2);
            break;
    }
    message.recycle();
    return true;
}

前面的handleMessage例子可以接收两种类型的消息,SYNC_DATASET_PROGRESS。第一个需要运行在一个单独的线程中,而第二个由于要更新UI需要运行在主线程中。要做到这一点,需要创建一个额外的Handler对象来发送消息,以便主线程处理。

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    mMainHandler = new Handler(getMainLooper(), this);
    HandlerThread handlerThread = new HandlerThread(“BackgroundThread”);
    handlerThread.start();
    mHandler = new Handler(handlerThread.getLooper(), this);
}

需要注意的是本例的onCreate方法和之前的基本相同。唯一例外的地方是创建mMainHandler的一行代码。不是启动一个HandlerThread,而是简单地获取主线程的Looper对象。这并不影响主线程的运行,只需要一个额外的Handler,然后在主线程处理回调。系统会在回调函数中处理发送给该Handler的消息,该回调函数同样会处理第二个用于后台操作Handler的消息。如果要更新ProgressBar,只需如下所示发送一个简单的消息:

Message.obtain(mMainHandler, SET_PROGRESS, progress, maxValue).
sendToTarget();

任何必须在主线程运行的操作都可以使用这种方法。既可以像上面一样发送简单的Message对象,也可以给obj成员变量设置更复杂的数据,或者在setData中设置Bundle参数。只需要确保把消息发送给正确的Handler即可。

选择合适的线程

前面显示了三种在Android上创建和使用线程的方式。API中和线程相关的类还有ExecutorServiceLoaderExecutorService适合处理并行运行的多个任务,这非常适合编写响应多客户端的服务器应用。AsyncTask内部同样使用ExecutorService处理多线程。如果希望能够并行执行多个AsyncTask,也可以通过使用正确的ExecutorService来完成。

如果需要一个专门的线程来进行操作,可以从前面所示的三个例子开始。不建议直接使用Thread类,除非是要完全控制线程的执行。大多数情况下推荐使用AsyncTaskHandler类,具体使用哪一个取决于具体的需求。如果不是很频繁地执行操作,比如超过每分钟一次,那么AsyncTask可能是个不错的选择。如果需要安排操作的时间或者需要快速间隔地执行操作,Handler会是更好的选择。从长远来看,使用Handler生成的代码更少,不过AsyncTask更容易使用。

小结

本文介绍了Java SE 5.0的几个高级的特性,有了JIT Dalvik虚拟机,开发者就可以在Android上安全地使用它们了。了解和使用这些特性可以简化代码编写,从而让代码更易于测试和维护。本文同样介绍了怎样使用java. util.concurrent包中的并发API,而不需要自己实现队列和锁。重复造轮子是一个很常见但是很大的错误:你需要测试和维护更多的代码,而代码多了也更容易引入bug。

本文同样解释了怎样避免内存分配的诸多陷阱。如果在代码中创建了很多临时的、生命周期短的变量,应用程序很可能在用户界面上表现不佳。高效且安全地重用对象可以带来更流畅的用户体验。

本文最后介绍了三种在Android上使用线程的方法,但只推荐使用其中的两个(AsyncTaskHandler)。多线程是一个复杂的话题,往往是很多难以发现的bug的原因。始终尝试使用现有的工具类来处理线程,因为它们会让事情变得更简单,且允许开发者关注自己代码的功能。

Java是一门强大的语言,它使开发人员更容易表达他们想要实现的目标。学会如何更有效地使用它会让你成为更优秀的开发者,并帮助开发者创建高质量的代码。

发布了270 篇原创文章 · 获赞 273 · 访问量 377万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览