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. 静态方法更好
如果你的某个方法不需要访问对象的变量,那么将你的方法改为静态的,这样调用起来会快很多,因为它将不需要经过method table(译者:方法表,这里涉及到了java的虚分派和方法表,对字节码和jvm感兴趣的可以去单独搜一下)。这也是一个很好的实践,因为你可以让方法签名知道在调用方法的时候不会更改对象的状态。
6. 避免使用内部的Getters/Setters
在像C++这种语言中,经常会见到getters(比如 i=getCount())来代替直接访问变量(i=mCount)。这是一个非常好的习惯,因为编译器会使用内联访问,而且如果你相对字段做约束或者进行调试的时候,你可以随时的修改代码。
在Android中,这不是一个好的主意。函数的调用开销大,远远超过了变量的访问。如果是一个普通的类,你应该直接访问变量本身,如果是一个公共接口,还是尽量遵循面向对象编程的实践使用Getters/Setters。
7. 常量声明Final
考虑下面的变量声明:
static int intVal = 42;
static String strVal = "Hello, world!";

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

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

类文件不再需要方法了,因为常量转为了虚拟机进行处理。代码访问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)。当第一次使用,类会调用初始化函数来对每一个枚举值创建对象。每一个对象都有自己的静态变量,而且全部都保存在一个数组中(一个叫"$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和时钟时间的组合,并且因为系统的提升将变化。值得注意的是这些值是相对的——例如,添加一个成员变量目前需要的时间大约是添加一个本地变量的四倍
行为
时间
添加一个本地变量
1
添加一个成员变量
4
调用String.length()
5
调用一个空的静态native方法
5
调用一个空的静态方法
12
调用一个空的函数
12.5
调用一个空的接口方法
15
HashMap调用Iterator:next()
165
HashMap调用put()
600
从XML反射出1个View
22,000
反射出包含1个TextView的LinearLayout
25,000
反射出包含6个View的LinearLayout
100,000
反射出包含6个TextView的LinearLayout         
135,000
启动一个空的Activity
3,000,000
13. 总结
写出好的、有效的代码的最好的方式,就是去理解你的代码到底作了什么。如果你真的想要在List上通过迭代器使用增强的for循环来访问;让它成为一个深思熟虑的选择,而不是成为你代码的副作用。
俗话说有备无患!了解你在代码中引入的东西!你可以把你喜欢的东西混合在一起,不过必须慎重考虑你的代码在做什么,然后找机会去提升它的速度。
分类: 前端

1 条评论

· 2017年12月30日 22:47

Saved as a fаvorite, I really like your web site!

回复 取消回复

Avatar placeholder

您的电子邮箱地址不会被公开。 必填项已用*标注