您的位置:首页 > 移动开发 > Android开发

Android官方开发文档Training系列课程中文版:性能优化建议

2017-08-20 11:35 543 查看
原文地址:http://android.xsoftlab.net/training/articles/perf-tips.html

本篇文章主要介绍那些能够提升总体性能的微小优化点。它与那些能突然改观性能效果的优化手段并不属于同一类。选择正确的算法与数据结构必定是我们的第一总则。可是这不是我们这篇文章要介绍的。你应该将这篇文章所提及的知识点作为编码的日常习惯,这能够提升常规代码的执行效率。

下面是书写代码的基本准则:

绝不要做你不须要的工作。

假设能够不申请内存就不要申请。要合理复用已有的对象。

另一个较复杂的问题就是被优化过的APP肯定是要执行在各种类型的硬件平台上。不同版本号的虚拟机执行在不同的处理器上肯定会有不同的执行速度。须要特别说明的是。在模拟器上測试非常少会得知其他设备的性能。

在不同设备上另一个非常大的不同点就是有没有JIT(JIT的意思是即时编译器):在JIT设备上执行的最优代码并不总在没有JIT设备上有效。

为了确保APP能够在各类设备上执行良好,要确保代码在各个版本号的平台上都是高效的。

避免创建不必要的对象

创建对象绝不是没有成本的。尽管分代垃圾收集器能够使暂时对象的分配成本变得非常低,可是内存分配的成本总是远高于非内存分配的成本。

随着很多其他对象的生成,你可能就開始关注垃圾收集器了。尽管Android 2.3中出现的并发收集器可能会帮到你。可是不必要的工作总是应该避免的。

因此,要避免创建不须要的对象。

下面的演示样例可能会帮到你:

假设你有个返回字符串的方法。该方法所返回的字符串总是被接在一个StringBuffer对象后面。那么就能够更改此方法的实现方式:让该字符串直接跟在StringBuffer的后面返回。

这样就能够避免创建那些暂时性的变量。

当从字符串中提取子串时,应该尝试返回原始数据的子串,而不是创建一个副本。子串将会创建一个新的String对象,可是它与char[]共用的是同一数据。採用这样的方式的唯一不足就是:尽管使用了当中的一部分数据。可是剩余的数据还都保留在内存中。

一条更为先进的法则就是,将多维数组转换为平行数组使用:

int数组的效率要比Integer数组的效率高的多。

假设你须要实现一个用于存储(Foo,Bar)对象的数组,要记得使用两个平行的Foo[],Bar[]数组,这要比单一的(Foo,Bar)数组效率好太多。

通常来说。要尽量避免创建那些生命周期非常短的暂时变量。

更少的对象创建意味着更低频率的垃圾回收,这会直接反应到用户体验上。

首选静态

假设不须要訪问对象的属性,那么就能够将方法设置为静态方法。这样调用将会添加15%-20%的速度。这还是一个好的习惯。由于这样能够告诉其他方法一个信号:它们更改不了对象的状态。

使用常量

请先考虑下面声明:

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也会直接得到字符串”string constant” ,这样能够省去了查找字符串的过程。


Note: 这样优化手段仅仅适用于基本数据类型以及字符串常量。不要作用其他类型。



避免内部的get\set方法

像C++这样的本地语言通常都会使用get方法来訪问属性。这对C++来说是一个非常好的习惯。而且C#、Java等面向对象语言也广泛使用这样的方式,由于编译器一般会进行内联訪问,而且假设你须要限制訪问或者调试属性的话,仅仅须要加入代码就能够。

只是,这在Android上并非个好习惯。方法调用的开销是非常大的。尽管为了遵循面向对象语言提供get、set方法是合理的,可是在Android中最好是能够直接訪问对象的字段。

在没有JIT的设备中。直接訪问对象字段的速度要比通过get方法訪问的速度快3倍。

在含有JIT的设备中,这个效率会达到7倍之多。

注意:假设你使用了ProGuard,那么就有了一个两全其美的结果,由于ProGuard会直接为你进行内联訪问。

使用增强for循环

增强for循环可用于实现了Iterable接口的集合或数组。

