2026 年还在靠「感觉」调性能?Android Profiler 这样用才对

写给已经会用 Profiler、但还没把它用对的 Android 工程师

有一个判断我越来越确信:大多数 Android 性能问题,不是因为代码写得烂,而是因为根本没有在「正确的时机」用正确的工具看过它

更讽刺的是,Profiler 用得越多,越容易被它的信息量淹没------Call Chart 铺满屏幕、Heap 数字不断跳动、Network 时间线密密麻麻,最后反而不知道该盯哪里。

现在 AI 端侧推理在 Android 上越来越普遍(MediaPipe、ONNX Runtime、TFLite......),这批新场景对内存和 CPU 的压力模式跟传统业务逻辑完全不同,Profiler 的使用方式也得跟着变。这篇文章从这个背景出发,讲的不是"按哪个按钮",而是每个 Profiler 模块在什么场景下能给你真正有用的信息

一、CPU Profiler:你选的录制模式可能让结论完全失真

四种模式,用途天壤之别

这是我见过最多的误用:用 Trace Java Methods 分析"哪段代码最慢"。插桩模式会给每个方法入口/出口都加探针,应用会慢 2-10 倍,耗时数字完全失真,只能用来看调用关系,不能看时长

四种模式的正确使用场景:

Sample Java Methods:想知道「哪个方法总体最耗 CPU」→ 用这个。按固定间隔采样,开销小,适合长时间录制。盲区:执行时间极短的方法可能采不到。

Trace Java Methods:想知道「这段代码到底调了哪些方法」→ 用这个。看调用链,不看耗时。

Sample C/C++ Functions:Native 层(JNI、MediaPipe、ONNX Runtime 等)的 CPU 热点 → 用这个,底层用 simpleperf。

Trace System Calls :UI 卡顿、ANR、线程锁竞争 → 必须用这个,等价于 systrace,能看帧时间线、Binder 跨进程、线程调度。

📌 ⚠️ 端侧 AI 推理场景特别注意:TFLite/ONNX 推理通常在 Native 层执行,Java Sample 根本看不到瓶颈在哪。这类场景必须用 Sample C/C++ Functions 或者 Trace System Calls,才能看到 NNAPI 委托、GPU 执行、内存搬运的真实开销。

Flame Chart 比 Call Chart 更有用

Call Chart 横轴是时间,竖轴是调用深度,信息量大但容易迷失。切换到 Flame Chart(火焰图),相同调用栈会被聚合,横向越宽的方法说明它在整个录制时间里占 CPU 比例越高------直接找最宽的那条,那就是优化目标。

Compose 项目还有一个额外工具:Composition Tracing 。在 Recomposition 时自动在 System Trace 里打标记,能精确看到哪个 Composable 被不必要地重组、每次重组耗时多少。比手动加 Trace.beginSection 省力得多:

复制代码
// build.gradle
debugImplementation("androidx.compose.runtime:runtime-tracing:1.0.0")
debugImplementation("androidx.tracing:tracing-perfetto:1.0.0")

// 然后用 Trace System Calls 模式录制,
// System Trace 里会自动出现 Compose 的 recomposition 事件

二、Memory Profiler:真正的泄漏藏在你意想不到的地方

内存曲线的正确读法:看「能不能降」

Memory Profiler 时间线上,关键不是内存有多高,而是手动 GC 之后内存能不能回落。正常应用:触发 GC → 内存明显下降 → 继续操作 → 再次上涨 → 再次 GC 后能回落,这是健康的锯齿形。如果 GC 后内存只降一点点,之后持续爬升,就是泄漏信号。

定位步骤:进入页面 → 操作 → 退出 → 点垃圾桶手动 GC → 观察内存。如果退出页面后 GC 内存没有回到进入前水位,Heap Dump 看 Activity/Fragment 实例数。

三种你最可能忽略的泄漏模式

模式一:ViewModel 里持有 View 引用(Compose 时代的新坑)

复制代码
// ❌ ViewModel 生命周期比 Activity 长,持有 View/Context 必泄漏
class UserViewModel : ViewModel() {
    var recyclerView: RecyclerView? = null  // 危险!
    var context: Context? = null         // 危险!
}

