导航

Android 性能小技巧

Posted on 2017-08-17 17:36  Young哥哥  阅读(80)  评论(0)    收藏  举报

前言

这篇文档主要覆盖能够提升总体应用性能的细微优化,但是这些改变不可能造成显著的性能效果。选择合适的算法和数据结构总是优先的,但是超出了这个文档的范围。为了高效率代码,你应该使用该文档中的技巧作为练习并植入到你的编码习惯当中。


写高效率代码有两个基本规则:

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

  2. 不分配内存如果你能够避免

 

当在一个 android 应用中微优化,你将面临的一个最复杂的问题是你的应用在不同类型的硬件上运行。不同版本的 VM 在不同的处理器上速度不同。你不能简单的说”设备 X 比设备Y快或者慢”,且从一个设备和其他设备衡量你的结果。实际上,对于任何设备虚拟机的测量结果告诉你很少。这里也有很大的不同在是否有 JIT 的设备上。对带 JIT 设备最好的代码并不一定是对没有 JIT 设备最好的代码。
为了确保你的应用表现优良在不同的设备当中,需要保证你的代码在所有级别上都是高效的并且极致的优化你的性能。

 

避免创建不需要的对象

对象的创建从不廉价,对于临时对象,带有线程分配池的垃圾收集器可以使分配廉价,但是分配内存总比不分配内存昂贵。
当你在应用中分配过多的内存,你将会强制周期性的垃圾回收,在用户体验上造成小的“打嗝”。并发垃圾收集器在 Android2.3 中被引入,但是不必要的工作应该被避免。
因此,你应该避免创建你不需要的对象实例。

 

一些可能有帮助的例子:

1.如果你有一个方法返回一个字符串,并且你知道它的结果应该总是被追加到StringBuffer,改变你的方法签名和实现让函数直接追加,而不是创建一个短命的临时对象。
2.当从输入数据中提取字符串,尝试返回原数据的子字符串而不是创建一个副本。

 
一个更激进的想法是将多维数组分割成平行的一维数组:

1.一个Int数组比 Integer 对象数组好,对于其他原始类型也一样。
2.如果你需要实现一个存贮(Foo,Bar)元组对象的容器,记住两个平行数组Foo[] 和 Bar[]一般要比一个单独的自定义对象(Foo,Bar)数组好的多。(例外:当你设计一个别人访问的 api,在这些情况下,最好是做些折中)
通俗而言,避免创建短命临时对象如果可以的话。越少的对象创建意味着低频率的垃圾回收,对用户体验有直接的影响。

 

Prefer Static Over Virtual

如果你不需要访问对象字段,让你的方法 Static。执行将比一般快15%-20%。它也是好的编码实践,因为你可以告诉方法签名调用方法而不引起对象状态改变。

 

对于常量使用 Static final

考虑在类中上方使用如下声明:

 

 

编译器生成一个类初始化器方法,叫做<clinit>,当类第一次被使用的时候执行。方法存贮 42 到 intVal,并且为 strVal 从类文件中的字符串常量表中提取引用。当这些值随后被引用,他们通过查找字段被访问。
我们可以改善这个问题通过 “final” 关键字

 

 

这个类不再需要一个<clinit>方法,因为常量进入静态字段初始化器在 dex 文件中。引用 intVal 的代码将直接使用 42,访问字符串使用一种相对不那么昂贵”字符串常量”指令而不是字段查找。

 

注意:这类优化只对原始类型和字符串常量适用,非引用类型。并且,声明static final 常量在任何可能的时候都是好的编码实践。

 

使用加强 for 循环语法

加强 for 循环(有时被认为”for-each”循环)能被用于实现 Iteratble 接口的集合和数组。在集合中,一个 iterator 被分配用于调用接口 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 还不能优化掉每次循环的时候获取 array 长度的损耗。
one() 是较快的,它把所有的东西存到本地变量,避免查找,只有数组的长度有性能上的获益。
two() 是最快的对于没有 JIT 的设备。在带 JIT 的设备上和 one() 没有太大区别。它使用加强 for 循环语法。

 

私有内部类考虑包访问而不是私有访问

参考如下类定义:

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”
问题是VM认为直接访问Foo’s的私有成员从Foo$Inner中是不合法的,因为Foo和Foo$Inner是不同的类,即使Java语言允许一个内部类访问一个外部类的私有成员,为了弥补差距,编译器生成一对合成方法。

 

 

内部类代码调用这些静态方法不管什么时候它需要访问 mValue 字段或者调用doStuff() 方法在外部类中。这意味着上面的代码变成访问成员变量通过访问器。早先我们讨论过访问器比直接访问字段要慢,所以这是一个造成“不可见”的影响性能的例子。
如果你在性能热点上使用这样的代码,你可以通过声明字段和方法被内部类用包访问来避免损耗。而不是私有访问。不幸的是这个字段能够被同一包中的其他类直接访问,所以你不应该在公共 Api 中这样使用。

 

其他

避免使用浮点数

一般来讲,浮点数要比整型慢两倍在支持 Android 的设备上。
在速度方面,float 和 double没有区别在多数现代设备上。空间方面,double 要大两倍。在多数台式机上,如果空间不是问题,你应该选择 double而不是 float.


并且,即使对于整型,一些处理器有硬件乘法器但是缺少硬件除法器,整型相除和系数操作在软件中执行-如果你正在设计一个hash表或者做很多数学运算的时候需要考虑。

 

了解和使用库

除了常见的原因,选择库而不是自己的代码,记住系统自由的把调用替换成手工编写的汇编程序的库方法,这些方法比 JIT 生成的更高效对于相同的 Java代码。典型的例子是 String.indexOf() 和相关的 API。相似的,System.arraycopd() 方法是手写循环的 9 倍在带有 JIT 的 Nexus 设备上。

 

谨慎的使用本地方法

开发你的应用使用 Android NDK 并不一定比使用 Java 代码高效。因为涉及到 Java-native 转换时是有损耗的。并且 JIT 不能跨越这些边界优化。如果你分配本地资源(本地堆的内存,文件描述符,或者其他),它显著的要比实时的分配这些资源的集合要困难。你也需要编译你的代码针对每种你支持的架构(而不是依赖它并带有 JIT)。你甚至必须编译多个版本对于你认为的同一种架构:为 ARM in G1 处理器编译的 native code 当中不能够充分利用在ARM in Nexus One,并且为 ARM in Nexus One 编译的代码无法运行在ARM in G1.
Native code 起到作用当你有现存的本地代码你希望移植到 Android 中,而不是加速你 Android 中用 java 写的部分功能。
如果你需要使用 native code,你应该阅读我们的 JNI Tips

 

性能误区

在没有 JIT 的设备上,调用方法通过一个带有确切类型的方法而不是接口要高效是真的。(例如,调用在 HashMap map 中的方法要比 Map map 轻量级,即使两个 map 都是 HashMap)它不是慢两倍,实际上的区别在 %6。并且 JIT 让二者无明显区别
在没有 JIT 的设备上,缓存字段访问要比重复访问一个字段快 %20.带有JIT,字段访问消耗和本地访问差不多,所以这不值得优化除非你觉得它让你的代码更容易读。(对于final,static,static final 字段也如此)

 

Always Measure

在你开始优化前,确保你有问题需要解决,确保你可以准确的测量你现在的性能,否则你不能测量你尝试替换方案的益处。
你可能发现 Traceview 对于性能分析是有帮助的,但是认识到它目前禁用了JIT 更重要。当执行不带 Traceview 时且在根据 Traceview 的建议数据做了改变之后,确保结果代码运行实际更快是相当重要的。