Android代码性能优化(Android Developer Training翻译)

本文翻译自Android Developer Training:https://developer.android.google.cn/training/articles/perf-tips.html

本文是 Zhengjt 原创,发表于http://zhengjt.com,请阅读原文支持原创:link

说到代码性能优化,选择合适的算法和数据结构应该永远是我们首先要考虑的,我们在此并不讨论这个。本文讨论的是可以提升app整体性能的代码优化方法,它们可能并不总能那么显著地提升整个app的性能,但是你可以将这些技巧融入你的编程习惯中,从而使你写出更高效的代码。

对于写出更高效的代码,有两个基本原则:

  • 避免做你不需要做的事情
  • 避免分配多余的内存

你必须对Android代码的每一个细节做优化的一个原因是:你的代码将运行在多种多样的硬件设备上。不同的处理器,不同虚拟机,不同的运行速度。你甚至不能简单地说一个设备肯定比另一个设备快。对于不同的设备来说,是否有JIT会造成很大的不同,对有JIT设备优化得最好的代码并不一定适合于没有JIT的设备。

为了确保你的app能够在跨平台多设备上都表现得都很好,需要保证在所有层级上优化你的代码。

避免创建不必要的对象

对象的创建从来都不是免费的,它需要消耗系统的资源。

当应用程序内存中的对象达到一定数量时,系统将强制开始进行GC,这时设备会发生短暂卡顿,从而影响用户体验。 在Android 2.3中引入了并行垃圾收集器有助于缓解这个问题,但我们应该总是要避免不必要的内存分配。

因此,应避免创建不需要的对象实例。 以下是一些样例建议:

  • 如果你的方法返回一个String,而这个返回的String总是要拼接成一个StringBuffer,那么,你应该改变的方法签名和返回,在方法中直接拼接StringBuffer,而不是创建一个生命周期很短的临时String变量。

  • 当你需要从输入数据中提取字符串的时候,使用substring而不是创建一个输入数据的拷贝,这样你将创建一个String对象,但是这个String对象将和输入数据共享常量char字符数组。(这样做的代价是如果你只需要提取输入数据的一小部分,最终你也必须在内存中保留整个输入数据)

有个更激进的想法是:用一维的数组替换多维数组

  • int数组比Integer要高效得多,两个元素一一对应的int数组依然比一个Object<int,int>数组高效得多。对于其他基本类型来说也是这样的。

  • 如果你需要一个元祖tuples(Foo,Bar)对象,记住使用两个平行的元素一一对应的Foo[]Bar[]将总是比创建一个Object<Foo, Bar>数组来的高效得多。(有一个例外是,当你设计API提供给其他代码调用的时候,为了实现良好的规范,你应该对这个优化做妥协)

一般来说,要尽量避免创建短期的临时变量,这样能够减少影响用户体验的垃圾收集器的GC的频率。

使用Static替代Virtual

如果你的方法不需要访问对象的成员字段,那将其设置为static,这样的话方法的调用速度会提升15%~20%。同时这也是良好的代码实践,因为你能够通过static修饰符知道这个方法不会改变对象的状态。

使用Static Final修饰常量

考虑如下在一个类的顶部的两个声明:

static int intVal = 42;
static String strVal = "Hello, world!";

编译器生成了一个初始化方法叫<clinit>,当变量第一次被引用的时候会调用这个方法。这个方法存储了intVal和42、strVal和"Hello, world!"的对应关系。当这两个变量被引用的时候,系统通过成员字段域查找来得到这两个变量的值。

我们可以使用关键字final来改变这种状况:

static final int intVal = 42;
static final String strVal = "Hello, world!";

这样这个类就不需要<clinit>方法了,因为常量直接被编译到dex文件中,被引用的intVal将直接被替换成42,strVal也被直接替换成字符串常量,而不需要查找成员字段域。

避免内部的Getters/Setters访问器

在类似C++的语言中,使用Getters/Setters而不是成员字段是通用的代码实践。这个对C++来说很好的实践也经常被应用于其他面向对象语言比如C#和Java,因为编译器总是能够内联优化代码,而如果你需要限制或者调试成员字段,你可以在任何时候更改Getters/Setters内的代码来实现。

然而,对Android来说,这并不是一个好主意。方法的调用比成员字段域查找要昂贵得多。公开的接口是有必要按照面向对象的法则声明getters和setters的,但是在类内部,你应该总是直接使用成员字段本身。

没有JIT的时候,直接的成语变量访问会比使用一个不必要的getter方法快3倍,如果有JIT,那么会快7倍(有JIT的时候访问成员字段和访问局部变量几乎是同等的低消耗)。

使用增强的循环语法(for-each)

增强的循环语法(有时候也常常叫做for-each循环)可以被任何实现了Iterable接口的对象集合使用。当循环ArrayList的时候,一个手写的计数循环会比for-each循环快3倍左右(无论是否有JIT),但是对其他集合collections来说,for-each循环几乎和使用iterator循环来得一样快。

对于数组循环来说,有以下几种选择:

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不能优化通过循环的每次迭代获得数组长度一次的成本,即每一次循环都需要计算一遍mArray.length。

