Android 性能建议 Performance Tips

译文 ( By Chikeong ):

 

这篇文章主要介绍一些结合起来使用能提升app 整体性能的细小的优化方法,但不要期待这些修改能带来巨大的性能改变。你应该花更多精力在选择合适的算法和数据结构,但这些不在该文章的主题之内。为了写出高性能的代码,你应该将这些帮助提示融入你的编码习惯中。

编写高效代码有两个基本原则:

  • 不做多余的事。
  • 尽量避免内存分配(操作)。
当对一个Android app 进行细小优化时,一个最棘手的问题是这个app 将运行在不同类型的硬件设备上。不同版本的VM 以不同的效率运行在不同的处理器上。这不仅仅是你可以简单说“设备A 比设备Y 运行得快的问题”,然后把这个结论从一个设备映射到其他设备的问题。特别是,在模拟器上的测量结果只能告诉你很少的性能信息。有没使用JIT,设备跟设备之间可能有巨大的性能差异:对于一个使用JIT的设备而言优秀的代码,并不总是意味着对于没有JIT的设备也如此。
为了确保你的App 在许多种设备中运行良好,确保你的代码在所有方面上都是高效的,并尽可能地对其进行优化。
 

避免多余对象创建


 

对象创建并非没有代价的。一个每个线程用于临时对象的的分配池的垃圾收集器可以降低内存分配的代价,但是一个需要分配内存的操作总是比不需要的代价大。

一旦你创建了过多的对象,便意味着你必须定期进行垃圾回收,从而对用户体验造成轻微卡顿的感觉。多线程的垃圾收集器在Android2.3 时被引入,但是仍然应该避免不必要的操作。

因此,你应该避免创建不必要的对象实例。一些例子:

  • 如果你有一个方法返回了一个String,而你知道这个返回值必定为被拼接(append)到一个StringBuffer 中,这时应该改变你的方法签名和方法实现,让你的方法内部直接完成这个拼接,而不是创建一个临时对象。
  • 当你从一连串的输入数据里面抽取出一个字符串时,试着返回一个源数据的substring(子串),而不是创建一个副本。你将会创建出一个新的string 对象,但是他将与源数据共享底部的char[].(这么做的代价是,即使你只适用源数据的其中一部分,你也会将所有数据保留在内存里)。

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

  • 一个存储int 的数组比存储Integer 对象的数组更好,同时这也引出一个事实:两个并行的使用的int 数组要比一个存储(int,int)结构对象的数组高效许多。这种情况对于所有基本类型都适用。
  • 如果你想要实现一个存储(Foo,bar)元组对象的容器(集合类),记住,两个并行使用的Foo[] 和 bar[] 一般来说都比一个自定义的(Foo,bar)元组对象 的数组高效得多。(当然,如果你是在设计一个对外开发使用的API,一般来说牺牲一点性能来达到良好的API 设计总是值得的。但是,在你内部的代码实现中,你应该尽量使用高效的做法)。
一般说来,尽量避免创建临时的对象。更少的对象创建意味着更低频的垃圾回收,这对用户体验有直接影响。
 

使用静态


 

如果你的方法没有使用到对象的域(成员变量),把你的方法改成static的。方法调用能提升15 % -20%。 这也是一个良好的做法,因为从方法的签名你能知道这个调用这个方法将不会改变对象的状态。

 

为变量添加 Static Final 标识


 

假设类里定义了一些变量:

staticint intVal =42;
staticString 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: 注意这个优化只适用于基本类型和String 常量,并非所有类型。不过,尽可能使用static final 来标示常量是一个良好的做法。

 

避免内部的Getter/Setters 


 

在原生的语言里,如C++, 一个常有的做法是使用getters(i = getCount()) 而不是直接使用域(i = mCount). 对于C++ 来说,这是一个好习惯。这也被其他的面向对象语言中使用,比如C#和Java,因为编译器自己能内联域。如果需要限制或者debug 域的使用,你可以加上这样的代码。

但是但是,这在Android 里头是个糟糕的做法。方法调用比域查找代价高得多。遵循面向对象语言的规范,在公共接口里使用getter 和setter 是合理的,但是在类的内部,则应该总是使用域。

不使用JIT,直接使用域比调用getter 获取 快三倍多。使用JIT(这时直接适用域就跟使用本地数据一样廉价)后,速度提升到7倍多。

注意:如果你在使用ProGuard 的话,你可以任意使用这两种方法,因为ProGuard 会帮你自动内联域。

使用加强版的迭代


 

加强版的迭代(即 for-each 方法迭代)能被使用在实现了Iterable 接口的集合类和数组上。对于集合类,可以使用iterator来 调用hasnext() 和next()来进行迭代。对于ArrayList,手动计数的迭代要快3倍多比iterator或者for-each迭代(不管有没使用JIT)。而对于其他集合类,使用for-each迭代跟使用iterator差不多。

一般来说,你应该使用for-each 迭代。但是对于ArrayList 则应考虑使用手动计数的迭代。

 

使用包 以取代对私有内部类的私有使用


 

假设有如下的类定义:

publicclassFoo{
    privateclassInner{
        void stuff(){
            Foo.this.doStuff(Foo.this.mValue);
        }
    }

    privateint mValue;

    publicvoid run(){
        Innerin=newInner();
        mValue =27;
        in.stuff();
    }

    privatevoid doStuff(int value){
        System.out.println("Value is "+ value);
    }
}

 

这里的关键点在于,我们定义了一个私有的内部类(Foo$Inner),内部类的方法里又调用了外部类的一个私有方法和一个私有的成员域(成员变量)。这是合法的,最后的结果也如预期打印出“Value is27”。

但是,问题在于VM 将把(Foo$Inner) 直接引用Foo 的私有成员当成是非法的操作,因为Foo 和 Foo$Inner 是不同的类,即使Java 允许一个内部类使用外部类的私有成员。为了解决这个问题,编译器将自己生成几个合成方法:

/*package*/staticintFoo.access$100(Foo foo){
    return foo.mValue;
}
/*package*/staticvoidFoo.access$200(Foo foo,int value){
    foo.doStuff(value);
}

当内部类需要引用外部类的mValue 私有成员域,或者调用doStuff()私有方法时,将调用这些静态方法。这意味着上述的代码变成了你是在使用方法调用来获取成员变量的值。早些时候我们已经提及方法调用要比直接的域使用效率低,因此这是一种程序语言惯用语法导致一个“隐性”的性能损失的例子。

如果你正在一个性能的关键处使用了类似语法,你可以通过将那些被内部类使用的域和方法改写成包访问权限的,而非私有权限的,以此来避免这些性能损失。不过,这会导致包内的其他类都能访问到该域和方法,因此,在公共(public)api 中,你不该这么做。

 

避免使用浮点数


经验告诉我们,在Android 设备上,使用浮点数,将比使用整形数慢上两倍。

从速度上讲,float 和double 在现代的硬件设备上没区别。存储空间上,double 是两倍大。对于桌面设备,不需要太关注考虑存储空间问题,所以你应该更多地使用double。

同样,对于整型数integer,一些处理器支持硬件乘法,但不支持硬件除法。在这种情况下,整型的除法和模除是在软件上进行的,这是当你在设计一个hash 表 或者做大量数学计算时应该考虑的事情。

使用的类库


除了一般我们提及的尽量使用类库而不是总靠自己实现的原因之外,有一点应该被牢记的是,系统可以把对类库的调用替换成更高效的汇编语言,这可能会比JIT 能生成等量Java 代码性能更好。典型的例子是,String.indexOf() 还有它相关的APIs,Dalvik 使用内联来替换原码。类似的System.arraycopy() 方法在使用JIT 的 Nexus One 设备上的效率是自己手写的循环复制的差不多9倍。

 

谨慎使用Native 方法


 

谨慎使用Native 方法

利用Android NDK 使用Native (本地)语言 开发的Android App 并不一定就比使用Java开发的性能更卓越。至少有一点值得提出的,Java-native 的关联和通信是有代价的,JIT 并不能实现优化这种语言之间的差异。如果你正在分配native 资源(在native heap 上分配内存,文件描述符,或者其他的),对这些资源的定期回收可能明显困难得多。同时你也需要将你的代码为运行其上的不同的架构分开编译,而非只是依赖JIT 去完成。你甚至可能还要为同一架构编写不同版本代码:对于运行在ARM 处理器,为G1 编译的native代码并不能充分发挥Nexus One 上的A处理器的性能,然后为Nexus One 编译的Native 代码将不能运行在G1上,虽然都是ARM 架构。

性能误区


对于一个没有JIT 优化 的设备,通过一个具体类型对象调用比通过一个接口调用方法确实是快一些。(比如,调用HashMap map 的方法是要比调用Map map 的方法代价小些,即使实际上map 都是引用的HashMap 的一个实例)。但是并不是因此造成2倍的性能差异,事实上它只是快了6% 左右。事实是,JIT的优化进一步扩大了这种差异。

对于没有JIT 优化的设备,保存一个类成员域的引用并多次使用(就像局部变量)比多次请求这个类成员域(需要域查找)提升20%的性能。使用JIT 优化,他们两者是性能相当的,所以这种优化并不值得,除非你觉得这么做能提升你的代码可读性。(这个情况适用于final, static和 static final 标识的域)。

 

坚持评估


 

在你开始优化之前,你应该确保你当前有一个问题亟需解决。你必须确定你能准确评估你当前的性能,否则,你将不可能评估你在尝试的优化的措施效益。

在这篇文章里的每个结论都是有基准测试作支撑的。这些数据可以在code.google.com"dalvik" project 找到。

这些基准测试 是使用 Caliper 构建的。Caliper  是适用于Java的微型基准测试框架。Caliper  替你完成了微型基准测试的困难工作,甚至能检测到你设计的测试的偏差。(比如说,VM 已经帮你的代码进行了优化)我们非常推荐你使用Caliper  来运行的微型基准测试。

你可能会发现Traceview 对于分析非常有用,但是很重要的一点是,确保你当前禁用了JIT,否则可能导致最后的结果错误地将JIT 实现的提升归功于代码本身。特别是,当你根据Traceview 的信息建议进行了一些修改之后,想观察最后的代码是否比修改前运行得更快。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值