Android 内存优化系列文章:
- Android代码内存优化建议-Android官方篇
- Android代码内存优化建议-Java官方篇
- Android代码内存优化建议-Android资源篇
- Android代码内存优化建议-OnTrimMemory优化
这篇文章介绍的是 Android Java 代码中的微优化。选择合适的算法与数据结构仍应放在前面;减少热点路径上的重复工作和临时分配,才是这些技巧的主要用途。
本文最初整理自 Google Android Training 的 Performance Tips。标题中的“Java 官方篇”指这份 Android 文档里的 Java 代码建议,并非 Oracle 的 Java 性能规范。
技术复核:2026-10-13。 旧文中的固定速度倍率、部分字符串实现与编译器限制已经过期,不能作为当前 Android 的通用结论。本文保留原来的 11 个主题,并分别说明历史条件与今天的使用方式。历史原文可查看 AOSP android-5.1.1_r1 中的 Performance Tips。
正文
通常来说,高效的代码需要避免两类开销:
- 不必要的重复工作。
- 热点路径上可以省掉的临时对象分配。
这不意味着为了少分配一个对象,就要破坏接口或让状态管理变得复杂。代码的执行效果还受 CPU、系统版本、运行时和编译状态影响。Android 7.0(API 24)引入了与 AOT 配合的 ART JIT;同一个方法可能经过解释执行、JIT 编译或 AOT 编译。旧文按“有无 JIT”给出的排名,不能直接套到现在的发布包上。参见 Android 7.0 的 JIT/AOT 说明 与 ART JIT 文档。
1)避免创建不必要的对象
虽然 GC 可以回收不再使用的对象,分配和回收仍有成本。优先检查高频路径上短命对象的来源,而不是单纯数代码里有多少个 new。
- 如果一个方法返回的字符串最终只会追加到已有的
StringBuffer,可以让方法直接向缓冲区追加,省掉中间字符串。单线程拼接也可以使用StringBuilder;不要为这点优化引入共享的可变缓冲区。 - 旧文建议用
substring()共享原字符串的char[]。这个实现可在 Android 4.4.4 的 Dalvik String 源码 中找到,但小子串也可能使整块原始字符数组继续存活。当前String.substring()API 规定返回的字符范围,没有承诺零拷贝或共享底层存储;不能把旧实现当成跨版本的省内存技巧。
数组布局也需要按数据用途选择。int[] 能避免为每个值保存对象引用,通常比装箱的 Integer[] 更紧凑,但它不能表示 null。旧文讨论的是把 (int, int) 或 (Foo, Bar) 元组对象数组拆成两组平行数组,以减少元组对象;不是“任意二维数组拆开都会更快”。平行数组还需要维护索引对应关系。只有热点和内存占用的测量支持这种改动时,才值得增加这份维护成本。
减少临时分配可能减轻 GC 压力,但一次分配不等于一次 GC,少几个对象也不保证页面更流畅。
2)选择Static而不是Virtual
如果方法在语义上不依赖某个实例,也不需要被实例方法覆写,可以声明为 static。旧文的“快 15%–20%”是早期运行时的经验数字,当前结果还取决于内联、调用位置和实际编译产物。
static 只表示没有隐式的 this,不代表没有副作用:它仍然可以修改静态字段,或修改传入的对象。不要只为这个旧倍率改变多态接口。
3)常量声明为Static Final
先看下面这种字段声明:
1 | static int intVal = 42; |
这些初始化赋值会进入类初始化方法 <clinit>;类初始化完成后,普通字段读取不会每次重新执行 <clinit>。
对于值固定的常量,可以写成:
1 | static final int intVal = 42; |
这里的两个字段都是编译期常量,引用处可以直接使用常量值。按照 Java SE 25 语言规范 §4.12.4,这要求 final 变量具有基本类型或 String 类型,并使用常量表达式初始化。static final 对象引用,或通过方法调用得到的值,都不能一概套用这个结论。引用不能重新赋值,也不等于它指向的对象不可变。
4)避免内部的Getters/Setters
这一节的标题保留了旧文的建议,但“getter 在 Android 上是糟糕写法”已经不适合作为规则。类内部读取普通字段可以很直接;如果 getter/setter 包含校验、延迟初始化或同步,绕过它就改变了行为。
旧文给出的 3 倍、7 倍差距不能代表现在的 ART。构建时的 R8 和运行时编译器都有机会内联简单方法,但具体调用是否被优化需要看产物与测量。R8 从 Android Gradle Plugin 3.4.0 起成为默认代码压缩器;这不等于所有构建都自动开启压缩和优化。检查实际 release 配置,参见 AGP 3.4.0 发布说明 和 启用应用优化。
保留有用的封装,只在已确认的热点里比较访问方式。
5)使用增强的For循环写法
下面三个方法用不同的循环写法求和,并返回结果。这是写法对比,不是可用于排名的微基准:
1 | public class LoopExamples { |
旧文按 zero()、one()、two() 排名,依据的是当时 JIT 尚未优化掉某些循环内读取的条件。今天不能据此断言 zero() 一定最慢,或手动缓存长度一定有收益。
增强 for 需要区分两种情况:Java 语言规范 §14.14.2 中,数组遍历转换为索引循环;遍历 Iterable 则通过迭代器。前者不会仅因使用 for-each 就创建迭代器,后者的实际分配与调用成本还受具体实现及优化影响。
数组遍历通常可以按可读性选择。对于 ArrayList,索引循环可以作为热点的候选方案,但不能把它推广成所有集合都更快的规则,也不能忽略遍历期间修改集合的语义。
6)使用包级访问而不是内部类的私有访问
这也是需要加上版本条件的历史建议。参考下面一段代码,调用 new Foo().run() 会打印 Value is 27:
1 | public class Foo { |
Java 语言允许这里的内部类访问外部类私有成员。旧编译链为了满足当时虚拟机的访问规则,会生成类似 access$100、access$200 的合成访问方法,把内部类的访问转接过去。这些名字是编译器细节,不是需要手写的 API。
Java 11 的 JVM 引入 nestmate 访问规则,同一个 nest 中的类可以访问彼此的私有成员,见 JVMS 11 §5.4.4。Android APK 还会经过 D8/R8 转换为 DEX,不能仅凭桌面 JDK 版本就断言最终一定有、或一定没有辅助方法。
当前代码应按封装需要选择 private。改成包级访问会把成员暴露给同包其他类;只有确认最终产物中存在有影响的额外开销,并评估这个可见性变化后,才考虑调整。
7)避免使用float类型
这条旧建议已过期。 “float 速度是 int 的一半”不是 Android API 的保证,也不能泛化成所有浮点运算或数据存取的固定比例。结果取决于处理器、指令、数据依赖和编译优化。
先按数值范围、精度和算法选择类型。需要小数的计算不能直接改成整数运算;整数截断和溢出可能改变结果。只有测出具体瓶颈后,才比较保持数值要求的替代实现。
8)使用库函数
优先考虑语义合适的库函数,例如 System.arraycopy()。它定义了数组类型、范围检查和源目标区间重叠时的复制行为,手写循环需要自行保证这些条件。
旧文的“9 倍多”特指 Nexus One、开启 JIT 的测试,不能承诺今天仍有同样倍率。是否更快、快多少,需要根据数组类型、长度和目标设备测量;也不应把库函数概括成“总是通过 JNI 执行”。
原文还推荐了《Effective Java》第二版第 47 条“了解和使用类库”;第三版对应第 59 条。
9)谨慎使用native函数
迁移已有 native 库,或者处理已确认的计算热点时,可以评估 JNI。性能比较必须算上跨边界调用、数据转换和复制的成本,不能只比较 native 函数内部的耗时。
尽量减少细碎的跨边界交互,同时明确 native 资源和 JNI 引用由谁释放。当前入口为 Android NDK 的 JNI Tips。
10)关于性能的误区
Map<K, V> map = new HashMap<>(); 和声明为 HashMap<K, V> 时,可以指向同一种实现。这里讨论的是调用点的声明类型与分派,不是两种不同容器的数据结构性能。
旧文的“约 6%”与“JIT 后几乎无差异”同样有当时的运行环境前提。先按接口设计选择类型,再对确有开销的热点检查优化结果;不要为了未经测量的分派差异,把所有 Map 都改成 HashMap。
11)关于测量
旧文用 Traceview 观察 Dalvik 时,方法跟踪会干扰 JIT;这不能推广为“所有 profiler 都会关闭 JIT”,也不能从带插桩的数据推算实际运行只会稍快一点。
Traceview 已弃用,官方对 Android Studio 3.2 及以上版本建议改用 CPU Profiler。现在可以先用 Android Studio Profiler 或 Perfetto 定位热点,再选择测量范围:
- Jetpack Microbenchmark:比较已定位的方法或循环,处理预热并观察耗时、分配量。测试必须使用真实输入并消费结果,防止无用计算被优化掉。
- Macrobenchmark 与应用性能测量:检查启动、滚动等用户流程,判断局部优化是否改善实际体验。
比较时固定设备、系统版本、构建配置和编译状态,在真实设备上使用适合性能测量的非 debuggable 构建。记录预热与采样方式,避免把 Debug、模拟器或不同编译状态的结果直接放在一起排名。
关于我 && 博客
下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!
- 博主个人介绍 :里面有个人的微信和微信群链接。
- 本博客内容导航 :个人博客内容的一个导航。
- 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
- Android性能优化知识星球:付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。
一个人可以走的更快 , 一群人可以走的更远
