【Andorid应用开发】-(3)性能优化设计

这篇文章说性能设计,我估摸着有很多童鞋都没看到过原文,这里推荐下,文章来自Android官方,在下载的AndroidDocsDevGuide可以看到。如果你没读过这篇文章,那么我强烈建议去细读它。


看到了吗?BestPractices,最佳实践!我很惭愧做了这么久开发也是在一个偶然的机会才关注到它!请猛击下面链接:

http://developer.android.com/guide/practices/design/performance.html(原文)

http://wikidroid.sinaapp.com/Guide/practices/design/performance(中文翻译)

为了方便阅读我将文章转载如下(字体加粗部分)。

英文原文:http://developer.android.com/guide/practices/design/performance.html译者署名:曲天译者链接:http://androidlearner.net


一个Android应用程序运行在有着有限的计算能力和存储空间及受限的电池寿命的移动设备上。有鉴于此,该应用程序应该是高效的。即便你的程序看起来运行得“足够快”,电池寿命可能是你想要优化你的应用程序的一个原因。对用户来说电池寿命重要,Android的电池耗尽意味着用户将知道是你的应用程序让电池耗尽的。

请注意,虽然本文主要是讲解微优化的,但是这些优化几乎不会破坏你的应用程序。你首要的任务依然是选择合适的算法和数据结构,但这些不在本文讨论的范围之内。

目录

[隐藏]

简介

以下是编写高效代码的两条基本原则:

  • 不要做不必要的事
  • 不要分配不必要的内存

明智地进行优化

这份文档是关于Android平台微优化的,这里假定你已经分析了程序知道了那些代码是需要优化的,并且你已经有了一个方法用于衡量你所做的任何改变产生的结果(好或坏)。你只有这么点工程时间用来投资,重要的是你要知道你明智使用了它们。

