[Android 性能优化系列]那些不能忽略的小细节

时间:2021-12-05 15:41:42

大家如果喜欢我的博客,请关注一下我的微博,请点击这里(http://weibo.com/kifile),谢谢

转载请标明出处(http://blog.csdn.net/kifile),再次感谢


原文地址:http://developer.android.com/training/articles/perf-tips.html

在接下来的一段时间里,我会每天翻译一部分关于性能提升的Android官方文档给大家

写在开头的话:

在下文中,会有一个经常出现的术语,叫做 JIT,他的全写是 Just In Time compiler,指的是Java 里的即时编译,他能够极大的优化你的代码运行速度。

下面是本次的正文:

################

本文将主要介绍一些能够提升整个应用性能的细节优化,但是他们并不会引起太过显著的性能提升。选择正确的算法和数据结构才应该是你的首选,但这就不在本文的讨论范围之类了。你应该将本文的这些小窍门作为一种编程习惯,这样你的编程会更加有效。

这里有两条最基本的规则

1.不要做你不需要做的工作

2.当你不需要的时候就把内存释放掉

你能碰到的最棘手的问题之一可能是当在不同硬件环境下进行细节优化。不同的虚拟机版本他们的处理器速度不尽相同,你不能简单地认为 X 设备比 Y 设备快或慢F倍。然后就把一台设备上的结果按照这个比例搬到另一个设备上。尤其是,在虚拟机上运行的效率和真机上是完全不一样的。一台有着JIT的设备和没有的设备也是非常不一样的。对于JIT而言是很好的代码,并不意味着对没有JIT的设备也是这样。

请确保你的应用在不同设备,不同SDK版本之间运行良好之后再来优化他的效率。


不要创建不需要的对象

创建对象永远不会是免费的,垃圾收集器虽然在每个线程中都有一个针对临时变量的分配池,这会使得分配临时变量的消耗变得更低,但是分配内存的消耗总是要比不分配内存高.

如果你在你的应用中分配更多的对象,你会让垃圾回收器的工作周期变短,导致用户使用时卡顿。同步垃圾回收器在Android2.3的帮助文档中有介绍,但是我们应该尽可能避免不必要的工作

因此,你不应该创建你不需要的对象,这里有一些例子:

1.如果你有一个方法需要返回String,并且你知道他的结果是通过StringBuffer进行叠加的。那么请改变你的参数和实现方式,直接添加字符上去,而不要创建一个短命的临时变量

2.当你准备从一组输入数据中获取一个string时,不要创建一个备份。你创建一个新的String对象,但是他会跟原有数据共享char[]对象。(如果你这么做了,即使你只是用了原始输入的一小部分,但是系统将保留所有这个对象的内存数据)


一个更加有用的想法是将多维数组改成多个一维数组

1.ints的数组会比Integer对象的数组要好,两个ints类型的数组相比于一个(int,int)类型的数组效率更高,这对所有基本类型都是通用的。

2.如果你需要实现一个容器来存储(Foo,Bar)这类元组,那么记住创建两个类似的Foo[],Bar[]数组比单独创建一个(Foo,Bar)对象的数组要 更好。(当然,当你设计一个API供人访问的时候,你最好为了实现一个优雅的API接口而对速度进行妥协,但是在内部调用的时候,你还是应该尽量让他变得更有效)、

综上所述,尽量不要创建临时变量,更少的创建可能减小垃圾回收的频率,提高你的用户体验


尽量使用静态方法

如果你不需要访问一个对象的具体字段,那么让这个方法称为静态方法,将会提升15%-20%的效率,因为你你告诉了这个方法他需要的参数,并且不会改变这个对象本身


为常量使用Static Final

看一下的定义

static int intVal = 42;
static String strVal = "Hello, world!";
编译器会生成一个初始化方法,叫做clinit,这个方法会在类第一次被用到的时候执行。这个方法会将intVal赋值为42,并且为strVal何一个字符串建立映射。当你之后使用这些变量的时候,它们可以通过字段查找被访问

我们可以通过使用final关键词提升速度

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

现在类不再需要一个clinit方法,因为所有的常量都在dex文件中被静态初始化了。intVal直接同42绑定在一起,strVal也同一个字符绑定在一起,你不再需要通过字段查找来访问他们。

注意:这个优化只针对基本类型和String类型。对于其他的变量类型,并没有直接进行关联。但是,如果可能的话还是尽量使用static final来声明常量


拒绝在内部使用Getter/Setter

在类似于C++的本地语言中,我们通常使用getter,而不是直接访问字段、对于C++,这是一个很好的习惯,以至于在面向对象的语言,类似于C#,Java中也被这样使用了,因为编译器可以使用内联访问,如果你需要限制,或者调试字段,你可以在任何时间加上这些代码。

但是,这在Android 上是一个坏喜欢。调用方法的代价很大,还不如去寻找对应字段。这也是为什么面向对象的语言会让Getter和Setter方法作为一个公开的接口,但是在类里你可以访问他的原因。

如果没有 JIT,直接访问一个字段要比通过getter访问*倍。但是如果有了JIT(他可以直接访问字段,消耗更小),直接访问字段将比通过getter调用快7倍

注意,如果你使用了ProGuard,那就随便你了,因为Proguard能够帮你实现内联


使用增强的For循环语法

增加的For循环(通常被称作遍历循环for-each)可以被用到实现了Iterable接口的集合以及数组的循环中。在集合里,他可以通过访问hasNext()和next()方法来实现遍历。但是在ArrayList方法中,一个老式的循环会快3倍。而对于其他的集合,增强后的循环语法效率会相同,并且目标显得更加明确。

这里是使用数组的循环的替代方式:

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()的速度也基本相同。它使用了从java1.5开始支持的增强的循环语法。

因此你应该默认应用增强语法,但是在使用ArrayList的时候选择使用手写的

详情可以参看Josh Bloch's 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),他可以直接访问外不能的私有方法和私有字段。他可以正对的调用stuff方法,输出Value is 27.


那么问题来了,虚拟机认为从Foo$Inner中直接访问Foo的私有成员是非法的,因为Foo和Foo$Inner是不同的类,即使在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()。之前,我们讨论过通过getter访问和直接访问的效率问题,因此,这里就是一个由于语法问题导致的不可见的性能优化点

如果你在一个性能消耗大户上使用上面的代码,你可以通过给相关字段和方法包访问权限来优化他。不幸的是,那样的话,拥有同样包名的类也可以直接访问他,所以你不应该在一个公共的Api中这么做


拒绝使用浮点型

经验之谈,浮点型在Android设备上的速度会比整型慢两倍。

在速度方面,浮点型和双精度型在现在的设备上没有什么区别。当然在空间上,双精度型是浮点型的两倍。在电脑设备上,这或许不是什么问题,你会更加喜欢双精度型。

同样的,相对于整型,一些处理器能够使用硬件来进行他的乘法。但是通常整型的除法和取模操作是通过软件实现的,因此当你准备设计哈希表或者做数学运算的时候就要额外注意了。


了解和使用依赖库

一般而言,使用三方库而不自己实现的原因是三方库的运行效率比自己实现的好。就比如说 String 的 indexOf()方法,及相关的一些 API,在这里 Dalvik 使用内联的方式做了替换。同样的,Systtem 的 arrayCopy()方法大概比 NexusOne上使用 JIT 之后的代码效率提升了九倍。


小心的使用本地方法

通过Android NDK来为你代码开发本地代码有时候并不代表着比使用Java更有效。例如,在Java本地化调用的时候需要消耗,并且JIT并不能优化这些。如果你企图分配本地资源(内存在本地堆上)纳闷你很难管理这些资源开销,你还必须为你希望运行的各个平台分别编译代码。你甚至可能要为同一种平台编译不同版本,例如同样是ARM架构的G1和Nexus One,两者间的本地化代码不能通用。

本地化代码在你有一个已经存在的本地化库,并且希望将它移植到Android上的时候是有用的,但是不要因为提升速度二区将你的Java代码移植到本地区

如果你的确需要使用本地代码,你应该读一下这个JNI Tips


性能神话

在一个没有JIT的设备上,通过一个实际类型的变量来访问方法要比通过一个接口类型来访问方法要有效。(举个例子,通过HashMap map来访问方法,比用Map map来访问效率更高)。虽然它们之间的效率差别不至于慢两倍,其实大概也有6%左右的差别。而 JIT 可以使他们的效率差别几乎无法觉察。

一个没有JIT的设备,他会缓存之前访问过的字段,使得下次访问的时候能够提升20%的效率。但是如果有了JIT,那么访问全局字段和访问局部变量的花费就一样了,因此,除非你觉得这样能够使得你的代码更容易阅读,否则这不是一个值得优化的点。(这对那些静态或者常量的字段同样有效)


不要忘记查看你的性能

当你开始优化之前,请确保你知道你需要处理哪些问题。最好先检测一下你目前的性能情况,否则你无法和优化后的效果进行对比。

本文的每一个小点都是通过标准进行备份的,你可以在code.google.com "dalvik" project找到他的源码

而设计的标准是来源于 Caliper 的Java标准框架。微控制台很难说名一个正确的方案,于是 Caliper 就用它的方式帮你解决所有困难的问题,甚至于你不要要检测那里你认为需要检测的东西,因为虚拟机已经优化过了你的所有代码。我们强烈推荐你使用 Caliper 来作为你自己的控制台

你也许在寻找 Traceview 的相关信息,但是在运行他的时候,会关闭掉 JIT 功能,所以你检测到的东西和开启 JIT 功能之后可能会大不相同。当然根据 Traceview 的数据 来进行优化也能够使你在不使用 Traceview 的时候的性能提升

更过关于检测和调试你应用的信息,请参看下面的文章: