Designing For Performance

Android 开发者指南 开发者指南(17) —— Designing For Performance 


前言 

本章内容为开发者指南(Dev Guide)/Best Practices/Designing For Performanc,这里 译为“性能优化”,版本为 Android3.1 r1,翻译来自:"[email protected]",欢迎大家访问 他的博客:"http://admires.iteye.com/",再次感谢"[email protected]" !期待你一起 参与翻译 Android 的相关资料,联系我 [email protected]


Designing for Performance

 译者署名: [email protected] 

译者链接:http://admires.iteye.com/ 版本:Android 3.1 r1 

原文 

http://developer.android.com/guide/practices/design/performance.html 

性能优化

 Android 应用程序运行的移动设备受限于其运算能力,存储空间,及电池续航。由此,它必须是高效的。 电池续航可能是一个促使你优化程序的原因, 即使他看起来已经运行的足够快了。 由于续航对用户的重要性,当电量耗损陡增时,意味这用户迟早会发现是由于你的程序。 

虽然这份文档主要包含着细微的优化,但这些绝不能成为你软件成败的关键。选择合适的 算法和数据结构永远是你最先应该考虑的事情,但这超出这份文档之外。 


简介 

写出高效的代码有两条基本的原则: 不作没有必要的工作。 尽量避免内存分配。


明智的优化 

这份文档是关于 Android 规范的细微优化,所以先确保你已经了解哪些代码需要优化,并 且知道如何去衡量你所做修改所带来的效果(好或坏)。开发投入的时间是有限的,所以明智的 时间规划很重要。 (更多分析和笔记参见总结。) 这份文档同时确保你在算法和数据结构上作出最佳选择的同时, 考虑 API 选择所带来的潜在 影响。使用合适的数据结构和算法比这里的任何建议都更有价值,优先考虑 API 版本带来的影 响有助于你找到更好的实现。(这在类库代码中更为重要,相比应用代码) 

(如果你需要这样的建议,参见 Josh Bloch's Effective Java, item 47.) 

在优化 Android 程序时,会遇到的一个棘手问题是,保证你的程序能在不同的硬件平台上 运行。虚拟机版本和处理器各部相同,因此运行在之上的速度也大不一样。但这并且不是简单的 A 比 B 快或慢,并能在设备间做出排列。特别的,模拟器上只能评测出一小部分设备上体现的 东西。 有无 JIT 的设备间也存在着巨大差异, JIT 设备上好的代码有时候会在无 JIT 的设备上 在 表现的并不好。 如果你想知道一个程序在设备上的具体表现,就必须在上面进行测试。

 避免创建不必要的对象 

对象创建永远不会是免费的。 每个线程的分代 GC 给零时对象分配一个地址池以降低分配开 销,但往往内存分配比不分配需要的代价大。 

如果在用户界面周期内分配对象,就会强制一个周期性的垃圾回收,给用户体验增加小小的 停顿间隙。Gingerbread 中提到的并发回收也许有用,但不必要的工作应当被避免的。 

因此,应该避免不必要的对象创建。

下面是几个例子: 

如果有一个返回 String 的方法,并且他的返回值常常附加在一个 StringBuffer 上, 改变声明和实现,让函数直接在其后面附加,而非创建一个短暂存在的零时变量。

当从输入的数据集合中读取数据时,考虑返回原始数据的子串,而非新建一个拷贝.这 样你虽然创建一个新的对象,但是他们共享该数据的 char 数组。(结果是即使仅仅使 用原始输入的一部分,你也需要保证它的整体一直存在于内存中。) 

一个更彻底的方案是将多维数组切割成平行一维数组:

 Int 类型的数组常有余 Integer 类型的。推而广之,两个平行的 int 数组要比一个 (int,int)型的对象数组高效。这对于其他任何基本数据类型的组合都通用。 如果需要实现一个容器来存放元组(Foo,Bar),两个平行数组 Foo[],Bar[]会优于一 个(Foo,Bar)对象的数组。(例外情况是:当你设计 API 给其他代码调用时,应用 好的 API 设计来换取小的速度提升。但在自己的内部代码中,尽量尝试高效的实现。) 通常来讲,尽量避免创建短时零时对象.少的对象创建意味着低频的垃圾回收。而这对于用 户体验产生直接的影响。 


性能之谜 

前一个版本的文档给出了好多误导人的主张,这里做一些澄清: 在没有 JIT 的设备上, 调用方法所传递的对象采用具体的类型而非接口类型会更高效 (比如, 传递 HashMap map 比 Map map 调用一个方法的开销小,尽管两个 map 都是 HashMap). 但这并不是两倍慢的情形,事实上,他们只相差 6%,而有 JIT 时这两种调用的效率不相上下。 在没有 JIT 的设备上,缓存后的字段访问比直接访问快大概 20%。而在有 JIT 的情况下, 字段访问的代价等同于局部访问,因此这里不值得优化,除非你觉得他会让你的代码更易读(对 于 final ,static,及 static final 变量同样适用) 

用静态代替虚拟 

如果不需要访问某对象的字段,将方法设置为静态,调用会加速 15%到 20%。这也是 一种好的做法,因为你可以从方法声明中看出调用该方法不需要更新此对象的状态。 

避免内部的 Getters/Setters

在源生语言像 C++中,通常做法是用 Getters(i=getCount())代替直接字段访问 (i=mCount)。这是 C++中一个好的习惯,因为编译器会内联这些访问,并且如果需要约束 或者调试这些域的访问,你可以在任何时间添加代码。 而在 Android 中,这不是一个好的做法。虚方法调用的代价比直接字段访问高昂许多。通常根据面向对象语言的实践,在公共接口中使用 Getters 和 Setters 是有道理的,但在一个字 段经常被访问的类中宜采用直接访问。 无 JIT 时,直接字段访问大约比调用 getter 访问快 3 倍。

有 JIT 时(直接访问字段开销等同于 局部变量访问) 要快 7 倍。 Froyo 版本中确实如此 , 在 但以后版本可能会在 JIT 中改进 Getter 方法的内联。

 对常量使用 Static Final 修饰符 

考虑下面类首的声明: 

static int intVal = 42;
static String strVal = "Hello, world!";

编译器会生成一个类初始化方法<clinit>,当该类初次被使用时执行,这个方法将 42 存入 intVal 中,并得到类文件字符串常量 strVal 的一个引用。当这些值在后面被引用时,他们通过 字段查找进行访问。

 我们改进实现,采用 final 关键字: 

static final int intVal = 42;
static final String strVal = "Hello, world!";

类不再需要<clinit>方法,因为常量通过静态字段初始化器进入 dex 文件中。引用 intVal 的代码,将直接调用整形值 42;而访问 strVal,也会采用相对开销较小的“字符串常量”(原文: “sring constant”)指令替代字段查找。(这种优化仅仅是针对基本数据类型和 String 类型常 量的,而非任意的引用类型。但尽可能的将常量声明为 static final 是一种好的做法。 

使用改进的 For 循环语法 

改进 for 循环(有时被称为“for-each”循环)能够用于实现了 iterable 接口的集合类及数 组中。在集合类中,迭代器让接口调用 hasNext()和 next()方法。在 ArrayList 中,手写的计数循环迭代要快 3 倍(无论有没有 JIT),但其他集合类中,改进的 for 循环语法和迭代器具有 相同的效率。 

这里有一些迭代数组的实现:

static class Foo {
        int mSplat;
    }
    Foo[] mArray = ...

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

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

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

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

 zero()是当中最慢的,因为对于这个遍历中的历次迭代,JIT 并不能优化获取数组长度的 开销。 One()稍快,将所有东西都放进局部变量中,避免了查找。但仅只有声明数组长度对性能 改善有益。 Two()是在无 JIT 的设备上运行最快的,对于有 JIT 的设备则和 one()不分上下。他采用 了 JDK1.5 中的改进 for 循环语法。 

结论:优先采用改进 for 循环,但在性能要求苛刻的 ArrayList 迭代中,考虑采用手写计数 循环。 (参见 Effective Java item 46.)

在私有内部内中, 在私有内部内中,考虑用包访问权限替代私有访问权限

考虑下面的定义:

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

    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);
    }
}