one()更快一些。它将所有内容都放到局部变量中,避免重复计算。但是只有len的计算优化了性能。

对于没有JIT的设备,two()是最快的,并且与具有JIT的设备无法区分。 它使用了Java1.5版本中引入的增强型for循环语法。

因此,对于ArrayList循环,如果你对性能非常敏感的话,可以考虑一个手写的计数循环。除此之外,您应该默认使用增强型for循环。

提示:另请参见Josh Bloch的《 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),它直接访问外部类中的私有方法和私有实例字段。 对于Java语言语法来说,这是合法的,代码打印“Value is 27”符合预期。

但是问题是虚拟机认为从Foo$Inner直接访问Foo的私有成员是非法的,因为Foo和Foo$Inner是两个不同的类,即使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()方法时调用这些静态方法。 这意味着,上面的代码会归结为通过访问器方法访问成员字段的情况。 前面我们讨论了访问器如何比直接字段访问慢,所以这是一个因为特定语言语法导致“隐形”性能问题的例子。

如果你在对性能要求严格的场景中使用这样的代码,你可以通过改变内部类访问的字段和访问的访问的权限为包访问,而不是私有访问,从而避免上述开销。 但是这意味着字段可以被同一个包中的其他类直接访问,所以你不应该在公共API中使用它——因此最佳的方法是使用包替代私有内部类对外部类的私有域的访问。

避免使用Float

根据经验,浮点数在Android设备上比整数慢两倍。

在速度方面,在更现代的硬件上float和double几乎没有区别。 在存储空间方面,double是Float的2倍大。 和PC一样,假设存储空间不是问题,你应该偏向使用double。

此外,即使对于整数,一些处理器具有硬件乘法,但是缺少硬件除法。 在这种情况下,整数除法和模数运算会在软件中执行,想想你在设计一个哈希表或做大量的数学计算(这将消耗大量的系统资源)——因此我们也应该尽量避免除法运算或者将它转换成乘法运算。

熟悉和使用库

使用系统库代码,除了有那些我们熟知的好处之外,记住,系统可以使用底层汇编方法优化代码。 这里典型的例子是String.indexOf()和相关的API,Dalvi使用了一个内联的内在替换(提高了性能)。 类似地,System.arraycopy()方法比使用JIT的Nexus One上的手写的编码循环快大约9倍。

提示:另见Josh Bloch的《Effective Java》,第47条。

谨慎使用Native方法

使用Android NDK开发具有Native代码的应用程序不一定比使用Java语言编程更有效。首先,存在与Java代码转换的相关成本,并且JIT不能跨越这些边界进行优化。如果您分配了本机资源(本机堆上的内存,文件描述符或任何内容),那么安排这些资源的及时回收会更加困难。你还需要编译你想要运行的每个架构的代码(而不是依赖一个JIT)。你甚至可能需要为你认为相同的架构编译多个版本:为G1中的ARM处理器编译的Native代码不能充分利用Nexus One中的ARM,而为Nexus One中的ARM编译的代码将不能在G1上的ARM上运行。

Native代码主要用于当你有一个现有的Native代码库,你想要移植到Android,而不是用于“加速”使用Java语言编写的Android应用程序的部分。

如果你确实需要使用Native代码,你应该阅读我们的JNI提示。

提示:另见Josh Bloch的《Effective Java》第54条。

性能神话

在没有JIT的设备上,通过具有确切类型的变量而不是接口调用方法稍微更高效些。(例如,使用HashMap声明map会比使用Map声明map来的更高效,即使在这两种情况下映射的都是HashMap对象)这里的区别并不会夸张到一种比另一种慢两倍这么大,实际差异更像是慢6%这么多。 此外,有JIT的话,这两者的效率几乎相同。

在没有JIT的设备上,缓存成员字段访问比重复访问成员字段快约20%。 使用JIT,成员字段访问成本大约与本地字段访问成本相同,因此这不是一个值得优化的地方,除非你觉得它使你的代码更容易阅读。 (对于 final, static和static final修饰的成员字段也是如此)。

比较优化效果

在开始优化之前,请确保您有一个需要解决的问题。 确保您可以准确地衡量您现有的绩效,否则您将无法衡量您尝试的替代方案的优势。

您也可能发现Traceview对于分析有用,但是重要的是要意识到它当前禁用了JIT,这可能导致它错误地将时间归因于使用JIT可能提升性能的代码。 在进行Traceview数据建议的更改后,确保生成的代码在没有Traceview的情况下运行更快时尤其重要。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容

  • 关于android性能,内存优化 看了些资料整理了下,安卓的性能和内存优化的一些方法和注意事项。分享出来。 随着技...
    ifeng_max阅读 1,021评论 0 14
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 原文地址 这篇文档主要覆盖能够提升总体应用性能的细微优化,但是这些改变不可能造成显著的性能效果。选择合适的算法和数...
    CyrusChan阅读 445评论 0 2
  • 买了这本书有大半年,就开始的时候看了前面的三章,然后就半途而废的放在了我的床头一直到现在! 今晚又重新拾起来看了第...
    李宇航妈妈阅读 110评论 0 1