Android 开发者指南 - 性能提示

前言

这篇文档主要涵盖了一些微小的优化,组合它们能够提升应用的整体性能,但是这些变化不会带来戏剧性的效果。你应该优选选择正确的算法和数据结构,但是它超出了本文档要说明的范围。在一般的开发练习中,你应该使用本文档中的提示,这样才能把提高代码效率当成一种习惯。

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

  • 不要做不该做的事
  • 尽量避免分配内存

当你微优化安卓应用时,面对最棘手的问题之一就是,你的应用会运行在各种不同类型的硬件上。不同版本的虚拟机跑在不同处理器上,运行速度也不同。通常你不能简单地说,设备 X 是比设备 Y 运行快/慢的因素,将结果从一个设备扩展到其他设备。特别是,关于在其他设备上的性能,模拟器上的测量结果不全面。有没有 JIT 的设备也有非常大的差异:具有 JIT 的设备的最佳代码并不总是没有设备的最佳代码。

为确保你的应用在各种设备上都能正常运行,确保你的代码在各个级别都高效,并积极优化你的性能。

避免创建不必要的对象

创建对象并不是没有开销的。分代垃圾收集器具有用于临时对象的每个线程分配池,这可以使分配更便宜,但是分配内存总是比不分配代价要大。

当你在应用中创建更多的对象时,你将被迫进行垃圾收集,对于用户体验来说,它就像「打嗝」一样的。在安卓 2.3 之后引入了并发垃圾收集器,但是也应该避免不必要的工作。

因此,你要避免创建不必要的对象。下面是一些例子:

  • 如果你的方法返回一个字符串,你知道它的结果总会拼接到 StringBuffer,这时你就该更改签名和实现,这样函数会直接追加,而不是创建存活期短的临时对象。
  • 当从输入数据提取字符串时,尝试返回原始数据到子字符串,而不是创建一个拷贝。你会创建一个新的 String 对象,但是它会和原始数据共享 char[]。(需要考虑的是,如果你只使用原始输入的一小部分,那么无论如何,如果你用这个方法,你都会在内存中保留它。))

一个激进的想法是,把多维数组切片变成并行的一维数组。

  • int 数组比 Integer 对象数组好多了。但是概括来说,两个并行的 int 数组同样比二维数组 (int,int)高效。对于其他的基本数据类型的组合也是如此。
  • 如果你需要实现一个容器,用来存储二元组 (Foo,Bar) 对象,记住两个并行的 Foo[] 和 Bar[] 数组通常比一个常规的 (Foo,Bar) 对象数组要好得多。(当然例外情况是,你为其他代码设计 API 以进行访问。在这些情况下,为了实现良好的 API 设计,通常最好对速度进行小的折衷。但是在你自己的内部代码中,你应该尝试尽可能高效。)

一般来说,尽量避免创建短期的临时对象。更少地创建对象意味着更低频率的垃圾回收,这对用户体验有直接影响。

首选静态虚拟

如果你不需要访问对象的字段,请将方法设为静态,调用速度就会提高 15%-20%。这也是很好的做法,因为你可以从方法签名中看出,调用方法不能改变对象的状态。

考虑下面的在类首部的声明。

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

编译器生成一个类的初始化方法,叫做 ,当第一次使用类的时候,该方法会被执行。这个方法把值 42 存在 intVal 变量中,从类文件字符串常量表中提取一个引用指向 strVal。当稍后引用这些值时,通过字段可以访问它们。

我们可以使用 final 关键字改善这一步:

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

这样,类就不需要 方法了,因为常量进入 dex 文件中的静态字段初始值设定项。引用 intVal 的代码会直接使用整数值 42,访问 strVal 会使用相对划算的「字符串常量」指令,而不是字段查找。

注意:此优化仅适用于基本类型和字符串常量,而不适用于任意引用类型。尽管如此,最好尽可能地声明常量 static final 值。

使用增强型 for 循环

增强型 for 循环(也就是 for-each 循环)可以遍历实现了 Iterable 接口的集合和数组。对于集合,迭代器被分配用于创建叫做 hasNext() 和 next() 的接口。对于 ArrayList,一个手写的计数循环比 for-each 快约 3 倍,但是对于其他集合,增强型 for 循环完全等同于显式迭代器用法。

这里有几个遍历数组的方案:

static class Foo {
    int splat;
}

Foo[] array = ...

public void zero() {
    int sum = 0;
    for (int i = 0; i < array.length; ++i) {
        sum += array[i].splat;
    }
}