在集合内部,迭代器须要实现接口方法:hasNext()以及next()。

有下面几种訪问数组的方式:

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()方法是最快的。它使用了增强for循环。

所以应当在默认情况下使用增强for循环。


Tip: 也能够查看Josh Bloch 的 Effective Java,第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);
}
}


上面的代码定义了一个内部类。它能够直接訪问外部类的私有成员以及私有方法。这是正确的。这段代码将会打印出我们所期望的”Value is 27”。

这里的问题是:VM会觉得Foo$Inner直接訪问Foo对象的私有成员是非法的,由于Foo和Foo$Inner是两个不同的类。尽管Java语言同意内部类能够直接訪问外部类的私有成员(PS:虚拟机与语言是两种互不干扰的存在)。为了弥补这样的差异,编译器专门为此生成了一组方法:

/*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()方法时会调用上面这些静态方法。上面的代码归结为你所訪问的成员属性都是通过訪问器方法訪问的。早期我们说通过訪问器訪问要比直接訪问慢非常多,所以这是一段特定语言形成的隐性性能开销演示样例。

避免使用浮点型

一般来说,在Android设备上浮点型要比整型慢大概2倍的速度。

在速度方面,float与double并没有什么区别。在空间方面。double是float的两倍大。所以在桌面级设备上,假设空间不是问题,那么我们应当首选double。而不是float。

还有。在对待整型方面,某些处理器擅长乘法,不擅长除法。

在这样的情况下,整型的除法与取模运算都是在软件中进行的。假设你正在设计一个哈希表或者做其他大量的数学运算的话,这些东西应该考虑到。

使用本地方法要当心

使用本地代码开发的APP并不一定比Java语言编写的APP高效多少。首先。它会花费在Java-本地代码的转换过程中,而且JIT也不能优化到这些边界。

假设你正在申请本地资源,那么对于这些资源的收集能明显的感觉到困难。

除此之外,你还须要对每一种CPU架构进行单独编译。你可能甚至还须要为同一个CPU架构编译多个不同的版本号:为G1的ARM处理器编译的代码不能执行在Nexus One的ARM处理上,为Nexus One的ARM处理器编译的代码也相同不能执行在G1的ARM处理器上。

本地代码在这样的情况下适宜採用:当你有一个已经存在的本地代码库,你希望将它移植到Android上时,不要为了改善Java语言所编写的代码速度而去使用本地代码。

假设你须要使用本地代码。那么应该读一读JNI Tips.


Tip: 相关信息也能够查看Josh Bloch 的 Effective Java,第54条。



性能误区

在没有JIT的设备中,通过详细类型的变量调用方法要比抽象接口的调用要高效,这是事实。

举个样例。通过HashMap map调用方法要比Map map调用方法要高效的多,开销也少,尽管这两个实现都是HashMap。

其实速度并不会慢2倍这么多。真实的不同大概有6%的减缓。进一步讲。JIT会使两者的区别进一步缩小。

在没有JIT的设备上,通过缓存訪问属性要比重复訪问属性要快将近20%的速度。在JIT的设备中。属性訪问的花销与本地訪问的花销基本一致。所以这不是一项有多少价值的优化手段。除非你觉得这样做的话代码更易读(这对static,final,常量相同适用)。

常常估測

在開始优化之前,要确保你有个问题须要解决:要确保你能够精准測量现有的性能,否则将不能观察到优化所带来的提升。

基准点由Caliper的微型基准点框架创建。基准点非常难正确获得,所以Caliper将这份非常难处理的工作做了。甚至是在你没有在測量那些你想測量的地方的时候它也在工作。我们强烈的推荐你使用Caliper来创建自己的微型基准点。

你可能还发现Traceview非常有助于提升性能。只是你应该意识到Traceview工作的时候JIT并没有开启。这会错误的觉得JIT会将损失掉的时间弥补回来。这尤其重要:依据Traceview所更改的结果会使实际代码执行的更快。

有关很多其他提升APP性能的工具及方法。请參见下面文档:

Profiling with Traceview and dmtracedump

Analyzing UI Performance with Systrace
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