提升Android应用性能的小贴士
又过46年
8年前
<p>本文主要介绍了提升Android应用性能的一些小方法,组合使用这些方法往往能够改善我们所开发的应用的性能表现。但是,我们应优先关注应用所选用的数据结构和算法,切勿本末倒置。想写出高效代码,有两个基本原则:</p> <ul> <li>不要做本不必做的工作;</li> <li>不要分配不必要的内存。</li> </ul> <p>以上两个基本原则很好理解,第一条是让我们尽可能降低应用的时间复杂度,第二条是让我们尽可能降低应用的空间复杂度。下文的各个建议实际上也是围绕着这两个基本原则展开的。</p> <p>当我们对Android应用做优化时,我们必须面对的一个最蛋疼的问题之一,便是我们的应用将会在各种类型的设备上运行,这意味着它们往往有着不同的硬件架构。不同版本的Android虚拟机在各种硬件架构上会以不同的速率运行。而我们大多数情况下无法简单地断定,X设备的速度会是Y设备的多少倍。我们在模拟器上的测试结果,对我们做真机上的性能评估又往往没什么帮助。</p> <p>更令人蛋疼的是,支持即时编译(JIT)的设备和不支持即时编译的设备又有着巨大的差异——支持即时编译的设备上的最优代码,对于不支持即时编译的设备来说,不总是最优的。</p> <p>要确保你的应用在各种各样的设备上都有着良好的性能表现,要确保你的代码在各种情况下都是高效的,这需要我们下一番功夫来对代码进行优化。以下是一些性能优化的建议。</p> <h3><strong>避免创建不必要的对象</strong></h3> <p>创建对象总是会产生代价的。一个支持线程级分配池的分代垃圾回收器可以使得一次内存分配所花的代价更少,但是再少也少不过“根本不进行内存分配”。</p> <p>当你的应用中创建了足够多的对象,便会导致垃圾回收经常发生,这可能会带来用户界面的“卡顿”。因此,你应该避免创建不必要的对象,比如:若你有一个返回一个String的方法,并且你确信返回结果总是会被添加到一个StringBuffer(StringBuilder)中,那么应对该方法做出修改,让它不再返回String,而是直接把结果添加到相应的StringBuffer(StringBuilder)中,这样一来便可以避免创建一个临时的String对象。</p> <p>还有一个更“激进”的建议是把多维数组都“展开”成一维数组:</p> <ul> <li>多个int数组要比一个int[]对象数组更加高效,这对于其他原始数据类型(primitive data type)同样适用;</li> <li>若你需要一个存储(Foo, Bar)元组的容器,要记得使用Foo数组和Bar数组要比使用(Foo, Bar)对象数组高效的多(例外情况是当你设计API时,为了一个良好的API设计,我们应该在性能上做出小小的妥协);</li> </ul> <p>对于本条建议,概括起来就是 <strong>尽可能避免创建短时存活的对象</strong> 。</p> <h3><strong>尽量使用static而不是virtual</strong></h3> <p>若你不需要访问对象的字段,那么请定义你的方法为静态(static)方法而不是虚(virtual)方法,这会带来15%到20%的性能提升。这同样也是一个好的编程实践,因为如此一来我们可以清楚的知道,调用该方法不会改变对象的状态。</p> <h3><strong>对于常量使用static final</strong></h3> <p>考虑下面的声明:</p> <pre> <code class="language-java">static int intVal = 42; static String strVal = "Hello, world!";</code></pre> <p>当声明了以上语句的类被初次使用时,编译器会为之创建一个名为“<clinit>“的类初始化器方法。这个方法会将42存储在intVal中,并将字符串“Hello, world!”的引用存储在strVal中,稍后我们引用到这两个变量时,便会通过“字段查找(field lookup)”来访问它们。</p> <p>然而通过为以上两句变量声明加上“final”关键字,那么intVal和strVal就会变为两个常量,访问它们时便无需通过字段查找,这样会提升性能。</p> <p>注意:这个优化建议只对String和原始数据类型有效。</p> <h3><strong>使用增强版循环语法</strong></h3> <p>增强版循环语法指的就是for-each写法,它可以用于数组以及实现了Iterable接口的集合类。for-each只有对ArrayList使用时,要比常规的for循环慢,对于其他情况,for-each与常规for速度相差无几。由于for-each能够简化代码编写,我们优先考虑使用它;只有我们使用ArrayList并且追求“极致性能”时,才应使用常规for循环。</p> <h3><strong>对以下场景考虑使用包(package)而不是私有(private)</strong></h3> <p>考虑下面的代码:</p> <pre> <code class="language-java">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); } }</code></pre> <p>以上代码的问题在于,我们在Foo类的私有内部类Inner中访问了Foo类的私有方法和私有字段,尽管这在Java语法中是合法的。但是对虚拟机来说,会将Foo$Inner和Foo视为两个不同的类,所以虚拟机为了实现内部类对外围类私有方法/字段的访问,需要创建两个充当“沟通纽带”的方法,如下:</p> <pre> <code class="language-java">static int Foo.access$100(Foo foo) { return foo.mValue; } static void Foo.access$200(Foo foo, int value) { foo.doStuff(value); }</code></pre> <p>也就是说,内部类Inner要通过上面两个方法来访问外围类的私有字段/方法,这显然比直接字段访问的开销要大。这种情况下,我们可以考虑将mValue和doStuff()的可见性改为默认的包范围。当然,我们不应该对公共API应用这一点。</p> <h3><strong>避免使用浮点型</strong></h3> <p>对于Android设备,使用浮点数要比整数大概慢两倍;而双精度浮点和单精度浮点在时间效率上相差无几,只是前者会占用二倍于后者的存储空间。还应该注意的是,有些Android设备在硬件层面上不支持除法。我们在开发中也要注意这一点。</p> <h3><strong>学习并使用系统API</strong></h3> <p>有时候对于某种业务逻辑,与自己实现相比,我们更应该优先使用SDK提供给我们的方法,因为系统提供给我们的实现往往更加高效。一个典型的例子是System.arrayCopy()方法要比我们手动用循环进行数组复制快9倍左右(在支持即时编译的Nexus One设备上)。</p> <h3><strong>可能无需进行的优化</strong></h3> <p>我们先来考虑以下两个方法:</p> <pre> <code class="language-java">void doWork(Map map); void doWork(HashMap map);</code></pre> <p>在一个不支持即时编译的设备上,第一个方法只比第二个方法慢一点(6%);而支持即时编译的设备上,二者效率的差异就更小了。</p> <p>我们再来考虑一下”重复访问字段”和”把字段缓存为局部变量”所带来的性能差异。在不支持即时编译的设备上,缓存要比重复访问快20%,而对于支持即时编译的设备,两者几乎一样快。</p> <p>因此对于以上两种情况,无需我们费心进行“优化”。</p> <h3><strong>记得做性能测试</strong></h3> <p>在你着手进行优化之前,确保你已经发现了性能问题,毕竟“过早优化是万恶之源”。此外,还要确保你已经精确测试过应用现阶段的性能表现,否则我们难以衡量优化工作的成效。我们可以使用SysTrace和TraceView来量化我们应用的性能表现。</p> <p> </p> <p> </p> <p>来自:http://www.jianshu.com/p/c3b734323f01</p> <p> </p>