// ✅ ViewModel 只持有数据和状态,View 层观察 StateFlow/LiveData
class UserViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(UserUiState())
    val uiState: StateFlow = _uiState.asStateFlow()
}

模式二:协程泄漏(GlobalScope 或未绑定生命周期)

复制代码
// ❌ GlobalScope 的协程生命周期跟 Activity 无关,Activity 销毁后协程仍在跑
override fun onCreate(...) {
    GlobalScope.launch {
        loadData()  // Activity 死了,这个还在跑,还可能回调更新已销毁的 View
    }
}

// ✅ 用 lifecycleScope,随 Activity 生命周期自动取消
override fun onCreate(...) {
    lifecycleScope.launch {
        loadData()
    }
}

// ✅ Compose 里用 LaunchedEffect,随 Composable 退出自动取消
LaunchedEffect(userId) {
    viewModel.loadUser(userId)
}

模式三:端侧 AI 推理的 Tensor/Buffer 未释放

复制代码
// ❌ TFLite Interpreter 如果不 close(),Native 内存不会被 Java GC 回收
val interpreter = Interpreter(modelBuffer)
runInference(interpreter)
// 忘了 interpreter.close() → Native 堆持续增长,Java Heap 看起来正常,实际 OOM

// ✅ 用 use() 确保释放,或者在 ViewModel.onCleared() 里 close
Interpreter(modelBuffer).use { interpreter ->
    runInference(interpreter)
}  // 自动调用 close()

📌 ⚠️ Native 内存泄漏在 Memory Profiler 里看 Java Heap 发现不了。需要看 Others 分类(native heap),或者用 Android Studio 的 Native Memory Profiler(需要 API 29+)。端侧推理项目强烈建议开启。

三、Network Profiler:知道它能看什么,更要知道它看不了什么

Network Profiler 有一个很多人不知道的硬限制:它只能抓 HttpURLConnection 和基于它的封装(OkHttp、Retrofit),gRPC(HTTP/2 + Protobuf 二进制)、WebSocket、自定义 Socket 通信全部要么显示不完整,要么 Body 是乱码。

遇到这些情况,更可靠的方案:

OkHttp 项目 :加 HttpLoggingInterceptor(Level.BODY),Logcat 里看完整请求响应

gRPC 项目 :加 gRPC 的 ClientInterceptor,在反序列化之后打印 Protobuf 内容

想抓完整流量(含 HTTPS 解密):用 Charles Proxy 或 mitmproxy,配合系统证书或 Network Security Config

端侧模型下载场景:模型文件通常几十 MB 到几百 MB,用 Network Profiler 看传输时间线,结合 Energy Profiler 看大文件下载对电量的影响

四、Energy Profiler:后台耗电的真凶往往不是你以为的那个

Wake Lock 泄漏:沉默的电量杀手

Energy Profiler 里,重点不是看能耗估算数字(那只是估算),而是看 Wake Lock 持有时长。正常情况下 Wake Lock 是秒级甚至毫秒级的。如果看到一个 Wake Lock 持续了几分钟,一定是代码有问题。

复制代码
// ✅ 两个好习惯同时做:try-finally 保证释放 + 设置超时兜底
val wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager)
    .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::SyncTask")

wakeLock.acquire(10 * 60 * 1000L)  // 10分钟超时兜底,代码 bug 也不会永久持锁
try {
    doSyncWork()
} finally {
    if (wakeLock.isHeld) wakeLock.release()
}

端侧推理的电量问题:比你想象中更复杂

在 Android 上跑 AI 推理,GPU 委托(NNAPI / GPU Delegate)的功耗比 CPU 低,但频繁唤醒 GPU 的开销可能比持续占用 CPU 还高。Energy Profiler 里看 CPU 活动曲线时,如果发现间歇性的高峰(每次推理结束后功耗瞬间飙升),通常是 GPU 唤醒开销。

优化思路:批量推理(把多帧合并成一个 batch),减少唤醒次数,或者用 WorkManager 把推理任务集中到充电或低负载时段。

五、实战:一个完整的 ANR 定位流程

ANR 的定位有固定套路,从 traces.txt 到 System Trace 再到代码,每步都有具体动作:

第一步:读 traces.txt,找主线程状态

复制代码
adb pull /data/anr/traces.txt

# 主线程状态关键词:
# Blocked          → 等锁,找 waiting to lock held by thread XX
# Sleeping/Waiting → 主线程调了 sleep() 或等 I/O,直接违规
# Runnable         → 主线程在跑 CPU 密集计算,找最宽的调用帧

第二步:System Trace 复现,定位锁的持有方

用 CPU Profiler 的 Trace System Calls 模式录制,复现 ANR 操作。在 trace 里找:

Waiting to lock 段落的持续时间(超过 5 秒触发 ANR)

• 持锁线程的调用栈(通常是把数据库查询、文件 I/O 放在了有锁的代码路径里)

• 主线程 Choreographer#doFrame 的间隔(正常 <16ms,卡顿时会拉长)

第三步:开发阶段用 StrictMode 提前拦截

复制代码
if (BuildConfig.DEBUG) {
    StrictMode.setThreadPolicy(
        StrictMode.ThreadPolicy.Builder()
            .detectAll()          // 主线程 I/O、网络、慢调用全检测
            .penaltyLog()
            .penaltyFlashScreen() // 屏幕闪烁,开发时很直观
            .build()
    )
    StrictMode.setVmPolicy(
        StrictMode.VmPolicy.Builder()
            .detectActivityLeaks()
            .detectLeakedClosableObjects()  // 能捕获 TFLite Interpreter 未 close
            .penaltyLog()
            .build()
    )
}

StrictMode 的 detectLeakedClosableObjects() 对端侧 AI 推理项目特别有用------TFLite Interpreter、ONNX Session 都实现了 Closeable 接口,忘了 close 会被直接捕获。

六、Profiler 之后:你真正需要的是「可观测性」

说一个不讨好的判断:Profiler 是开发工具,不是运维工具。它只能在连了 USB 的设备上工作,而真正的性能问题往往在用户的低端机、弱网环境、特定 ROM 上爆发。

这是为什么光靠 Profiler 不够,需要线上可观测性体系:

Android Vitals(Google Play Console):真实用户的 ANR 率、崩溃率、冻结帧率,P75/P95 分位数分布

Macrobenchmark + Baseline Profiles:自动化性能回归测试 + 预编译关键路径,冷启动时间可降 20-40%

自研 APM 埋点:关键业务路径耗时、端侧推理延迟分位数,比通用指标更有业务价值

Native 崩溃捕获(Crashlytics / Bugly):AI 推理 Native 层崩溃的堆栈,Java 层看不到

Profiler 帮你理解「为什么」,线上监控告诉你「有多严重」「影响了多少用户」。两件事都不能省,只是用的时机不同。

如果现在只能做一件事提升性能工程能力,我的建议是:把 Baseline Profiles 接进 CI。它不需要你找到具体的性能 bug,直接对整个应用的启动路径做预编译,效果立竿见影,而且是 Google 在 2023-2026 年持续推进的方向,值得投入。

如有问题欢迎留言交流。

相关推荐
咏&志2 小时前
目标检测Faster-RCNN论文简读
人工智能·目标检测·计算机视觉
研究点啥好呢2 小时前
3月28日Github热榜推荐 | 你还没有为AI接一个数据库吗
数据库·人工智能·驱动开发·github
财迅通Ai2 小时前
探路者旗下通途半导体推出人工智能全栈压缩技术 撬动万亿级端侧人工智能市场
人工智能·探路者
cxr8282 小时前
OpenClaw Node 行业实践案例
人工智能·ai智能体·openclaw
不一样的故事1262 小时前
测试的核心本质是风险管控
大数据·网络·人工智能·安全
草莓熊Lotso2 小时前
MySQL 多表连接查询实战:内连接 + 外连接
android·运维·数据库·c++·mysql
禁默2 小时前
从零吃透大语言模型 LLM,AI 应用开发必懂底层逻辑
人工智能·机器学习·语言模型·大模型
weixin199701080162 小时前
《苏宁商品详情页前端性能优化实战》
前端·性能优化
空空潍2 小时前
Spring AI 实战系列(二):ChatClient封装,告别大模型开发样板代码
java·人工智能·spring