Android程序性能设计最佳实践

Android应用应该要很快,更精确的说应该是要有效率。那就是说移动设备环境中有限的计算能力和数据存储,很小的屏幕,有限的电池寿命中要更有效率。
这篇博客我就会向你展示为性能而设计的最佳实践。

1. 避免创建对象
对象的创建在android中开销要比在java中大的多。尽量去避免创建一个对象,越多的对象意味着越多的垃圾回收,越多的垃圾回收意味着用户会觉得有点“小卡”。
一般的说,尽可能避免创建短暂的临时变量,更少的对象创建意味着更少的垃圾回收,将会提升用户体验。如果能创建一个“池”来管理对象的话,那是最好的了。

2. 将阻塞操作从UI线程中剥离出来
使用AsyncTask, Thread, IntentService,或者简单的后台Service去做大开销的操作。使用Loaders去简化管理很长时间加载数据的状态就像一个指针。如果一个操作需要消耗时间和资源,那就放到另外的进程中异步进行,这样你的程序就能够继续响应并且用户也可以进行操作。

3. 使用Native方法
同样一个Java循环,C/C++代码可以快10到100倍。

4. 实现优于接口
假设你有一个HashMap对象,你可以使用通用的Map来声明这个HashMap:

Map myMap1 = new HashMap();

HashMap myMap2 = new HashMap();

哪一种更好?
传统的观点认为你应该使用Map,因为它允许你更改为只要实现了Map接口的底层实现。传统观点对于常规编程是正确的,但是对于嵌入式系统来说就不是那么好了。调用一个接口引用的方法相对于调用一个固定实现的引用要多花费2倍的时间。
如果你HashMap能处理你要做的事情,那么使用Map来申明就没有一点价值了。让IDE帮你重构你的代码,就算你对代码还没有头绪,使用Map也是没什么价值的。(当然,公共的API可能有不同:一个好的API肯定胜过小的性能问题)

5. 静态方法更好
如果你的方法不需要访问一个对象的变量,那么将你的方法改为静态的。调用起来会快很多,因为它将不需要经过virtual method table。这也是一个很好的实践,因为你可以告诉方法签名在调用方法的时候不会更改对象的状态。

6. 避免使用内部的Getters/Setters
在像C++这种语言中,经常会见到getters(比如 i=getCount())来代替直接访问变量(i=mCount)。这是一个非常好的习惯,因为编译器会使用内联访问,而且如果你相对字段做约束或者进行调试的时候,你可以随时的修改代码。
在Android中,这是一个不好的想法。虚拟函数的调用开销很大,远远超过了变量的访问。如果是一个类,你应该直接访问变量,如果是一个公共接口,还是尽量遵循面向对象编程的实践使用Getters/Setters。

7. 常量声明Final
考虑下面的变量声明:
static int intVal = 42;
static String strVal = "Hello, world!";

编译器生成一个类的构造方法,叫<clinit>,它会在类第一次使用的时候触发。方法会将42保存到intVal,从String表里获取一个引用给strVal。当这些值被引用后,他们访问时就能够去查找了。

我们可以使用final关键字来提升性能:
static final int intVal = 42;
static final String strVal = "Hello, world!";

类文件不再需要<clinit>方法了,因为常量转为了虚拟机进行处理。代码访问intVal的时候直接就能拿到42,访问strVal的时候开销也会相对更小。

声明类或者方法final并不能获得性能上的好处,不够能够有其他的效果。比如,如果不想让子类重写getter方法,那么就可以声明final。

你也可以本地变量final。然而这不会有任何的性能效益。对于本地变量,使用final仅仅能让代码语义更加清晰(或者你可以让匿名类访问到这个变量)。

8. 小心使用增强的for循环
增强行的for循环(也可以说是for-each循环)可以使用在任何实现了iterable接口的集合上。对于这些对象,iterator会调用hasNext()和next()接口来创建,对于ArrayList,你最好避开这种方式,不过对于其他种类的集合,增强型for循环和显式的使用迭代循环相差无几。

下面代码展示了增强型的for循环:

public class Foo {
    int mSplat;
    static Foo mArray[] = new Foo[27];

    public static void zero() {
        int sum = 0;
        for (int i = 0; i < mArray.length; i++) {
            sum += mArray[i].mSplat;
        }
    }

    public static void one() {
        int sum = 0;
        Foo[] localArray = mArray;
        int len = localArray.length;

        for (int i = 0; i < len; i++) {
            sum += localArray[i].mSplat;
        }
    }

    public static void two() {
        int sum = 0;
        for (Foo a: mArray) {
            sum += a.mSplat;
        }
    }
}

zero()检索static变量两次,而且每次循环都会获取数组的长度。

one()将所有东西就放到了临时变量中,避免了查找。

two()使用1.5版本java的增强型for循环,由编译器来生成代码,复制数组引用和数组长度到本地变量,产生一个好的访问数组方案,它会产生一个额外的本地加载存储(保存a这个对象),它会比one()要慢一点点。

总结就是:增强型for循环有更好的语义和代码结构,但是要谨慎使用它,因为有可能会有额外的创建对象开销。

9. 避免Enums
枚举非常的方便,但是不幸的是他的速度的大小都是让人痛苦的。比如说:

