性能小贴士

性能小贴士

本文主要介绍一些代码优化方面的小贴士,结合起来使用能整体性的提升应用性能。但是,这些技巧不可能带来戏剧性的性能改变。合适的算法和数据结构是解决性能的首选考虑(还有程序的执行流程优化),但这已经脱离了本文的范畴。

本文介绍的小贴士是每个有追求的程序员应有的编码习惯。

关于如何写出高效的代码,这里有两个基本的原则:

  • Don‘t do work that you don‘t need to do
  • Don‘t allocate memory if you can avoid it

面临的现状

一个非常棘手的问题,你的应用运行在不同类型的硬件设备上的。不同版本的虚拟机以不同的运行速度运行在不同的处理器上。通常,不能轻易的下结论说"设备x比设备y快或者慢了多少倍"。尤其是,模拟器和真机的差别非常大,模拟器上的性能测试不能充分反应真机的情况,况且,一个有JIT和一个没有JIT的设备之间也存在巨大的差别。

为了保证你的应用性能在绝大多数的设备上有一个好的表现,确保你的代码在各个方面的高效性并竭尽全力去优化你的性能。

避免创建不必要的对象

对象的创建绝不是免费的。现在主流的垃圾收集器中每个线程的临时对象分配池使得对象的创建更加低廉,但是分配内存的代价还是要比不分配要高昂的多。 正如你在程序里创建了大量的对象,垃圾回收会定期的触发,会造成一点点卡顿的感觉。虽然Android在2.3引入了并发的垃圾回收机制,但不必要的工作还是应该避免。

因此,不要创建那些非必需的实例对象。以下的几个例子可能具有参考意义:

  • 如果你有一个方法返回一个string,而且你知道这个返回结果是要被加到一个StringBuffer中去的,那么改变你的方法实现,变成直接添加而不是创建一个短暂的临时对象。
  • 当从一个输入数据中提取字符串,尝试去返回原数据的子串,而不是创建原数据的一个拷贝。采用子串虽然也会创建一个新的string对象,但是数据的内容还是和原数据共用的。
  • 尽可能的使用一维数组,而不是多维的数组。

关于减少不必要的对象创建,我们还可以有以下的实践:

  • 使用Message.obtain()或者handler.ontainMessage()去获取一个Message对象,而不是直接new。
  • 使用优化过的数据结构,SparseArray来替代Map。
  • 使用线程池去重用线程而不是每次新建线程。
  • Handler随意创建,只为把内容放到主线程执行,这个时候可以公用一个Handler。
  • 在那些被高频调用的方法里,避免创建临时对象,比如View的onDraw(),在循环里等等。
  • 在API23以下,当做图片清理设置ImageView.setImageBitmap(null)时,方法内部会创建一个BitmapDrawable对象。所以,我们 使用ImageView.setImageDrawable(null)来代替。

总的来说,尽你所能的避免创建短暂的临时对象。垃圾回收对体验有较大的影响,越少的对象创建意味着越低频的垃圾回收。

Prefer Static Over Virtual

把一个方法定义成static的,相比于对象方法大概会有15~20%的速度提升。这也是一个好的实践,因为你可以从方法声明上分辨出这个方法调用不会改动对象的状态。

使用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>方法就不会被调用,因为这两个常量直接存放进dex文件的静态字段初始化器中。指向intVal的引用直接使用42,访问strVal也会使用一个相对廉价的字符串常量而不是通过字段查找的方式。

注意:这个优化只适用于基本类型以及字符串常量,而非任意的引用类型。但是,为常量加上static final的声明是一个好习惯。

避免内部的Getters/Setters

在像C++这样的本地语言中,通过getters方法的形式替代直接的字段访问会比较平常。这在C++里是一个优秀的习惯,在C#或者Java这样的面向对象的语言里也是被经常使用,因为大部分的编译器都做了内联优化。

然而,这在Android上并不是一个好的实践。方法的调用比实例字段的查找要昂贵的多。遵循面向对象的编程习惯提供setter和getter方法是好的,但是在类的内部应该直接使用字段访问。

在没有JIT的情况下,直接的字段访问要比调用一个无用的getter方法快大约3倍。在有JIT(直接的字段方法跟本地访问一样低廉)的情况下,直接的字段访问要比方法调用快大约7倍。(特地做了验证,在我6.0系统的motox设备上大约是10倍)

