Android 性能问题最终都会落到用户体验上:页面滑动不流畅、点击后迟迟没有响应、应用打开太慢、使用一段时间后越来越卡,甚至直接崩溃。很多团队一提到性能优化就想到“换框架”“重构架构”“加缓存”,但真正有效的优化通常更朴素:先定位瓶颈,再针对关键链路做小而确定的改动。
性能优化最怕凭感觉。比如“这个页面好像卡”,如果没有数据,就很难判断是布局层级太深、主线程做了耗时任务、图片太大,还是列表刷新方式不合理。正确的思路是:
本文重点看三个最常见、也最容易产生收益的方向:布局渲染、内存占用、启动速度。
优化之前要先知道自己在优化什么。建议至少关注这些指标:
常用工具包括 Android Studio Profiler、Perfetto、Layout Inspector、Memory Profiler、Macrobenchmark、LeakCanary、Firebase Performance 或自研埋点。工具本身不是目的,它们的价值是把“感觉慢”变成“哪段代码慢”。
Android 页面渲染大致经历 measure、layout、draw。布局越复杂,嵌套越深,View 树越大,系统要做的测量和绘制工作就越多。布局优化的核心不是把 XML 写得更短,而是减少不必要的层级、重复测量和过度绘制。
早期项目里常见 LinearLayout 套 LinearLayout,再配合多个 RelativeLayout。简单页面问题不大,一旦列表项、复杂表单、卡片流里大量重复使用,就会放大成本。
ConstraintLayout 的优势是可以用较少层级表达复杂约束。例如一个头像、标题、副标题、右侧按钮的列表项,如果用多层 LinearLayout 包裹,可能需要三四层容器;用 ConstraintLayout 往往一层就够。
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="72dp"
android:padding="16dp">
<ImageView
android:id="@+id/avatar"
android:layout_width="40dp"
android:layout_height="40dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintEnd_toStartOf="@id/action"
app:layout_constraintTop_toTopOf="@id/avatar" />
<Button
android:id="@+id/action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>使用 Layout Inspector 可以直接查看 View 层级。优化时优先处理列表项、首页、复杂弹窗这些高频页面,因为它们会被反复创建和绘制。
过度绘制指同一个像素被重复绘制多次。比如 Activity 背景是白色,根布局又设置白色,卡片容器也设置白色,最后 TextView 还叠一层背景。用户只看到最终结果,GPU 却做了多余工作。
可以在开发者选项里开启 “Debug GPU overdraw” 观察页面。如果大片区域显示过度绘制严重,可以检查:
不是所有 overdraw 都必须清零。半透明蒙层、圆角卡片、复杂动效本身就有成本,关键是避免无意义的重复背景。
RecyclerView 是性能敏感区域。一个页面如果有大量列表项,单个 item 的小问题会被放大很多倍。
建议遵守几条原则:
notifyDataSetChanged() 全量刷新。onBindViewHolder() 中只做绑定,不做网络请求、数据库查询或复杂计算。例如局部刷新可以避免整个 item 重新绑定:
class MessageAdapter : ListAdapter<Message, MessageVH>(DIFF) {
override fun onBindViewHolder(holder: MessageVH, position: Int, payloads: MutableList<Any>) {
if (payloads.contains("readState")) {
holder.updateReadState(getItem(position).read)
return
}
super.onBindViewHolder(holder, position, payloads)
}
}如果只是已读状态变化,却重新加载头像、重算富文本、刷新整个 item,就会浪费大量主线程时间。
内存优化不是简单地“少 new 对象”。现代 Android 设备上,正常业务创建对象并不可怕,可怕的是对象生命周期失控、图片资源过大、缓存没有上限,以及页面关闭后仍被持有。
内存上涨不一定是泄漏。比如打开图片列表,内存上升是正常的;退出页面后经过 GC 仍然降不下来,才值得怀疑泄漏。
排查时可以这样做:
如果是缓存导致的上涨,需要看缓存策略;如果是生命周期对象无法释放,需要看引用链。
一张 1080 × 1920 的 ARGB_8888 图片,解码后大约占 8MB 内存。列表里同时加载多张大图,很容易造成内存压力。图片优化通常收益很明显:
使用 Coil 时可以指定尺寸和生命周期:
imageView.load(url) {
size(imageView.width, imageView.height)
crossfade(true)
placeholder(R.drawable.placeholder)
error(R.drawable.image_error)
}图片库不是魔法。如果传入的是超大原图地址,并且页面里同时显示很多张,仍然会产生明显内存和解码成本。
缓存可以提升速度,但无边界缓存会把性能问题变成内存问题。常见风险包括:
mutableMapOf() 长期缓存接口结果,从不清理。更稳妥的做法是为缓存设置容量、过期时间和清理时机。比如 LruCache 会按最近使用情况淘汰旧数据:
class AvatarMemoryCache(maxSize: Int) {
private val cache = object : LruCache<String, Bitmap>(maxSize) {
override fun sizeOf(key: String, value: Bitmap): Int {
return value.byteCount / 1024
}
}
fun get(url: String): Bitmap? = cache.get(url)
fun put(url: String, bitmap: Bitmap) {
cache.put(url, bitmap)
}
}缓存策略要和业务一致:头像、配置、短期列表、离线内容的生命周期都不一样,不应该用同一种全局缓存处理所有数据。
启动速度是用户最直观感受到的性能指标之一。冷启动时,系统要创建进程、初始化 Application、启动入口 Activity、加载布局、执行首屏请求和渲染。任何一个环节阻塞主线程,都会拉长等待时间。
很多项目把各种 SDK 初始化都放在 Application.onCreate():统计、推送、地图、支付、图片库、数据库、日志、配置中心。随着业务增长,启动自然越来越慢。
优化思路是给初始化任务分级:
示例:
class App : Application() {
override fun onCreate() {
super.onCreate()
initCrashReporter()
initCoreConfig()
Handler(Looper.getMainLooper()).post {
initAnalytics()
initImagePreload()
}
}
}这不是鼓励把所有任务都 post 到后面,而是提醒:启动链路只保留真正必要的工作。延迟任务也要注意总量,避免首页刚显示就立刻卡顿。
首页经常同时承担很多职责:用户信息、Banner、推荐流、弹窗、红点、配置、广告、AB 实验。如果全部串行完成后才显示页面,冷启动必然慢。
可以把首屏拆成不同优先级:
在协程里可以并发请求互不依赖的数据:
viewModelScope.launch {
val profile = async { repository.loadProfile() }
val tabs = async { repository.loadTabs() }
renderBasicPage(
user = profile.await(),
tabs = tabs.await()
)
launch { loadRecommendFeed() }
launch { loadMarketingCard() }
}注意并发不是越多越好。网络请求过多会抢占带宽,也会增加服务端压力。关键是缩短关键路径,而不是把所有事情同时扔出去。
启动优化很容易反弹。今天移走一个 SDK 初始化,明天另一个需求又把耗时任务加回 Application。因此建议用 Macrobenchmark 或线上性能监控持续观察。
Macrobenchmark 可以从外部进程启动应用,更接近真实用户路径:
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startup() = benchmarkRule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(StartupTimingMetric()),
iterations = 10,
startupMode = StartupMode.COLD
) {
pressHome()
startActivityAndWait()
}
}当启动耗时超过阈值时,让 CI 或监控报警,比上线后等用户反馈更可靠。
布局、内存、启动之外,还有一个贯穿所有性能问题的原则:主线程只做 UI 相关的轻量工作。常见的主线程陷阱包括:
onCreate() 中同步查询数据库。onBindViewHolder() 中解析大 JSON。onDraw() 中创建对象或加载资源。可以用 StrictMode 在开发阶段暴露问题:
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
}StrictMode 不一定适合直接开启所有检测并阻断流程,但它非常适合帮助团队发现“本不该在主线程发生”的操作。
一次性能优化完成后,建议留下三类东西:
否则优化成果很容易随着需求迭代流失。比如列表已经改成 DiffUtil,但后来有人为了省事又加回 notifyDataSetChanged();启动已经拆掉无关 SDK 初始化,但新功能又把同步 I/O 放进 Application。没有防线,性能会慢慢退化。
布局渲染:
onDraw() 中创建对象。内存占用:
启动速度:
Application.onCreate() 是否只做必要初始化。Android 性能优化不是追求某个神奇技巧,而是一套工程化方法:用数据定位问题,用小步改动解决瓶颈,再用监控和测试防止退化。
布局优化关注 View 树、过度绘制和列表绑定;内存优化关注对象生命周期、图片体积和缓存边界;启动优化关注关键路径、初始化分级和首屏优先级。把这些基础工作做好,大多数应用的流畅度和稳定性都会有明显提升。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。