(参见#结束语可以了解更多的分析应用程序和编写高效的测试标准的技巧)

同时该文档假定你选择了正确的数据结构和算法,并且考虑到了改变你的程序将带来的性能影响。正确地选用数据结构和算法带来的影响与本文档提供所有建议相比将有很大不同,而预知你的改变带来的性能影响将有助于你切换到更好地实现方案(比起应用代码,这对类库代码更重要些)。

(如果你需要这方面的建议,可以参考Josh Bloch编写书籍Effective Java, 第47条)

在对你的Android应用程序进行微优化时,你将会遇到一个棘手的问题:你要保证你的应用程序运行在多个硬件平台上。不同版本的虚拟机运行在不同的处理器上其运行速度是不同的。你可以说“设备X比设备Y快/慢F倍”,但这个结论是不可以从一台设备推广到其他设备的。特别的是,你在模拟器上的测试结果并不能告诉你在其他任何设备中的性能怎么样。同样,使用JIT的设备和不使用JIT的设备也有相当大的差异:在使用了JIT的设备上运行得性能“最好”的代码可能并不适用于没有使用JIT的设备。

如果你想知道你的应用程序在指定设备上运行的情况,你需要在该设备上做测试。

避免创建不必要的对象

创建对象并非是免费的。虽然GC为每个线程都建立了临时对象池,可以使创建对象的代价变得小一些,但是分配内存永远都比不分配内存的代价大。

如果你在用户界面循环中分配对象内存,就会引发周期性的垃圾回收,用户就会觉得界面像打嗝一样一顿一顿的。Android2.3引入的并发收集器虽有助于垃圾回收,但不必要的工作还是应该避免。

所以,应尽量避免创建不必要的对象实例。下面的例子将有助你理解这条原则:

  • 如果你有一个函数返回一个String对象,而你确切的知道这个字符串会被附加到一个StringBuffer,那么,请改变这个函数的参数和实现方式,直接把结果附加到StringBuffer中,而不要再建立一个短命的临时对象。
  • 当你从用户输入的数据中截取一段字符串时,尽量使用substring函数取得原始数据的一个子串,而不是为子串另外建立一份拷贝。这样你就有一个新的String对象,它与原始数据共享一个char数组。(后果就是如果你只是用原始输入的一小部分,那么使用这种方式你将一直把它保留在内存里)

一个更极端的例子是,把多维数组分成多个一维数组。

  • int数组比Integer数组好,这也概括了一个基本事实,两个平行的int数组比(int,int)对象数组性能要好很多。同理,这试用于所有基本类型的组合。
  • 如果你想用一种容器存储(Foo,Bar)元组,尝试使用两个单独的Foo[]数组和Bar[]数组,一定比(Foo,Bar)数组效率更高。(也有例外的情况,就是当你建立一个API,让别人调用它的时候。这时候你要注重对API借口的设计而牺牲一点儿速度。当然在API的内部,你仍要尽可能的提高代码的效率)

总体来说,就是避免创建短命的临时对象。减少对象的创建就能减少垃圾收集,进而减少对用户体验的影响。

性能优化的神话

我们将在这里澄清这份文档先前版本造成的误导性声明。

在不使用JIT的设备中,调用一个接口的引用会比调用实体类的引用花费更多的时间(举个例子,调用一个HashMap类型的map比调用一个Map类型的map代价要低,即便在这两种情况下,map都是HashMap的实例)。这里并非是先前说的慢2倍,实际上是更像是慢了6%。此外,JIT有效的区分了两者。

在不使用JIT的设备中,访问缓存的局部变量比重复调用该成员变量快大约20%。在使用JIT的设备中,访问局部变量的花费跟访问成员变量的花费是一样的,所以这并不是一个很好的优化措施,除非你觉得这样会使你的代码更容易阅读(这条建议对final、static和final static 修饰的成员变量也是适用的)。

用静态方法比虚方法好

如果你不需要访问一个对象的成员变量,那么请把方法声明成static。使用静态方法调用将快15-20%。这是一个很好的技巧,因为你可以通过方法签名调用该函数而不会改变对象的状态。

不要在类内部使用getter和setter

在很多本地语言如C++中,使用getter(比如:i = getCount())而不是直接访问成员变量(i = mCount)是一种很普遍的用法。在C++中这是一个非常好的习惯,因为编译器能够内联访问,如果你需要约束或调试变量,你可以在任何时候添加代码。

在Android上,这就不是个好主意了。虚方法的开销比直接访问成员变量大得多。设计公共的接口时,可以遵循面向对象的普遍做法来定义getters和setters,但是在一个类内部,你应该直接访问变量。

在没有JIT的设备上,直接访问成员变量比调用一个简单的getter方法快大约3倍。在有JIT的设备上,直接访问成员变量比调用一个简单的getter方法快大约7倍。目前这是Froyo(android2.2)版本上的事实,我们将在后续版本上改善JIT内联getter方法的效率。

使用Static Final 修饰常量

让我们来看看这两段在类前面的声明:

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>方法,因为在成员变量初始化的时候,会将常量直接保存到类文件中。用到intVal的代码被直接替换成42,而使用strVal的会指向一个相对来说代价小的字符串常量,而不是使用成员变量(要注意的是这个优化只适用于基本类型和字符串常量,而不适用引用类型。尽管如此,这依然是一种很提倡的做法,你应该尽可能地把常量声明为static final)。

使用for-each语法

加强的for循环(有时也叫for-each循环)可以用在实现了Iterable接口的集合类型和数组上。对集合来说,一个迭代器意味着可以调用hasNext()和next()方法。在ArrayList中,手写的计数循环比使用for-each循环快大约3倍,但是对其他集合类来说,foreach相当于使用 iterator。

下面展示了遍历一个数组的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()比zero()要快些,它将所有变量用局部变量保存,避免了查找。但仅仅是存储数组长度会带来效率的提升。

two()使用了在java1.5中引入的foreach语法。对于没有JIT设备来说,这种方法是最快的,区分于使用了JIT的one()。

综上所述,默认可以使用加强型的for(即foreach),在影响关键性能的ArrayList遍历时,考虑手写计数循环。(可参考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);
     }
 }

这其中的关键是,我们定义了一个私有内部类(Foo$Inner),它需要访问外部类的私有成员变量和函数。这是合乎语法的,并且会打印出我们希望的结果”Value is 27″。

问题在于Foo和Foo$Inner是两个不同的类,虚拟机认为在Foo$Inner类中直接访问Foo类的私有成员是是非法的,即便在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、setter)比直接访问速度要慢。所以这个例子就是在特定语法下面产生的一个“隐性的”性能障碍。

如果你的代码中是类似情况,你可以通过将内部类访问的变量和函数声明由私有范围改为包范围来避免开销。遗憾的是,这些域和方法可以被同一个包内的其他类直接访问,因此当你设计公共API的时候应该谨慎使用这条优化原则。

避免使用浮点数

从常理来讲,在Android设备上浮点数比整数慢2倍。在缺少FPU和JIT的G1和使用了FPU和JIT的Nexus,这都是事实。(当然,这两者之间的运算速度相差约10倍)

在速度方面,double和float在现代硬件面前并没有明显差异。在空间方面,double占用的空间是float的2倍。如果跟台式机一样你的空间不是问题,你可以使用double而非float。

同样,即便是整数,一些芯片对乘法有硬件支持而缺少对除法的支持。这种情况下,整数的除法和取模运算都是由软件来完成的——你可以想象下在设计哈希表或者做大量数学运算时做的那些事(就知道效率有多么低了)。

了解和使用Android提供的类库