使用增强的for循环

增强的for循环也叫"for-each"循环,通常用于遍历那些实现了Iterable接口的集合。在集合中,一个迭代器一般会调用hasNext()next()。在使用ArrayList时,一个手写的计数的循环在有JIT的情况下性能大约是没有时的3倍,但是在使用其它的集合时使用增强的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不能优化掉每次循环中的数组长度的查找。

one()要快一点。它把所有的都放到本地变量,避免了查找。但其实这里只有数组长度的定义能提升性能。

two()在没有JIT的情况下是最快的,在有JIT的情况下跟one()基本一样。

所以,默认情况下我们应该使用增强的for循环,但在处理ArrayList时,如果对性能特别敏感的情况下,我们考虑使用手写的计数循环。(依然是motox手机,100万的数据量遍历,手写的带计数的循环的性能要比for loop快15倍左右)

对于私有的内部类,考虑使用包访问权限代替私有访问权限

如下的类定义:

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),这个私有类直接访问了外部类的一个私有方法doStuff(), 同时访问了外部对象的私有的字段。这是合法的,代码也如预期打印出了"Value is 27"。 问题在于虚拟机认为直接从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()方法时会调用这些静态方法。这就意味着上述的代码归根结底到了这样一种场景,通过方法的存取来访问字段。在前面有讲到过,存取要比直接的字段方法要慢。所以,这个例子展示了一个特定的语言特性所引起的无形的性能损耗。

多使用系统提供的函数

除了常规的理由外,我们更愿意选择系统函数的原因是因为系统可能会使用汇编的方式去替换方法的实现,这比JIT生成的代码有着更好的性能。典型的有String.indexOf()和相关的APIS。类似的,在有JIT的Nexus One手机上,System.arraycopy()方法要比你手写的循环快9倍左右。

慎用本地方法

使用NDK通过C++等本地代码开发出来的应用的性能并不一定比用Java代码来的高效。至少,Java-Native是有传输消耗的,JIT也无法跨越这个边界进行优化。而且你还会面临以下的问题:

  • 如果你分配了本地的资源,就需要考虑如何及时的回收这些资源
  • 你需要为要支持的CPU架构编译不同的动态库
  • 就算是相同的CPU架构,你可能需要编译不同的版本。举个例子,为G1手机的ARM架构的处理器编译的动态库在Nexus One上不能发挥出应有的效果,而为Nexus One编译的动态库,在G1上根本就跑不起来。

本地代码最主要的使用场景是复用已有C/C++代码把它们使用到Android里来,而不是为了提升速度。

多测量

开始优化前,首先要确认问题是否存在。其次,你要能精确的测量出当前问题的性能数据,否则就不能很好的量化提升效果。(对于性能问题都需要用数据说话)

总结

今天讲的内容有什么用,能减小多少内存,提升多少程序性能呢?

    作为一个有追求的程序员,本文的小技巧都应该纳入个人的编码习惯。

现在的设备硬件性能这么强了,还有必要做这些细节优化吗?

    虽然,现在的设备的硬件性能非常强了,但是应用程序面临的内存、性能等问题仍然非常严峻。比如说,加载几张高清图,
不做任何限制就能让程序崩溃。
    又比如,上面提到过的语言层面的一些性能损耗。面向对象的java语言在创建对象时,会先创建它   所继承的父类的对象。
创建一个继承关系比较复杂的类的实例时,会顺带创建很多父类实例。这就是无形的性能损耗。然而面向对象能带来的好处让
我们只能选择妥协。所以,相比于这些我们无法解决或者很难解决的问题,本文所介绍的技巧可以算是性价比非常高的实践。
时间: 2024-07-28 16:03:43

性能小贴士的相关文章

JDBC性能小贴

本文收集了一些用于提升JDBC性能的方法.Java应用或者JavaEE Web应用的性能是很重要的,尤其是数据库后端对应用的性能影响.不知你是否经历过Java.JavaEE web应用非常慢的案例没有(处理一个简单的请求都要花上好几秒的时间用于数据库访问,分页.排序等).下面这些贴士也许能提升Java应用的性能.它们 非常简单同时还可以应用于其它编程语言,如果是用数据库作为后端存储的话. 这几个JDBC性能贴示不见得有多酷或者有些你从没听说过,虽然讲的很基础但是在实践中上很多程序员经常忽略它们,

Git 中级用户的25个小贴士

