> 编写:[kesenhoo](https://github.com/kesenhoo) - 原文:[http://developer.android.com/training/articles/perf-tips.html](http://developer.android.com/training/articles/perf-tips.html)
这篇文章主要介绍一些小细节的优化技巧,当这些小技巧综合使用起来的时候,对于整个App的性能提升还是有作用的,只是不能较大幅度的提升性能而已。选择合适的算法与数据结构才应该是你首要考虑的因素,在这篇文章中不会涉及这方面。你应该使用这篇文章中的小技巧作为平时写代码的习惯,这样能够提升代码的效率。
通常来说,高效的代码需要满足下面两个规则:
- 不要做冗余的工作
- 如果能避免,尽量不要分配内存
在优化App时最难解决的问题之一就是让App能在各种类型的设备上运行。不同版本的虚拟机在不同的处理器上会有不同的运行速度。你甚至不能简单的认为“设备X的速度是设备Y的F倍”,然后还用这种倍数关系去推测其他设备。特别的是,在模拟器上的运行速度和在实际设备上的速度没有半点关系。同样,设备有没有JIT(即时编译,译者注)也对运行速度有重大影响:在有JIT情况下的最优化代码不一定在没有JIT的情况下也最优化。
为了确保App在各设备上都能良好运行,就要确保你的代码在不同档次的设备上都尽可能的优化。
### 避免创建不必要的对象
创建对象从来不是无代价的。在线程分配池里的逐代垃圾回收器可以使临时对象的分配变得廉价一些,但是分配内存总是比不分配更昂贵。
随着你在App中分配更多的对象,你可能需要强制GC(垃圾回收,译者注),为用户体验做一个小小的减压。Android 2.3 中引入的并发GC会帮助你做这件事情,但毕竟不必要的工作应该尽量避免
因此请尽量避免创建不必要的对象,有下面一些例子来说明这个问题:
- 如果你需要返回一个String对象,并且你知道它最终会需要连接到一个StringBuffer,请修改你的函数签名和实现方式,避免直接进行连接操作,应该采用创建一个临时对象来做这个操作.
- 当从输入的数据集中抽取出String的时候,尝试返回原数据的substring对象,而不是创建一个重复的对象。你将会 new 一个 String 对象,但是它应该和原数据共享内部的 `char[]`(代价是如果你只是用原数据中的一小部分,你只需要保存这一小部分的对象在内存中)
一个稍微激进点的做法是把所有多维的数据分解成一维的数组:
- 一组int数据要比一组Integer对象要好很多。可以得知,两组一维数组要比一个二维数组更加的有效率。同样的,这个道理可以推广至其他原始数据类型。
- 如果你需要实现一个数组用来存放(Foo,Bar)的对象,记住使用Foo[]与Bar[]要比(Foo,Bar)好很多。(例外的是,为了某些好的API的设计,可以适当做一些妥协。但是在自己的代码内部,你应该多多使用分解后的容易)。
通常来说,需要避免创建更多的临时对象。更少的对象意味者更少的GC动作,GC会对用户体验有比较直接的影响。
### 选择Static而不是Virtual
如果你不需要访问一个对象的值域,请保证这个方法是static类型的,这样方法调用将快15%-20%。这是一个好的习惯,因为你可以从方法声明中得知调用无法改变这个对象的状态。
### 常量声明为Static Final
考虑下面这种声明的方式
~~~
static int intVal = 42;
static String strVal = "Hello, world!";
~~~
编译器会使用一个初始化类的函数,然后当类第一次被使用的时候执行。这个函数将42存入`intVal`,还从class文件的常量表中提取了`strVal`的引用。当之后使用`intVal`或`strVal`的时候,他们会直接被查询到。
我们可以用`final`关键字来优化:
~~~
static final int intVal = 42;
static final String strVal = "Hello, world!";
~~~
这时再也不需要上面的方法了,因为final声明的常量进入了静态dex文件的域初始化部分。调用`intVal`的代码会直接使用42,调用`strVal`的代码也会使用一个相对廉价的“字符串常量”指令,而不是查表。
> 注意:这个优化方法只对原始类型和String类型有效,而不是任意引用类型。不过,在必要时使用`static final`是个很好的习惯
### 避免内部的Getters/Setters
像C++等native language,通常使用getters(i = getCount())而不是直接访问变量(i = mCount)。这是编写C++的一种优秀习惯,而且通常也被其他面向对象的语言所采用,例如C#与Java,因为编译器通常会做inline访问,而且你需要限制或者调试变量,你可以在任何时候在getter/setter里面添加代码。
然而,在Android上,这是一个糟糕的写法。虚函数的调用比起直接访问变量要耗费更多。在面向对象编程中,将getter和setting暴露给公用接口是合理的,但在类内部应该仅仅使用域直接访问。
在没有JIT(Just In Time Compiler)时,直接访问变量的速度是调用getter的3倍。有JIT时,直接访问变量的速度是通过getter访问的7倍。
请注意,如果你使用[ProGuard](http://developer.android.com/tools/help/proguard.html), 你可以获得同样的效果,因为ProGuard可以为你inline accessors.
### 使用增强的For循环
增强的For循环(也被称为 for-each 循环)可以被用在实现了 Iterable 接口的 collections 以及数组上。使用collection的时候,Iterator (迭代器,译者注) 会被分配,用于for-each调用`hasNext()`和`next()`方法。使用ArrayList时,手写的计数式for循环会快3倍(不管有没有JIT),但是对于其他collection,增强的for-each循环写法会和迭代器写法的效率一样。
请比较下面三种循环的方法:
~~~
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-each。
所以请尽量使用for-each的方法,但是对于ArrayList,请使用方法one()。
> 你还可以参考 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`),它直接访问了外部类中的私有方法以及私有成员对象。这是合法的,这段代码也会如同预期一样打印出"Value is 27"。
问题是,VM因为`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()函数时,它都会调用这些静态方法。这意味着,上面的代码可以归结为,通过accessor函数来访问成员变量。早些时候我们说过,通过accessor会比直接访问域要慢。所以,这是一个特定语言用法造成性能降低的例子。
如果你正在性能热区(hotspot:高频率、重复执行的代码段)使用像这样的代码,你可以把内部类需要访问的域和方法声明为包级访问,而不是私有访问权限。不幸的是,这意味着在相同包中的其他类也可以直接访问这些域,所以在公开的API中你不能这样做。
### 避免使用float类型
Android系统中float类型的数据存取速度是int类型的一半,尽量优先采用int类型。
就速度而言,现代硬件上,float 和 double 的速度是一样的。空间而言,double 是两倍float的大小。在桌面机上,空间不是问题的情况下,你应该使用 double 。
同样,对于整型,有些处理器实现了硬件几倍的乘法,但是没有除法。这时,整型的除法和取余是在软件内部实现的,这在你使用哈希表或大量输血操作时要考虑到。
### 使用库函数
除了那些常见的让你多使用自带库函数的理由以外,记得系统函数有时可以替代第三方库,并且还有汇编级别的优化,他们通常比带有JIT的Java编译出来的代码更高效。典型的例子是:Android API 中的 `String.indexOf()`,Dalvik出于内联性能考虑将其替换。同样 `System.arraycopy()`函数也被替换,这样的性能在Nexus One测试,比手写的for循环并使用JIT还快9倍。
> 参见 Josh Bloch 的 《Effective Java》这本书的第47条
### 谨慎使用native函数
结合Android NDK使用native代码开发,并不总是比Java直接开发的效率更好的。Java转native代码是有代价的,而且JIT不能在这种情况下做优化。如果你在native代码中分配资源(比如native堆上的内存,文件描述符等等),这会对收集这些资源造成巨大的困难。你同时也需要为各种架构重新编译代码(而不是依赖JIT)。你甚至对已同样架构的设备都需要编译多个版本:为G1的ARM架构编译的版本不能完全使用Nexus One上ARM架构的优势,反之亦然。
Native 代码是在你已经有本地代码,想把它移植到Android平台时有优势,而不是为了优化已有的Android Java代码使用。
如果你要使用JNI,请学习[JNI Tips](http://developer.android.com/guide/practices/jni.html)
> 参见 Josh Bloch 的 《Effective Java》这本书的第54条
### 关于性能的误区
在没有JIT的设备上,使用一种确切的数据类型确实要比抽象的数据类型速度要更有效率(例如,调用`HashMap map`要比调用`Map map`效率更高)。有误传效率要高一倍,实际上只是6%左右。而且,在JIT之后,他们直接并没有大多差异。
在没有JIT的设备上,读取缓存域比直接读取实际数据大概快20%。有JIT时,域读取和本地读取基本无差。所以优化并不值得除非你觉得能让你的代码更易读(这对 final, static, static final 域同样适用)。
### 关于测量
在优化之前,你应该决定你遇到了性能问题。你应该确保你能够准确测量出现在的性能,否则你也不会知道优化是否真的有效。
本章节中所有的技巧都需要Benchmark(基准测试)的支持。Benchmark可以在 [code.google.com "dalvik" project](http://code.google.com/p/dalvik/source/browse/#svn/trunk/benchmarks) 中找到
Benchmark是基于Java版本的 [Caliper](http://code.google.com/p/caliper/) microbenchmarking(基准微测,译者注)框架开发的。Microbenchmarking很难做准确,所以Caliper帮你完成这部分工作,甚至还帮你测了你没想到需要测量的部分(因为,VM帮你管理了代码优化,你很难知道这部分优化有多大效果)。我们强烈推荐使用Caliper来做你的基准微测工作。
我们也可以用[Traceview](http://developer.android.com/tools/debugging/debugging-tracing.html) 来测量,但是测量的数据是没有经过JIT优化的,所以实际的效果应该是要比测量的数据稍微好些。
关于如何测量与调试,还可以参考下面两篇文章:
- [Profiling with Traceview and dmtracedump](http://developer.android.com/tools/debugging/debugging-tracing.html)
- [Analysing Display and Performance with Systrace](http://developer.android.com/tools/debugging/systrace.html)
- 序言
- Android入门基础:从这里开始
- 建立第一个App
- 创建Android项目
- 执行Android程序
- 建立简单的用户界面
- 启动其他的Activity
- 添加ActionBar
- 建立ActionBar
- 添加Action按钮
- 自定义ActionBar的风格
- ActionBar的覆盖层叠
- 兼容不同的设备
- 适配不同的语言
- 适配不同的屏幕
- 适配不同的系统版本
- 管理Activity的生命周期
- 启动与销毁Activity
- 暂停与恢复Activity
- 停止与重启Activity
- 重新创建Activity
- 使用Fragment建立动态的UI
- 创建一个Fragment
- 建立灵活动态的UI
- Fragments之间的交互
- 数据保存
- 保存到Preference
- 保存到文件
- 保存到数据库
- 与其他应用的交互
- Intent的发送
- 接收Activity返回的结果
- Intent过滤
- Android分享操作
- 分享简单的数据
- 给其他App发送简单的数据
- 接收从其他App返回的数据
- 给ActionBar增加分享功能
- 分享文件
- 建立文件分享
- 分享文件
- 请求分享一个文件
- 获取文件信息
- 使用NFC分享文件
- 发送文件给其他设备
- 接收其他设备的文件
- Android多媒体
- 管理音频播放
- 控制音量与音频播放
- 管理音频焦点
- 兼容音频输出设备
- 拍照
- 简单的拍照
- 简单的录像
- 控制相机硬件
- 打印
- 打印照片
- 打印HTML文档
- 打印自定义文档
- Android图像与动画
- 高效显示Bitmap
- 高效加载大图
- 非UI线程处理Bitmap
- 缓存Bitmap
- 管理Bitmap的内存
- 在UI上显示Bitmap
- 使用OpenGL ES显示图像
- 建立OpenGL ES的环境
- 定义Shapes
- 绘制Shapes
- 运用投影与相机视图
- 添加移动
- 响应触摸事件
- 添加动画
- View间渐变
- 使用ViewPager实现屏幕侧滑
- 展示卡片翻转动画
- 缩放View
- 布局变更动画
- Android网络连接与云服务
- 无线连接设备
- 使得网络服务可发现
- 使用WiFi建立P2P连接
- 使用WiFi P2P服务
- 执行网络操作
- 连接到网络
- 管理网络
- 解析XML数据
- 高效下载
- 为网络访问更加高效而优化下载
- 最小化更新操作的影响
- 避免下载多余的数据
- 根据网络类型改变下载模式
- 云同步
- 使用备份API
- 使用Google Cloud Messaging
- 解决云同步的保存冲突
- 使用Sync Adapter传输数据
- 创建Stub授权器
- 创建Stub Content Provider
- 创建Sync Adpater
- 执行Sync Adpater
- 使用Volley执行网络数据传输
- 发送简单的网络请求
- 建立请求队列
- 创建标准的网络请求
- 实现自定义的网络请求
- Android联系人与位置信息
- Android联系人信息
- 获取联系人列表
- 获取联系人详情
- 使用Intents修改联系人信息
- 显示联系人头像
- Android位置信息
- 获取最后可知位置
- 获取位置更新
- 显示位置地址
- 创建和监视地理围栏
- Android可穿戴应用
- 赋予Notification可穿戴特性
- 创建Notification
- 在Notifcation中接收语音输入
- 为Notification添加显示页面
- 以Stack的方式显示Notifications
- 创建可穿戴的应用
- 创建并运行可穿戴应用
- 创建自定义的布局
- 添加语音功能
- 打包可穿戴应用
- 通过蓝牙进行调试
- 创建自定义的UI
- 定义Layouts
- 创建Cards
- 创建Lists
- 创建2D-Picker
- 创建确认界面
- 退出全屏的Activity
- 发送并同步数据
- 访问可穿戴数据层
- 同步数据单元
- 传输资源
- 发送与接收消息
- 处理数据层的事件
- Android TV应用
- 创建TV应用
- 创建TV应用的第一步
- 处理TV硬件部分
- 创建TV的布局文件
- 创建TV的导航栏
- 创建TV播放应用
- 创建目录浏览器
- 提供一个Card视图
- 创建详情页
- 显示正在播放卡片
- 帮助用户在TV上探索内容
- TV上的推荐内容
- 使得TV App能够被搜索
- 使用TV应用进行搜索
- 创建TV游戏应用
- 创建TV直播应用
- TV Apps Checklist
- Android企业级应用
- Ensuring Compatibility with Managed Profiles
- Implementing App Restrictions
- Building a Work Policy Controller
- Android交互设计
- 设计高效的导航
- 规划屏幕界面与他们之间的关系
- 为多种大小的屏幕进行规划
- 提供向下和横向导航
- 提供向上和历史导航
- 综合:设计样例 App
- 实现高效的导航
- 使用Tabs创建Swipe视图
- 创建抽屉导航
- 提供向上的导航
- 提供向后的导航
- 实现向下的导航
- 通知提示用户
- 建立Notification
- 当启动Activity时保留导航
- 更新Notification
- 使用BigView风格
- 显示Notification进度
- 增加搜索功能
- 建立搜索界面
- 保存并搜索数据
- 保持向下兼容
- 使得你的App内容可被Google搜索
- 为App内容开启深度链接
- 为索引指定App内容
- Android界面设计
- 为多屏幕设计
- 兼容不同的屏幕大小
- 兼容不同的屏幕密度
- 实现可适应的UI
- 创建自定义View
- 创建自定义的View类
- 实现自定义View的绘制
- 使得View可交互
- 优化自定义View
- 创建向后兼容的UI
- 抽象新的APIs
- 代理至新的APIs
- 使用旧的APIs实现新API的效果
- 使用版本敏感的组件
- 实现辅助功能
- 开发辅助程序
- 开发辅助服务
- 管理系统UI
- 淡化系统Bar
- 隐藏系统Bar
- 隐藏导航Bar
- 全屏沉浸式应用
- 响应UI可见性的变化
- 创建使用Material Design的应用
- 开始使用Material Design
- 使用Material的主题
- 创建Lists与Cards
- 定义Shadows与Clipping视图
- 使用Drawables
- 自定义动画
- 维护兼容性
- Android用户输入
- 使用触摸手势
- 检测常用的手势
- 跟踪手势移动
- Scroll手势动画
- 处理多触摸手势
- 拖拽与缩放
- 管理ViewGroup中的触摸事件
- 处理键盘输入
- 指定输入法类型
- 处理输入法可见性
- 兼容键盘导航
- 处理按键动作
- 兼容游戏控制器
- 处理控制器输入动作
- 支持不同的Android系统版本
- 支持多个控制器
- Android后台任务
- 在IntentService中执行后台任务
- 创建IntentService
- 发送工作任务到IntentService
- 报告后台任务执行状态
- 使用CursorLoader在后台加载数据
- 使用CursorLoader执行查询任务
- 处理查询的结果
- 管理设备的唤醒状态
- 保持设备的唤醒
- 制定重复定时的任务
- Android性能优化
- 管理应用的内存
- 代码性能优化建议
- 提升Layout的性能
- 优化layout的层级
- 使用include标签重用layouts
- 按需加载视图
- 使得ListView滑动顺畅
- 优化电池寿命
- 监测电量与充电状态
- 判断与监测Docking状态
- 判断与监测网络连接状态
- 根据需要操作Broadcast接受者
- 多线程操作
- 在一个线程中执行一段特定的代码
- 为多线程创建线程池
- 启动与停止线程池中的线程
- 与UI线程通信
- 避免出现程序无响应ANR
- JNI使用指南
- 优化多核处理器(SMP)下的Android程序
- Android安全与隐私
- Security Tips
- 使用HTTPS与SSL
- 为防止SSL漏洞而更新Security
- 使用设备管理条例增强安全性
- Android测试程序
- 测试你的Activity
- 建立测试环境
- 创建与执行测试用例
- 测试UI组件
- 创建单元测试
- 创建功能测试
- 術語表