需要注意的关键是:我们定义的一个私有内部类(Foo$Inner),直接访问外部类中的一 个私有方法和私有变量。这是合法的,代码也会打印出预期的“Value is 27”。

 但问题是, 虚拟机认为从 Foo$Inner 中直接访问 Foo 的私有成员是非法的, 因为他们是两 个不同的类,尽管 Java 语言允许内部类访问外部类的私有成员,但是通过编译器生成几个综合 方法来桥接这些间隙的。 

/*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 方法的地方调用这些静态方法。 这意味着这些代码将直接存取成员变量表现为通过存取器方法访问。 之前提到过存取器访 问如何比直接访问慢,这例子说明,某些语言约会定导致不可见的性能问题。

 如果你在高性能的 Hotspot 中使用这些代码,可以通过声明被内部类访问的字段和成员为 包访问权限,而非私有。但这也意味着这些字段会被其他处于同一个包中的类访问,因此在公共 API 中不宜采用。

合理利用浮点数

通常的经验是,在 Android 设备中,浮点数会比整型慢两倍,在缺少 FPU 和 JIT 的 G1 上 对比有 FPU 和 JIT 的 Nexus One 中确实如此(两种设备间算术运算的绝对速度差大约是 10 倍)

 从速度方面说,在现代硬件上,float 和 double 之间没有任何不同。更广泛的讲,double 大 2 倍。在台式机上,由于不存在空间问题,double 的优先级高于 float。 

但即使是整型,有的芯片拥有硬件乘法,却缺少除法。这种情况下,整型除法和求模运算 是通过软件实现的,就像当你设计 Hash 表,或是做大量的算术那样。

 了解并使用类库 

选择 Library 中的代码而非自己重写,除了通常的那些原因外,考虑到系统空闲时会用 汇编代码调用来替代 library 方法,这可能比 JIT 中生成的等价的最好的 Java 代码还要好。典 型的例子就是 String.indexOf,Dalvik 用内部内联来替代。同样的,System.arraycopy 方 法在有 JIT 的 Nexus One 上,自行编码的循环快 9 倍。 (参见 Effective Java item 47.)

 合理利用本地方法 

本地方法并不是一定比 Java 高效。最起码,Java 和 native 之间过渡的关联是有消耗的, 而 JIT 并不能对此进行优化。当你分配本地资源时(本地堆上的内存,文件说明符等),往往很 难实时的回收这些资源。同时你也需要在各种结构中编译你的代码(而非依赖 JIT)。甚至可能 需要针对相同的架构来编译出不同的版本:针对 ARM 处理器的 GI 编译的本地代码,并不能充 分利用 Nexus One 上的 ARM, 而针对 Nexus One 上 ARM 编译的本地代码不能在 G1 的 ARM 上运行。 

当你想部署程序到存在本地代码库的 Android 平台上时,本地代码才显得尤为有用,而并 非为了 Java 应用程序的提速。 (参见 Effective Java item 54.)

结语 

最后:通常考虑的是:先确定存在问题,再进行优化。并且你知道当前系统的性能,否则 无法衡量你进行尝试所得到的提升。 

这份文档中的每个主张都有标准基准测试作为支持。你可以在 code.google.com“dalvik” 项目中找到基准测试的代码。 

这个标准基准测试是建立在 Caliper Java 标准微基准测试框架之上的。标准微基准测试很 难找到正确的路, 所以 Caliper 帮你完成了其中的困难部分工作。 并且当你会察觉到某些情况的 测试结果并想象中的那样(虚拟机总是在优化你的代码的)。我们强烈推荐你用 Caliper 来运行 你自己的标准微基准测试。 

同时你也会发现 Traceview 对分析很有用,但必须了解,他目前是不不支持 JIT 的,这可 能导致那些在 JIT 上可以胜出的代码运行超时。特别重要的,根据 Taceview 的数据作出更改 后,请确保代码在没有 Traceview 时,确实跑的快了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章