前言
2024 年底我接到一个"性能救火"项目——某 Top 内容平台 Android 客户端的核心页 P99 从 600ms 退化到 800ms。每周线上有 0.8% 的用户遇到白屏、卡死、双击重放。老板说:"你能不能把它压回去?"
压力在于:
那是 50 万行代码的工程,已经迭代了 4 年;
同时有 8 个业务线并行开发;
没有"冻结期",意味着改性能的 PR 要能在一周内合进去。
三个月后我们把核心页 P99 压到 220ms,无任何线上事故。这篇文章是复盘。
一、先用数据定义"慢"
最大的失败是"凭感觉优化"。我们第一步搭了一个基线采集系统:
class PerfTracer private constructor(private val tag: String) {
private val start = SystemClock.elapsedRealtime()
private val sections = mutableListOf<Pair<String, Long>>()
private var last = start
fun next(label: String) {
val now = SystemClock.elapsedRealtime()
sections.add(label to (now - last))
last = now
}
fun end() {
val total = SystemClock.elapsedRealtime() - start
// 上报到 APM
EventBus.post(PerfEvent(tag, total, sections))
}
companion object {
inline fun <T> trace(tag: String, block: () -> T): T {
val t = PerfTracer(tag)
try {
block()
} finally {
t.next("end")
t.end()
}
return block()
}
}
}
业务里只需 PerfTracer.trace("FeedPager.firstPaint") { ... }。这是基线 KPI:所有改动都要"敢"上报。
二、瀑布分析:先找到最长的短板
60% 的慢来自 20% 的代码。我们拿 Systrace 拉了 12 帧的瀑布:
结论:90% 的优化集中在前 5 项——并且只有 2 个在主线程上。
三、第一刀:Application 启动瘦身
读 Application 看,单例有 17 个。其中 6 个一启动就在做 IO 或读 SP:
// 改造前
class App : Application() {
override fun onCreate() {
UserManager.init(this) // 10ms 同步 IO
PushManager.init(this) // 25ms 注册 FCM
AnalyticsManager.init(this) // 18ms 数据库连接
DeviceInfoManager.init() // 12ms
ConfigManager.init() // 30ms 远端拉
BuglyManager.init() // 35ms
// ... 还有 11 个
}
}
把"启动就必须的"和"延迟可接受的"切开:
class App : Application() {
override fun onCreate() {
// 1. 主线程必需
BuglyManager.init() // 安全网
CrashHandler.install()
ProcessLifecycleObserver.start()
// 2. 立即推到后台线程
GlobalScope.launch(Dispatchers.IO) {
UserManager.warmInit() // 首次访问时再实例化
PushManager.init() // 推到 WorkManager 初始任务里
AnalyticsManager.init()
ConfigManager.fetchAsync()
}
// 3. lazy 化(按需加载)
registerActivityLifecycleCallbacks(AppLazyInit)
}
}
实测优化:启动 Application 阶段 -180ms。
四、第二刀:SplashActivity 用 ViewStub 减负
原本 SplashActivity 用了三层 ConstraintLayout 嵌套,加 6 个 ImageView。系统测下来这屏幕 90ms 用来构建视图层级。
<FrameLayout>
<ImageView android:id="@+id/logo" />
<ViewStub android:id="@+id/loading_stub"
android:layout="@layout/loading_view"
android:inflatedId="@+id/loading" />
</FrameLayout>
ViewStub 只在 logo 动画结束后 inflate loading 视图。无 inflate 时整屏 inflate 60ms,直接砍到 30ms。
五、第三刀:Feed 列表 P99 优化
这是最大的一块儿。
5.1 多级缓存
理论上能命中 75% 的"上一页/下一页"操作完全跳过网络。
5.2 列表项复用
每条 Feed 内含 3 张图(图说+1 缩略图+1 头像)+ 5 个文本块+1 交互按钮。我们改用 itemViewType 5 类:
override fun getItemViewType(position: Int): Int = when (feed) {
is Feed.Video -> TYPE_VIDEO
is Feed.ImageGroup -> TYPE_IMAGE_GROUP
is Feed.Article -> TYPE_ARTICLE
is Feed.Ad -> TYPE_AD
else -> TYPE_TEXT
}
复用错位率从 23% 降到 4%,对应滑动稳定 FPS 提升 15。
5.3 离屏渲染与预拉
首页可见 4 个 Item,我们预渲染第 5、6 个:
RecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
val lastVisible = layoutManager.findLastVisibleItemPosition()
if (lastVisible >= totalItemCount - 2) {
// 触发下一页预拉
FeedPaging.requestNext()
}
}
})
预拉限制:只在 Wi-Fi + 电量充足时开启,移动网络降级到预渲染下一项。
六、第四刀:Compose 与 View 混用的雷区
主页用 Compose + View 混合。我们发现:
从 Compose 跳 View 会触发 ComposeView 的 dispose,导致 每一次回退都会重新 attach。
ViewHolder 重置时调用 setter 触发了 8 次刷新。
// 改造:用 DisposableEffect 缓存而不是每次创建
@Composable
fun FeedScreen(viewModel: FeedViewModel) {
val state by viewModel.uiState.collectAsState()
AndroidView(
factory = { ctx ->
// 关键:factory 只在第一次创建,必须缓存
FeedRecyclerView(ctx).apply {
adapter = FeedAdapter()
layoutManager = LinearLayoutManager(ctx)
}
},
update = { view ->
view.submitList(state.items)
}
)
}
关键点:update 不再创建新 View,而是复用现有的 RecyclerView 实例。
七、改动落地:怎么让 PR 一周合进去
性能优化的 PR 没人愿意合(破坏稳定)。我们用三层防御:
影子测试:同一份代码 A/B 上线,50% 流量走新逻辑,50% 走老逻辑;线上对比 P99。
灰度开关:在 Config 里用远端开关控制是否启用,周一开 5%、周三 20%、周五 100%。
回滚预案:任何 PR 必须有"关闭开关后行为等于老逻辑"的代码对照表。
八、剩下的 580ms
三个月压到 220ms 后,剩下的 580ms 都是"结构性"问题——方案性太大、影响面广,需要后续迭代:
整体从 MVC 迁到 MVI(涉及 50+ Activity);
把首页首屏的 7 个远程接口合并为 1 个 GraphQL;
自建二进制协议替换 JSON。
这些后面再说。一次性能优化的成熟标志是:你敢在优化结束后停下,剩下的留给下一次。
九、复盘要点
小结
性能优化本质是"把看不见的慢,变成看得见的指标,再把指标压下去"。流程化、可视化、可灰度,三件事缺一不可。下一篇文章我们聊聊 Android 工程师怎么从"做项目"走到"做开源",把影响力做出 App 外。
参考阅读
《Systems Performance》— Brendan Gregg
Android Vitals 与 Baseline Profiles 实践
大厂 APM 开源项目 Matrix / ARG の