public class Foo {
   public enum Shrubbery { GROUND, CRAWLING, HANGING }
}

会编译成900byte的.class文件 (Foo$Shrubbery.class)。当第一次使用,类会调用初始化函数<init>来对每一个枚举值创建对象。每一个对象都有自己的静态变量,而且全部都保存在一个数组中(一个叫"$VALUES"的静态变量)。这么多的代码,仅仅只是为了三个整数。

下面这句代码:

Shrubbery shrub = Shrubbery.GROUND;

一个静态变量的查找。如果"GROUND"是一个静态的int常量,编译器会把它当做一个已知常量并且使用内联。

从另一个方面说,你当然会从枚举类型中获得很多好用的API和编译时的检查。所以,通常需要这样来权衡:你应该在所有公共API的地方尽量使用枚举类型,不过在性能重要的时候,尽量避免使用。

在一些环境中,通过ordinal()方法来获取enum的数值很有用,比如:

for (int n = 0; n < list.size(); n++) {
    if (list.items[n].e == MyEnum.VAL_X)
       // do stuff 1
    else if (list.items[n].e == MyEnum.VAL_Y)
       // do stuff 2
}

然后:

int valX = MyEnum.VAL_X.ordinal();
int valY = MyEnum.VAL_Y.ordinal();
int count = list.size();
MyItem items = list.items();

for (int  n = 0; n < count; n++)
{
     int  valItem = items[n].e.ordinal();

     if (valItem == valX)
       // do stuff 1
     else if (valItem == valY)
       // do stuff 2
}

在一些案例中,这将会更快,尽管这没有保证。


10. 对内部类使用Package访问权限
考虑一下下面的类定义:

public class Foo {
    private int mValue;

    public void run() {
        Inner in = new Inner();
        mValue = 27;
        in.stuff();
    }


    private void doStuff(int value) {
        System.out.println("Value is " + value);
    }

    private class Inner {
        void stuff() {
            Foo.this.doStuff(Foo.this.mValue);
        }
    }
}

关键的事事:我们定义了一个内部类(Foo$Inner)直接访问了外部类的private方法和private实例。这是合法的,而且代码打印出了我们期望的"Value is 27"。

问题是Foo$Inner在技术上来说,是一个完全独立的类。它来直接访问Foo的私有成员是违法的。为了弥补这个问题,编译器会生成下面的方法:

/*package*/ static int Foo.access$100(Foo foo) {
    return foo.mValue;
}
/*package*/ static void Foo.access$200(Foo foo, int value) {
    foo.doStuff(value);
}

内部类代码会调用方法来访问外部的mValue或者调用外部的doStuff方法。这意味着上面的代码真正的情况是,你是通过的访问器访问而不是通过直接访问的代码。之前我们就讨论过了使用访问器要比直接访问变量更慢,所以这就是一个特定的语言习惯形成的一种"隐形"的性能影响。


我们可以通过声明package访问空间而不是private访问空间来避免这个问题。这将会运行的更快,而且避免了生成方法产生的开销。(不幸的是,这可能让在同一个package下的其他包能够直接访问这个变量,这违反了面向对象的设计思想。再次说明一下,如果你要设计一个公共的API,你就要仔细考虑一下了。)

11. 避免Float
在奔腾CPU发布之前,几乎所有的游戏开发者都会尽量使用整数运算。奔腾CPU将浮点数协处理器设置成了内置功能。通过交叉整数和浮点操作使游戏比以前纯整数运算的时候要快。桌面程序现在常见的做法是随意使用float。

不幸的是,嵌入式处理器通常没有浮点支持,所以所有的float和double操作开销都很大。一些基本的浮点操作可以在一毫秒的顺序完成。

同样,即使是整数,一些芯片支持乘法,但缺乏除法。在这种情况下,在软件执行整数除法和模量操作时,想想如果你设计一个哈希表或做大量的数学。

12. 一些简单的性能数字
为了说明我们的一些想法,我们对一些基本的行为列出了近似运行时间。请注意,这些值不应被视为绝对数字:他们是CPU和时钟时间的组合,并且对于系统的改进将变化。值得注意的是这些值是相对于彼此——例如,添加一个成员变量目前需要的时间大约是添加一个本地变量的四倍

Action
Time
Add a local variable
1
Add a member variable
4
Call String.length()
5
Call empty static native method
5
Call empty static method
12
Call empty virtual method
12.5
Call empty interface method
15
Call Iterator:next() on a HashMap
165
Call put() on a HashMap
600
Inflate 1 View from XML
22,000
Inflate 1 LinearLayout containing 1 TextView
25,000
Inflate 1 LinearLayout containing 6 View objects
100,000
Inflate 1 LinearLayout containing 6 TextView objects
135,000
Launch an empty activity
3,000,000

13. 总结
写出好的、有效的代码的最好的方式,就是去理解你的代码到底作了什么。如果你真的想要在List上通过迭代器使用增强的for循环来访问;让它成为一个深思熟虑的选择,而不是成为副作用。
俗话说有备无患!知道你引入了什么,插入一些你最喜欢的东西混合在一起也可以,不过必须慎重考虑你的代码在做什么,然后找机会去提升它的速度。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。