原文链接:25 Tips for Intermediate Git Users 作者:Andy Jeffries 时间:2009年11月1日 更新:这篇文章最初是在 2009年11月 发布到我的博客,它一直没有更新--不过有许多人发现这篇文章很有用,所以我想保持下去.请不要评论说"这些已经不再是中级小贴士了". 我使用 git 大约 18 个月了,以为自己已经比较了解 git 了.但当我们请 GitHub 的 Scott Chacon 来 LVS 公司(博彩/游戏软件开发商)做一些培训

备起来!Linux安全运维常见命令小贴士

备起来!Linux安全运维常见命令小贴士 常用命令 1. 查找关键词并统计行数 cat 2015_7_25_test_access.log | grep "sqlmap" | wc -l 2. 删除含有匹配字符的行 sed -i '/Indy Library/d' 2015_7_25_test_access.log 3. 查找所有日志中的关键词 find ./ -name "*.log" |xargs grep "sqlmap" |wc -l 4

每个程序员需掌握的20个代码命名小贴士

代码中到处都需要命名.作为程序员,我们得给类命名,给变量命名,给函数命名,给参数命名,给命名空间命名,等等等等.下面有20条小贴士能帮助你提高你的命名能力. 1.使用能够表达意图的名字 名字得能告诉我们它要做什么,为什么存在,以及是如何工作的.选择能够表达意图的名字,将更有利于我们理解代码. <span style="font-size:14px;">int d; // elapsed time in days int elapsedTimeInDays; int days

iframe的使用小贴士

1.之前又说到“根据内容计算iframe的高度” 链接 2.现在想说的是,一般iframe页面都是嵌套在父页面当中,所以在一般在iframe里面做相关动作时默认都是iframe页面的,不会影响到父页面.因此若是需要在iframe的子页面里面操作父页面的元素,我们会如何做? iframe 子页面操作父页面元素需要知道的两点是: (1)iframe 子页面和父页面必须属于同一个域下.(这个问题,一般在本地页面来做到,是不太可能的,经常会有这个错误出现 “Uncaught SecurityError:

每一个程序猿需掌握的20个代码命名小贴士

代码中到处都须要命名.作为程序猿.我们得给类命名,给变量命名,给函数命名,给參数命名.给命名空间命名,等等等等.以下有20条小贴士能帮助你提高你的命名能力. 1.使用可以表达意图的名字 名字得能告诉我们它要做什么,为什么存在,以及是怎样工作的.选择可以表达意图的名字.将更有利于我们理解代码. <span style="font-size:14px;">int d; // elapsed time in days int elapsedTimeInDays; int days

成为一个优秀程序员的11条小贴士

我是一个充满了激情的程序员,所以我觉得我很了解程序员.在这个领域耕耘了这么多年,我和许多非常聪慧的人们接触,他们编写了具有创意的代码,但是当其他人来维护这些代码的时候,他们就很抓狂了! 能够激励程序员的最重要的一点就是他们的激情.我们对于编写良好的程序富有激情,所以我们整合了一个有11条小贴士的清单来帮助您成为一个优秀的程序员.无论您是刚开始学习程序设计还是一个有经验的开发者,有一些东西是您在参考手册上找不到的.备注:我们不说是伟大的.但,绝对是很好的,也就是说好到足够让一些程序员依赖或者使用它

Swift小贴士

//1.强制类型转换的括号括表达式而不括类型 let r = 2 let pi = 3.14159 //let area = pi * r * r //这个是会报错的,因为类型推断无效 let area = Double (pi * r * r ) //强制类型转换的括号括表达式而不括类型 //2.等于号不能返回值 let x = 2 if x = 2 { //报错.因为swift的等于号表达式没有返回值,这也是防止了== 误写成 = } Swift小贴士

mybatis使用小贴士

分享了以下tips: 一.事务管理 二.xml配置sql代码段 三.#和$的区别 四.注意对<.>做转义 五.依据字符串是否为空,动态组织sql语句 六.使用自定义的类型转换器 七.resultMap的复用 一.事务管理 用户执行一个动作,后台需依次更新多个表,如果其中有一个更新失败,则要回滚之前的更新.这种情况,就是事务回滚. 要支持事务操作,需要: 1.确保数据库表的类型是InnoDB,而不是MyISAM (MyISAM不支持事务,这是一个坑,之前总结过 http://blog.csdn.