除了有特别的原因,建议你尽可能使用android提供的类库而不是你自己的,要记住的是Android系统会对现有的java类库做一些优化和扩展,这显然比原有JIT类库更有效率些。一个典型的例子是Dalvik使用内联优化了String.indexOf方法,同样的还有System.arraycopy方法,后者比使用了JIT的Nexus One快大约9倍。(这条可参考Effective Java第47条)

谨慎使用JNI调用本地方法

本地代码不一定比java代码更有效率。首先从java到本地代码的过渡是需要开销的,而这点JIT并不能优化。如果在本地代码里你分配了本地资源(本地的内存,文件,或任何其他的资源),那么及时地搜集这些资源将变得更困难。同时,对不同的架构平台你需要编译不同的代码(而不是依赖JIT给你提供一套)。甚至对同一个平台,你也需要编译多种版本的代码。(G1的ARM处理器编译的本地代码跟 Nexus One上的ARM处理器编译的本地代码并不通用)

当你已经有一个本地代码库并想把代码用于android时,你可以考虑使用JNI调用本地代码。事实上,自java1.3以后,使用本地方法来提高性能的做法已经不值得提倡了。

如果你确实需要用到本地代码,可以参考JNI Tips。(参见Effective Java第54条)

结束语

最后一件事情:要不断的测试。要带着问题去优化,确保你能够精确的度量未优化之前的性能,否则你将不能衡量你尝试做出的性能优化。

该文档中每一条都有相应的标准来衡量,在code.google.com “dalvik” project能找到这些标准。

这些标准都是构建在Caliper的java框架上的。尽管如此,”微标准“很难衡量,这个项目 Caliper可以帮助你,我们在此强烈建议你使用该框架。

你可能还会发现可以用Traceview来分析程序,但要意识到目前它是禁用JIT的,这会导致计算JIT调用的时间是不正确的。要确保使用Traceview数据带来的优化确实要比不使用Traceview好。


文中提到的Android性能优化的原则和方法,阅读过后可能还是有些茫然。我们平常开发也就处理些简单的数据或者实现一个简单的功能,没有牵涉到算法、内部类等。那么如何优化我们的程序呢?下面我针对几个常用的编程方法进行代码的微优化,在做Android应用开发的时候我们也要谨记,手机或者MID内存是相当有限的,开发的应用程序应该尽量让其高效率的执行代码,而非像做J2EE程序似的铺张浪费。有很多童鞋将之前开发J2EE的经验带到Android应用开发上来,其实这些也只是细节,多注意多思考吧。

1、关于获取数组和列表长度的方法,请看下面的代码

List<String> list = new ArrayList<String>(); for (int i = 0; i < 100; i++) { list.add("abcdefg"); } long startTime1 = System.currentTimeMillis(); for(int i = 0;i<list.size();i++){ System.out.println(list.get(i)); } long endTime1 = System.currentTimeMillis(); Log.e(TAG, "time:"+(endTime1-startTime1)); / long startTime2 = System.currentTimeMillis(); int size = list.size(); for(int i = 0;i<size;i++){ System.out.println(list.get(i)); } long endTime2 = System.currentTimeMillis(); Log.e(TAG, "time:"+(endTime2-startTime2));

通过上面简单的方法,结合打印信息,对比可知:先获取list的长度再循环效率高出一倍左右!那么在你的程序中知道该怎么做了吧?

2、关于Android静态变量问题

Android的静态变量有个特点,就是在关闭应用程序时静态变量任然存在,请看以下代码片段

private static int i = 10; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Log.e(TAG, "i=" + i); i = 15; } @Override protected void onDestroy() { Log.e(TAG, "onDestroy========="); //android.os.Process.killProcess(android.os.Process.myPid()); super.onDestroy(); }

将应用程序运行两次,看到如下结果:

怎么样,有意思吧!为了避免这种情况,可在Activity.onDestroy方法中加入这句代码:android.os.Process.killProcess(android.os.Process.myPid());将当前应用所在的进程中给杀掉。如果不杀掉的话,那么要谨慎使用了static,就像上面的代码由于没有销毁而导致i的初始值不正确。当然还有另外一种方法达到static的效果。那就是继承Application来达到多个ActivityService中使用的目的。我们都知道,在Android应用程序中所有的ActivityServiceBroadcast间接继承Context。在AndroidManifest.xml配置文件中,所有的ActivityService等都被包含在标签Application中。所有的ActivityService都可调用Application中非私有的方法和变量。于是可以通过继承Application自定一个Application的子类来达到目的。当然了,只要控制好了就没什么问题。

3、关于Android的布局优化,请看下一篇博客吧,因为很重要,也有很多知识点~

原创文章,转载请注明出处:http://blog.csdn.net/tangcheng_ok



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值