banner
NEWS LETTER

Android 性能优化实践:从定位问题到建立日常治理

Scroll down

Android 性能优化不是上线前临时跑一次 Profiler,也不是看到卡顿后盲目改几行代码。真正稳定的做法,是把性能问题拆成可观测、可复现、可验证的工程流程:先知道慢在哪里,再判断为什么慢,最后用指标证明优化确实有效。

一、先定义性能目标

性能优化最容易踩的坑,是一开始就讨论“怎么优化”,却没有定义“优化到什么程度”。

在项目里可以先固定几类指标:

  • 启动耗时:冷启动、温启动、首屏可交互时间。
  • 页面流畅度:掉帧、长帧、列表滑动是否稳定。
  • 内存表现:峰值内存、频繁 GC、大图和缓存占用。
  • 网络链路:接口耗时、重试、弱网下的用户等待时间。
  • 包体体积:APK/AAB 大小、so、资源、重复依赖。
  • 耗电和后台任务:定位、轮询、WorkManager、前台服务。

指标不需要一开始就非常复杂,但必须能被重复测量。否则优化结果只能依赖主观体感,很难在版本迭代中持续保持。

二、启动优化:减少主线程和首屏负担

启动慢通常不是单点问题,而是一组初始化逻辑叠加后的结果。

常见问题包括:

  • Application.onCreate() 初始化过多 SDK。
  • 主线程做磁盘 IO、数据库打开、JSON 解析。
  • 首屏布局层级过深或包含复杂自定义 View。
  • 首页一次性请求太多接口。
  • 启动时加载大图、字体、动态配置。

实践上可以按优先级处理:

  1. 只保留首屏必须初始化的逻辑。
  2. 非必要 SDK 延迟到用户真正使用前。
  3. IO 操作放到后台线程,并控制结果回主线程的时机。
  4. 首页数据分层加载,先展示骨架或缓存,再刷新网络数据。
  5. 用 Baseline Profile 优化核心启动路径和高频页面路径。

一个简单的延迟初始化示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class App : Application() {
override fun onCreate() {
super.onCreate()

initCrashReporter()
initLogger()

ProcessLifecycleOwner.get().lifecycle.addObserver(
object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
AppScope.launch {
initNonCriticalSdk()
}
owner.lifecycle.removeObserver(this)
}
}
)
}
}

这里的关键不是“全部异步”,而是明确哪些初始化真的影响首屏,哪些可以后移。如果把所有任务同时丢到线程池,也可能造成 CPU 争抢,反而影响启动。

三、卡顿优化:把主线程当成稀缺资源

Android 的 UI 渲染依赖主线程协作完成。主线程被长时间占用,就会出现掉帧、点击无响应和动画不顺滑。

排查卡顿时可以重点看:

  • 主线程是否有耗时方法。
  • RecyclerView 是否频繁创建对象。
  • 图片解码是否发生在主线程。
  • onBindViewHolder() 是否做了复杂计算。
  • 自定义 View 的 onDraw() 是否分配对象。
  • 是否存在同步锁竞争。

列表页面是最常见的卡顿来源。优化时可以先从绑定逻辑入手:

1
2
3
4
5
6
7
8
9
10
11
class ArticleAdapter : ListAdapter<Article, ArticleViewHolder>(DIFF) {
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
val item = getItem(position)
holder.title.text = item.title
holder.summary.text = item.summary
holder.cover.load(item.coverUrl) {
crossfade(false)
placeholder(R.drawable.placeholder_cover)
}
}
}

列表绑定中应该避免:

  • 每次绑定都格式化复杂时间。
  • 每次绑定都创建新的正则、画笔、监听器。
  • 在绑定阶段同步查询数据库。
  • 在绑定阶段直接解码 Bitmap。
  • 使用 notifyDataSetChanged() 刷新整个列表。

如果数据量较大,优先使用 ListAdapterDiffUtil 或 Paging,让刷新范围尽可能小。

四、内存优化:重点治理泄漏和大对象

内存问题的表现不一定是立刻崩溃,更多时候是页面越用越卡、GC 频繁、图片列表抖动。

常见风险包括:

  • Activity 或 Fragment 被单例、回调、Handler 持有。
  • 协程作用域超过页面生命周期。
  • Bitmap 缓存没有边界。
  • WebView、播放器、地图组件释放不彻底。
  • 大列表一次性加载完整数据。

页面里的协程建议绑定生命周期:

1
2
3
4
5
6
7
8
9
10
11
class ArticleFragment : Fragment(R.layout.fragment_article) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
render(state)
}
}
}
}
}

这里使用 viewLifecycleOwner,可以避免 Fragment View 销毁后仍继续更新旧 View。对于 ViewBinding,也要在 onDestroyView() 里释放引用。

1
2
3
4
override fun onDestroyView() {
binding = null
super.onDestroyView()
}