public void one() {
    int sum = 0;
    Foo[] localArray = array;
    int len = localArray.length;

    for (int i = 0; i < len; ++i) {
        sum += localArray[i].splat;
    }
}

public void two() {
    int sum = 0;
    for (Foo a : array) {
        sum += a.splat;
    }
}

zero() 最慢,因为每次通过循环迭代获得数组长度是有成本的,JIT 还不会优化。

one() 快一些,它将所有内容都拉到局部变量中,从而避免了查找。只有数组的长度才能提供性能优势。

two() 在没有 JIT 的设备上是最快的,与具有 JIT 的设备的 one() 无法区分。它使用了 Java 语言 1.5 版本后引入的增强型 for 循环语法。

所以,你应该默认使用增强型 for 循环,但是考虑一个手写的计数循环,用于性能关键的 ArrayList 迭代。

考虑包而不是私有内部类的私有访问

来看下面的类的定义:

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 is27」。

问题是,虚拟机认为从 Foo I n n e r 访 F o o F o o F o o 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() 方法时,它会调用这些静态方法。这意味着上面的代码实际上归结为,你通过访问器方法访问成员字段的情况。之前我们讨论到访问器如何比直接访问字段更慢。所以这是一个特定语言习语的例子,导致「看不见」的表演。

避免使用浮点型

根据经验,浮点数 比Android 设备上的整数慢约 2 倍。

在速度方面,现代硬件上的 float 和 double 没有区别。在空间方面,double 大 2 倍。与桌面计算机一样,假设空间不是问题,您应该更喜欢 double。

此外,即使对于整数,一些处理器也有硬件乘法但缺乏硬件除法。在这种情况下,整数除法和模数运算在软件中执行 - 如果您正在设计哈希表或进行大量数学运算,则需要考虑。

了解并使用库

除了喜欢库代码而不是自己编写代码,请记住系统可以自由地用手动编译汇编程序替换对库方法的调用,这可能比 JIT 可以生成的等效的 Java 最佳代码更好。这里典型的例子是 String.indexOf() 和相关的 API,Dalvik 用内联的内在代替。类似地,System.arraycopy() 方法比带有 JIT 的 Nexus One 上的手动编码循环快约 9 倍。

小心使用原生方法

使用 Android NDK 的原生代码开发应用,不一定比用 Java 语言开发的更高效。一方面,Java 和 原生之间传递有损耗,JIT 不会跨越这些边界优化。如果你分配了原生资源(原生堆上的内存,文件描述符,或其他内容),安排及时收集这些资源可能要困难得多。你还需要为要运行的每个体系结构编译代码(而不是依赖于具有 JIT 的体系结构)。你可能甚至需要为相同的架构编译多个版本:为 G1 中的 ARM 处理器编译的原生代码无法充分利用 Nexus One 中的 ARM,以及为 Nexus One 中的 ARM 编译的代码不会在 G1 中的 ARM 上运行。

性能神话

在没有 JIT 的设备上,通过具有精确类型而不是接口的变量调用方法确实更有效。(因此例如,调用 HashMap 映射上的方法比使用 Map 映射更便宜,即使在这两种情况下映射都是 HashMap。)情况并非如此慢 2 倍,实际差异更像是慢了 6%。此外,JIT 使两者有效地难以区分。

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

总是测量

在开始优化之前,请确保你遇到需要解决的问题。确保你可以准确衡量现有的绩效,否则你将无法衡量尝试的替代方案的好处。

你可能还会发现 Traceview 对于分析很有用,但重要的是要知道当前会禁用 JIT,这可能会导致它错误地将时间错误归结为 JIT 可能能够赢回的代码。在 Traceview 数据建议进行更改以确保在没有 Traceview 的情况下运行时生成的代码实际运行得更快时,这一点尤其重要。

最后

在现在这个金三银四的面试季,我自己在网上也搜集了很多资料做成了文档和架构视频资料免费分享给大家【包括高级UI、性能优化、架构师课程、NDK、Kotlin、混合式开发(ReactNative+Weex)、Flutter等架构技术资料】,希望能帮助到您面试前的复习且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。

资料获取方式:加入Android架构交流QQ群聊:513088520 ,进群即领取资料!!!

点击链接加入群聊【Android移动架构总群】:加入群聊

资料大全

猜你喜欢

转载自blog.csdn.net/weixin_43351655/article/details/89342274