如果项目里图片很多,需要检查图片加载库的内存缓存、磁盘缓存、缩略图策略和列表预加载策略。大图展示页最好按目标尺寸加载,不要把原图完整解码到内存。

五、网络优化:减少用户等待

网络性能不只看接口平均耗时,还要看弱网、失败重试和页面串行等待。

可以从几个方向入手:

  • 合并首屏必要接口,减少串行请求。
  • 非关键数据延迟加载。
  • 对稳定数据做本地缓存。
  • 请求失败时保留旧数据,而不是清空页面。
  • 给长请求设置合理超时。
  • 避免重复点击触发重复请求。

Repository 层可以统一处理缓存和刷新:

1
2
3
4
5
6
7
8
9
10
11
12
13
class UserRepository(
private val dao: UserDao,
private val api: UserApi
) {
fun observeUser(userId: String): Flow<UserEntity?> {
return dao.observeById(userId)
}

suspend fun refreshUser(userId: String) {
val remote = api.getUser(userId)
dao.upsert(remote.toEntity())
}
}

UI 先展示本地数据,再触发远端刷新。这样即使网络较慢,用户也不会一直面对空白页面。

六、布局优化:少嵌套,少重复测量

布局性能问题常出现在复杂首页、动态表单、嵌套列表和弹窗里。

建议优先检查:

  • 是否存在没有必要的多层嵌套。
  • ConstraintLayout 是否能替代多层 LinearLayout。
  • RecyclerView 是否嵌套 RecyclerView。
  • 图片是否设置了明确尺寸。
  • 文本是否因为宽高不稳定导致反复测量。
  • 是否过度使用 wrap_content 造成额外测量成本。

优化布局时不要只追求“层级越少越好”,还要保持结构可维护。一个清晰的布局,比为了少一层而写出难维护的约束关系更可靠。

七、包体优化:从依赖和资源开始

包体变大通常来自三类内容:依赖、资源、Native 库。

可以固定做这些检查:

  • 开启 R8 和资源压缩。
  • 删除无用图片、无用语言包、重复资源。
  • 检查第三方 SDK 是否引入过多传递依赖。
  • 按 ABI 控制 so 输出。
  • 大资源放到服务端或动态下发。
  • 使用矢量图替代简单 PNG。

Gradle 中常见配置:

1
2
3
4
5
6
7
8
9
10
11
12
android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}

包体优化要配合回归测试。尤其是混淆和资源压缩,必须覆盖反射、WebView JS 调用、路由跳转、序列化等路径。

八、建立日常治理机制

性能优化最怕“一次性运动”。真正有效的方式,是把性能指标放进日常开发流程。

可以建立一套轻量机制:

  • 每次发版前记录启动耗时、包体大小、核心页面帧率。
  • 新增重型 SDK 必须说明初始化时机和包体影响。
  • 大图、长列表、复杂自定义 View 合入前做专项检查。
  • 对启动链路和首页链路做基准测试。
  • 线上采集卡顿、ANR、OOM 和慢请求。
  • 性能问题进入缺陷管理,而不是只在群里口头提醒。

对于团队来说,性能优化的目标不是让某一次测试数据漂亮,而是让性能退化能被及时发现、及时止损。

九、一个实用排查顺序

如果接手一个已经变慢的 Android 项目,可以按这个顺序推进:

  1. 先测冷启动、首页加载、核心列表滑动、内存峰值。
  2. 用 Profiler、Perfetto 或系统 Trace 找主线程长任务。
  3. 清理 Application 中非必要初始化。
  4. 优化首页数据加载顺序。
  5. 排查列表绑定、图片加载和局部刷新。
  6. 用 LeakCanary 检查核心页面泄漏。
  7. 分析 APK,处理明显的大资源和冗余依赖。
  8. 把关键指标写入发版检查清单。

这个顺序的好处是先处理用户最容易感知的问题,再处理长期治理问题。每一步都能产生可验证的结果。

总结

Android 性能优化的核心是工程化,而不是技巧堆叠。

启动、卡顿、内存、网络、布局、包体都可以优化,但优先级应该由用户体验和数据决定。先建立指标,再定位瓶颈,最后用测试和线上监控确认效果。只有这样,性能优化才不会停留在“感觉变快了”,而是变成项目可以长期复用的能力。

其他文章
目录导航 置顶
  1. 1. 一、先定义性能目标
  2. 2. 二、启动优化:减少主线程和首屏负担
  3. 3. 三、卡顿优化:把主线程当成稀缺资源
  4. 4. 四、内存优化:重点治理泄漏和大对象
  5. 5. 五、网络优化:减少用户等待
  6. 6. 六、布局优化:少嵌套,少重复测量
  7. 7. 七、包体优化:从依赖和资源开始
  8. 8. 八、建立日常治理机制
  9. 9. 九、一个实用排查顺序
  10. 10. 总结
请输入关键词进行搜索