APM
APM 的全称是 Application Performance Management,中文译为 应用性能管理。它是一套监控、分析、优化应用全生命周期性能与稳定性的工具 / 方案体系,核心目标是提前发现性能瓶颈、定位线上故障、提升用户体验。
在 Android/iOS 等移动应用领域,APM 更是保障 App 稳定性的核心基础设施,和你之前关注的 OOM、ANR、Crash 等问题直接相关。
移动 APM 的核心能力
移动 App 的 APM 主要聚焦 「性能监控」「异常监控」「用户行为追踪」 三大模块,覆盖从开发到线上的全流程:
| 模块 | 核心监控内容 |
|---|---|
| 性能指标监控 | 启动耗时(冷 / 温 / 热启动)、页面加载耗时、帧率(FPS)、卡顿次数、内存占用、CPU 使用率、网络请求耗时 / 成功率 |
| 异常监控 | Crash 崩溃(Java/Native)、ANR、OOM、自定义异常、网络错误(超时 / 4xx/5xx) |
| 用户行为与链路 | 用户操作路径、页面跳转、点击事件、接口调用链路、设备信息(机型 / 系统版本 / 网络类型) |
| 资源与功耗监控 | 流量消耗、电量消耗、磁盘占用、后台唤醒次数 |
移动 APM 的核心价值
- 提前预警:在用户反馈前,主动发现性能劣化(如启动耗时突然增加 20%)和潜在故障(如某机型 Crash 率飙升);
- 快速定位:线上出现 ANR/Crash 时,提供完整上下文(如用户操作步骤、设备信息、堆栈日志、内存状态),避免 “无法复现” 的难题;
- 优化指导:通过性能数据对比(如优化前后启动耗时变化),验证优化效果;通过卡顿 / 慢接口排行,明确优化优先级;
- 版本评估:每次发版后,自动生成性能 / 稳定性报告,判断新版本是否达标(如 Crash 率是否低于 0.1%)。
移动 APM 的实现方案(开源 vs 商业化)
1. 开源 APM 方案(适合自研 / 定制化)
适合有技术团队、需要深度定制的企业,可基于开源框架二次开发:
| 开源方案 | 厂商 / 社区 | 核心优势 |
|---|---|---|
| Matrix | 腾讯 | 专注 Android,覆盖启动、卡顿、内存、网络、Crash 全维度,文档完善,接入简单 |
| ArgusAPM | 阿里 | 支持 Android/iOS,监控维度全面,内置告警机制 |
| WatchDog | 社区 | 轻量级,主打 ANR 监控和卡顿检测,适合小型项目 |
| KOOM | 阿里 | 专注内存问题(OOM / 内存泄漏),线上堆快照分析能力强 |
2. 商业化 APM 方案(适合快速接入 / 全平台)
适合中小型团队,无需自建服务,开箱即用:
| 商业化方案 | 厂商 | 核心优势 |
|---|---|---|
| Firebase Performance | 免费,与 Firebase Crashlytics 无缝集成,支持 Android/iOS/Web | |
| Bugly | 腾讯 | 国内主流,Crash / 性能监控一体,支持多平台,接入成本低 |
| 听云 APM | 听云 | 全链路监控(从 App 到服务端),适合前后端联动排查问题 |
| New Relic | 外企 | 全球化部署,支持跨平台,企业级告警和分析能力强 |
移动 APM 核心指标与阈值(Android 参考)
不同 App 类型(如电商 / 工具 / 游戏)阈值略有差异,以下是通用参考标准:
| 指标 | 健康阈值 | 需优化阈值 |
|---|---|---|
| 冷启动耗时(Android) | ≤ 3 秒 | > 5 秒 |
| 帧率(FPS) | 稳定 55-60 FPS | 频繁低于 45 FPS(卡顿) |
| Crash 率 | ≤ 0.1% | > 0.5% |
| ANR 率 | ≤ 0.05% | > 0.2% |
| 网络请求成功率 | ≥ 99% | < 95% |
| 内存泄漏率 | 0(关键页面) | 存在 Activity/Fragment 泄漏 |
APM 与相关工具的关系
你之前关注的 Perfetto、LeakCanary、BlockCanary 等工具,是 APM 的 “局部组件”,而 APM 是**“整合体系”**:
- LeakCanary/BlockCanary:负责开发阶段的内存泄漏 / 卡顿检测,属于 APM 的 “线下工具”;
- Perfetto:负责性能瓶颈深度分析,属于 APM 的 “问题定位工具”;
- APM 平台:整合上述工具能力,实现线上全量监控、数据上报、告警、可视化分析,形成 “监控 - 分析 - 优化” 的闭环。
APM 核心落地指南:性能监控 / 异常监控实现 + 实战案例
以下从「性能监控实现」「异常监控实操」「项目落地案例」三个维度,拆解 APM 在 Android 项目中的具体落地方式,包含代码模板、核心原理、大厂真实案例,可直接复用。
APM 如何实现性能监控?(核心:埋点采集 + 数据上报 + 分析)
性能监控的核心逻辑是:在关键节点埋点采集指标 → 标准化上报 → 后台聚合分析 → 可视化展示 / 告警,覆盖启动、卡顿、内存、网络、帧率等核心维度。
1. 核心性能指标采集实现(附代码模板)
(1)启动耗时监控(冷 / 温 / 热启动)
原理:采集「进程创建→Application.onCreate→首帧渲染」全链路时间戳,计算各阶段耗时。
代码实现:
// 1. 自定义 Application,记录进程启动时间(冷启动起点) class MyApplication : Application() { companion object { var processStartTime = 0L var appCreateTime = 0L var firstFrameTime = 0L } override fun onCreate() { super.onCreate() appCreateTime = System.currentTimeMillis() // 计算进程启动→Application.onCreate 耗时 val processToApp = appCreateTime - processStartTime // 2. 监听首帧渲染(冷启动终点) val handler = Handler(Looper.getMainLooper()) handler.postAtFrontOfQueue { firstFrameTime = System.currentTimeMillis() // 冷启动总耗时 = 首帧时间 - 进程启动时间 val coldLaunchTime = firstFrameTime - processStartTime // 上报启动耗时 ApmReporter.report("cold_launch_time", coldLaunchTime) ApmReporter.report("process_to_app_time", processToApp) } } } // 2. 在 AndroidManifest 中配置 ContentProvider,捕获进程启动时间(更早的起点) class LaunchProvider : ContentProvider() { override fun onCreate(): Boolean { MyApplication.processStartTime = System.currentTimeMillis() return true } // 省略其他空实现 } // 3. AndroidManifest 注册 Provider(进程启动时优先初始化) <provider android:name=".LaunchProvider" android:authorities="${applicationId}.launch.provider" android:exported="false"/>
(2)卡顿监控(帧率 / FPS + 主线程阻塞)
原理:
- 帧率:监听
Choreographer帧回调,计算 1 秒内帧数量(FPS),<45 判定为卡顿; - 主线程阻塞:监控
Looper消息执行时间,>200ms 判定为卡顿。
- 帧率:监听
代码实现:
// 1. 帧率监控 class FPSMonitor { private var frameCount = 0 private var startTime = 0L fun startMonitor() { // 监听帧回调 Choreographer.getInstance().postFrameCallback(object : Choreographer.FrameCallback { override fun doFrame(frameTimeNanos: Long) { if (startTime == 0L) { startTime = System.currentTimeMillis() } frameCount++ // 每1秒计算一次FPS val currentTime = System.currentTimeMillis() if (currentTime - startTime >= 1000) { val fps = frameCount / ((currentTime - startTime) / 1000.0) // 上报FPS,标记卡顿(FPS<45) ApmReporter.report("fps", fps) ApmReporter.report("is_jank", fps < 45) // 重置计数 frameCount = 0 startTime = currentTime } // 继续监听下一帧 Choreographer.getInstance().postFrameCallback(this) } }) } } // 2. 主线程阻塞监控(Looper 日志拦截) class BlockMonitor { fun init() { // 替换主线程 Looper 的日志打印,监控消息执行时间 Looper.getMainLooper().setMessageLogging { msg -> if (msg.startsWith(">>>>> Dispatching to")) { // 消息开始执行,记录时间 BlockTrace.beginTrace(msg) } else if (msg.startsWith("<<<<< Finished to")) { // 消息执行结束,计算耗时 val costTime = BlockTrace.endTrace(msg) // 耗时>200ms,判定为卡顿,上报堆栈 if (costTime > 200) { val stackTrace = Looper.getMainLooper().thread.stackTrace ApmReporter.report("main_thread_block", mapOf( "cost_time" to costTime, "stack" to stackTrace.contentToString(), "msg" to msg )) } } } } }
(3)内存监控(占用 / GC / 泄漏)
原理:通过
ActivityManager获取内存状态,监听GC事件,结合 KOOM 采集堆快照。代码实现:
class MemoryMonitor { private val activityManager by lazy { getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager } // 定时采集内存占用 fun startMemoryMonitor() { val timer = Timer() timer.scheduleAtFixedRate(object : TimerTask() { override fun run() { val memoryInfo = ActivityManager.MemoryInfo() activityManager.getMemoryInfo(memoryInfo) // 可用内存 val availMem = memoryInfo.availMem / (1024 * 1024) // 应用已用内存 val myMem = activityManager.getProcessMemoryInfo(intArrayOf(Process.myPid()))[0] val usedMem = myMem.totalPss / 1024 // MB // 上报内存指标 ApmReporter.report("avail_mem", availMem) ApmReporter.report("app_used_mem", usedMem) ApmReporter.report("is_low_memory", memoryInfo.lowMemory) } }, 0, 5000) // 每5秒采集一次 } // 监听GC事件(API 26+) fun listenGC() { val gcWatcher = GcWatcher { // GC 触发时上报 ApmReporter.report("gc_trigger", mapOf( "gc_type" to it.gcType, "gc_duration" to it.duration // GC耗时 )) } gcWatcher.startWatch() } }
(4)网络请求监控
原理:通过 OkHttp 拦截器采集请求耗时、状态码、请求大小等指标。
代码实现:
class NetworkInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { // 记录请求开始时间 val startTime = System.nanoTime() val request = chain.request() try { val response = chain.proceed(request) // 计算请求耗时(毫秒) val costTime = (System.nanoTime() - startTime) / 1000000.0 // 采集核心指标 val metrics = mapOf( "url" to request.url.toString(), "method" to request.method, "cost_time" to costTime, "status_code" to response.code, "request_size" to request.body?.contentLength() ?: 0, "response_size" to response.body?.contentLength() ?: 0, "is_success" to (response.code in 200..299) ) // 上报网络指标 ApmReporter.report("network_request", metrics) return response } catch (e: Exception) { // 上报网络异常 ApmReporter.report("network_error", mapOf( "url" to request.url.toString(), "error_type" to e.javaClass.simpleName, "error_msg" to e.message )) throw e } } } // OkHttp 配置拦截器 val okHttpClient = OkHttpClient.Builder() .addInterceptor(NetworkInterceptor()) .build()
2. 数据上报与分析(核心流程)
(1)数据标准化上报
采集的指标需标准化(如时间戳、设备信息、用户 ID、应用版本),通过 HTTP/HTTPS 批量上报(避免频繁请求):
object ApmReporter { private val metricsQueue = ConcurrentLinkedQueue<Metric>() private val executor = Executors.newSingleThreadExecutor() // 初始化:定时批量上报(每30秒) fun init(context: Context) { executor.scheduleAtFixedRate({ if (metricsQueue.isNotEmpty()) { val metrics = mutableListOf<Metric>() metricsQueue.drainTo(metrics) // 拼接设备信息 val deviceInfo = getDeviceInfo(context) val reportData = metrics.map { it + deviceInfo } // 上报到后台(示例:Retrofit) ApiService.uploadApmData(reportData) } }, 0, 30, TimeUnit.SECONDS) } // 上报单个指标 fun report(metricName: String, value: Any) { val metric = Metric( "metric_name" to metricName, "value" to value, "timestamp" to System.currentTimeMillis(), "app_version" to BuildConfig.VERSION_NAME ) metricsQueue.offer(metric) } // 获取设备基础信息 private fun getDeviceInfo(context: Context): Map<String, String> { return mapOf( "device_model" to Build.MODEL, "os_version" to Build.VERSION.RELEASE, "app_version" to BuildConfig.VERSION_NAME, "network_type" to getNetworkType(context), "user_id" to SPUtils.getUserId() ) } }
(2)后台分析与可视化
- 后台接收数据后,按「指标类型 + 维度」聚合:
- 趋势分析:启动耗时 / 帧率按版本 / 机型 / 时间段统计;
- 异常筛选:筛选卡顿 > 200ms、内存占用 > 80% 的异常数据;
- 告警配置:设置阈值(如启动耗时 > 5 秒、Crash 率 > 0.5%),触发钉钉 / 邮件告警。
如何使用 APM 进行异常监控?(核心:全量捕获 + 上下文还原 + 快速定位)
异常监控覆盖 Crash(Java/Native)、ANR、OOM、自定义异常,核心是100% 捕获异常 + 还原现场上下文 + 快速定位根因。
1. Crash 监控(Java/Native)
(1)Java Crash 捕获
原理:替换
Thread.UncaughtExceptionHandler,捕获未处理异常,收集上下文后上报。代码实现:
class CrashHandler : Thread.UncaughtExceptionHandler { private val defaultHandler = Thread.getDefaultUncaughtExceptionHandler() fun init() { // 设置全局异常处理器 Thread.setDefaultUncaughtExceptionHandler(this) } override fun uncaughtException(t: Thread, e: Throwable) { // 1. 收集异常上下文 val crashInfo = mutableMapOf<String, Any>().apply { put("thread_name", t.name) put("exception_type", e.javaClass.simpleName) put("exception_msg", e.message) put("stack_trace", getStackTraceString(e)) put("device_info", getDeviceInfo()) put("user_behavior", UserBehaviorTracker.getLast10Actions()) // 用户操作路径 put("memory_info", getMemoryInfo()) // 内存状态 } // 2. 上报Crash信息 ApmReporter.report("java_crash", crashInfo) // 3. 交给默认处理器(避免应用卡死) defaultHandler?.uncaughtException(t, e) } // 拼接堆栈信息 private fun getStackTraceString(e: Throwable): String { val sw = StringWriter() val pw = PrintWriter(sw) e.printStackTrace(pw) return sw.toString() } }
(2)Native Crash 捕获
原理:通过
Breakpad/LitePal等库捕获 Native 层崩溃(如 C/C++ 空指针),生成 minidump 文件,解析后上报。集成方式(以腾讯 Bugly 为例):
// 集成 Bugly Native 崩溃监控 dependencies { implementation 'com.tencent.bugly:crashreport:latest.release' implementation 'com.tencent.bugly:nativecrashreport:latest.release' } // 初始化 Bugly.init(context, "你的APP_ID", BuildConfig.DEBUG)
2. ANR 监控
原理:通过
FileObserver监听/data/anr/traces.txt文件变化,ANR 触发时读取文件内容,结合用户行为上报。代码实现:
class AnrMonitor { fun startMonitor() { // 监听ANR日志文件 val anrFile = File("/data/anr/traces.txt") val fileObserver = object : FileObserver(anrFile.absolutePath, FileObserver.MODIFY) { override fun onEvent(event: Int, path: String?) { if (event == FileObserver.MODIFY && path == "traces.txt") { // 读取ANR日志 val anrLog = anrFile.readText() // 收集上下文 val anrInfo = mapOf( "anr_log" to anrLog, "user_behavior" to UserBehaviorTracker.getLast10Actions(), "timestamp" to System.currentTimeMillis() ) // 上报ANR ApmReporter.report("anr", anrInfo) } } } fileObserver.startWatching() } }
3. OOM 监控
原理:监听
OutOfMemoryError异常,结合 KOOM 采集堆快照(hprof 文件),解析泄漏对象后上报。集成方式
(以 KOOM 为例):
// 集成 KOOM dependencies { implementation 'com.kuaishou.koom:koom-native-leak:2.2.0' } // 初始化 KOOM class MyApplication : Application() { override fun onCreate() { super.onCreate() KoomConfig.Builder() .setUploader(object : KoomLogUploader { override fun upload(file: File) { // 上传堆快照文件到后台 uploadFileToServer(file) } }) .build() .init() } }
4. 异常监控闭环
| 阶段 | 核心动作 |
|---|---|
| 异常捕获 | 全量捕获 Java/Native Crash、ANR、OOM、自定义异常,收集完整上下文 |
| 数据上报 | 异常发生时优先上报(非批量),确保不丢失;弱网下缓存到本地,联网后重传 |
| 根因分析 | 后台解析堆栈、还原混淆(mapping.txt)、关联用户行为 / 设备信息 |
| 告警与修复 | 按异常等级告警(P0:Crash 率 > 1% 立即告警),修复后灰度验证,回归监控数据 |
APM 在实际项目中的应用案例(大厂实战)
案例 1:电商 App 启动耗时优化(某头部电商)
- 问题:冷启动耗时 > 6 秒,用户流失率高;
- APM 落地:
- 采集启动各阶段耗时:进程启动(1.2s)→ Application 初始化(2.5s)→ 首帧渲染(2.3s);
- 定位瓶颈:Application 中 3 个第三方 SDK(埋点、推送、统计)同步初始化耗时 1.8s;
- 优化方案:将非必需 SDK 改为子线程懒加载,首屏布局改为 ViewStub 懒加载;
- 效果:冷启动耗时从 6.2s 降至 2.8s,用户留存率提升 15%。
案例 2:短视频 App 卡顿优化(某短视频平台)
- 问题:滑动视频时频繁卡顿(FPS<40),用户投诉多;
- APM 落地:
- 采集卡顿数据:主线程阻塞 > 200ms 的场景中,70% 是视频帧解码 + 图片加载阻塞;
- 定位根因:视频解码在主线程执行,图片加载未做内存缓存;
- 优化方案:视频解码移到子线程,图片加载用 LruCache 缓存,开启硬件解码;
- 效果:卡顿率从 8.2% 降至 1.5%,播放流畅度提升 90%。
案例 3:出行 App Crash 率优化(某打车平台)
- 问题:部分机型 Crash 率 > 1%,主要是 OOM 和空指针;
- APM 落地:
- 全量捕获 Crash:OOM 占比 60%,空指针占比 25%;
- 分析 OOM 根因:地图 SDK 内存泄漏 + Bitmap 未压缩;
- 优化方案:修复地图 SDK 引用泄漏,Bitmap 按屏幕尺寸压缩,内存不足时清空缓存;
- 效果:Crash 率从 1.2% 降至 0.08%,线上投诉减少 90%。
案例 4:金融 App ANR 优化(某银行 App)
- 问题:支付页面偶现 ANR,无法复现;
- APM 落地:
- 监听 ANR 日志:捕获到 ANR 时主线程阻塞在「加密算法计算」,耗时 8 秒;
- 定位根因:支付密码加密在主线程执行,低配机计算耗时久;
- 优化方案:加密算法移到子线程,增加加载动画,避免主线程阻塞;
- 效果:ANR 率从 0.3% 降至 0.02%,支付成功率提升 5%。
APM 落地关键注意事项
- 采集开销控制:
- 性能指标采集频率不宜过高(如内存每 5 秒一次,帧率每 1 秒一次);
- 上报采用批量 + 压缩,避免占用过多流量 / 电量;
- 隐私合规:
- 不采集用户敏感信息(如手机号、身份证),上报数据脱敏;
- 遵循 GDPR / 工信部规范,提供隐私政策说明;
- 灰度验证:
- 新功能 / 优化方案先灰度发布,通过 APM 对比性能数据,验证效果后全量;
- 告警分级:
- P0(紧急):Crash 率 > 1%、ANR 率 > 0.5%;
- P1(高优先级):启动耗时 > 5 秒、卡顿率 > 5%;
- P2(普通):内存占用略高、网络请求成功率 < 99%。
APM 集成代码
以下是 启动监控、卡顿监控、ANR 上报、Crash 上报 的完整 APM 集成代码(Android/Kotlin),包含核心采集逻辑、数据上报、工具类封装,可直接复制到项目中使用,适配 Android 9+ 全版本。
这套代码实现了 启动 / 卡顿 / ANR/Crash 四大核心监控能力,纯原生实现无第三方依赖,适合中小型项目快速集成。后续也可以集成:
- 集成 Native Crash 监控;
- 对接第三方 APM 平台(如 Bugly/Matrix);
- 增加内存 / OOM 监控;
核心依赖(build.gradle)
android {
compileSdk 34
defaultConfig {
minSdk 21
// 混淆配置(Crash上报需保留堆栈)
buildConfigField "boolean", "DEBUG", "${BuildConfig.DEBUG}"
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
// 保留混淆映射文件(用于解析Crash堆栈)
mappingFileUploadEnabled true
}
}
}
dependencies {
// 核心依赖(无第三方SDK,纯原生实现)
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'com.squareup.okhttp3:okhttp:4.12.0' // 用于数据上报
implementation 'com.google.code.gson:gson:2.10.1' // 数据序列化
}
APM 核心配置类(APMConfig.kt)
统一管理监控开关、上报地址、采集频率等配置:
object APMConfig {
// 开关配置
const val ENABLE_LAUNCH_MONITOR = true // 启动监控
const val ENABLE_JANK_MONITOR = true // 卡顿监控
const val ENABLE_ANR_MONITOR = true // ANR监控
const val ENABLE_CRASH_MONITOR = true // Crash监控
// 上报配置
const val REPORT_URL = "https://your-api.com/apm/report" // 替换为你的后台接口
const val BATCH_REPORT_INTERVAL = 30_000L // 批量上报间隔(30秒)
const val MAX_QUEUE_SIZE = 100 // 最大缓存队列长度
// 卡顿阈值(主线程消息执行>200ms判定为卡顿)
const val JANK_THRESHOLD_MS = 200L
// FPS阈值(<45判定为卡顿)
const val FPS_LOW_THRESHOLD = 45.0
// 设备信息(全局缓存)
val DEVICE_INFO by lazy {
mapOf(
"device_model" to android.os.Build.MODEL,
"os_version" to android.os.Build.VERSION.RELEASE,
"app_version" to BuildConfig.VERSION_NAME,
"app_version_code" to BuildConfig.VERSION_CODE.toString(),
"package_name" to BuildConfig.APPLICATION_ID,
"cpu_abi" to android.os.Build.CPU_ABI
)
}
}
通用工具类
1. 数据上报工具(APMReporter.kt)
负责批量缓存、异步上报,弱网下本地缓存:
import android.content.Context
import android.content.SharedPreferences
import com.google.gson.Gson
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Executors
object APMReporter {
private val gson = Gson()
private val executor = Executors.newSingleThreadExecutor()
private val metricsQueue = ConcurrentLinkedQueue<Map<String, Any>>()
private val okHttpClient = OkHttpClient.Builder().build()
private lateinit var sp: SharedPreferences
private val MEDIA_TYPE = "application/json; charset=utf-8".toMediaType()
// 初始化(在Application中调用)
fun init(context: Context) {
sp = context.getSharedPreferences("apm_cache", Context.MODE_PRIVATE)
// 加载本地缓存的未上报数据
val cacheData = sp.getString("apm_queue", null)
if (cacheData != null) {
val list = gson.fromJson(cacheData, Array<Map<String, Any>>::class.java).toList()
metricsQueue.addAll(list)
}
// 启动定时批量上报
executor.scheduleAtFixedRate({
batchReport()
}, 0, APMConfig.BATCH_REPORT_INTERVAL, java.util.concurrent.TimeUnit.MILLISECONDS)
}
// 上报单个指标
fun report(metricType: String, data: Map<String, Any>) {
if (metricsQueue.size >= APMConfig.MAX_QUEUE_SIZE) {
metricsQueue.poll() // 队列满了,移除最早的一条
}
// 拼接基础信息
val finalData = mutableMapOf<String, Any>().apply {
putAll(APMConfig.DEVICE_INFO)
put("metric_type", metricType)
put("timestamp", System.currentTimeMillis())
putAll(data)
}
metricsQueue.offer(finalData)
// 紧急指标(Crash/ANR)立即上报,不等待批量
if (metricType in listOf("crash", "anr")) {
executor.submit {
val singleData = listOf(finalData)
doReport(singleData)
}
}
}
// 批量上报
private fun batchReport() {
if (metricsQueue.isEmpty()) return
val dataList = mutableListOf<Map<String, Any>>()
metricsQueue.drainTo(dataList)
// 上报前缓存到本地,避免进程被杀丢失
sp.edit().putString("apm_queue", gson.toJson(metricsQueue)).apply()
// 异步上报
executor.submit {
val success = doReport(dataList)
if (success) {
// 上报成功,清空本地缓存
sp.edit().remove("apm_queue").apply()
} else {
// 上报失败,把数据放回队列
metricsQueue.addAll(dataList)
}
}
}
// 实际上报逻辑
private fun doReport(dataList: List<Map<String, Any>>): Boolean {
return try {
val json = gson.toJson(dataList)
val requestBody = json.toRequestBody(MEDIA_TYPE)
val request = Request.Builder()
.url(APMConfig.REPORT_URL)
.post(requestBody)
.build()
val response = okHttpClient.newCall(request).execute()
response.isSuccessful
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
2. 用户行为追踪工具(UserBehaviorTracker.kt)
用于 ANR/Crash 时还原用户操作路径:
kotlin
object UserBehaviorTracker {
private val behaviorList = LinkedList<String>()
private const val MAX_BEHAVIOR_COUNT = 10 // 最多保留10条
// 记录用户行为
fun track(behavior: String) {
if (behaviorList.size >= MAX_BEHAVIOR_COUNT) {
behaviorList.removeFirst()
}
behaviorList.add("${System.currentTimeMillis()}: $behavior")
}
// 获取最近行为(用于上报)
fun getLastBehaviors(): String {
return behaviorList.joinToString("\n")
}
// 清空行为记录
fun clear() {
behaviorList.clear()
}
}
核心监控实现
1. 启动监控(LaunchMonitor.kt)
捕获冷 / 温启动全链路耗时:
import android.content.ContentProvider
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.view.Window
class LaunchMonitor {
companion object {
var processStartTime = 0L // 进程启动时间(最早起点)
var appCreateTime = 0L // Application.onCreate 时间
var firstFrameTime = 0L // 首帧渲染时间
// 初始化(在Application中调用)
fun init(application: Context) {
if (!APMConfig.ENABLE_LAUNCH_MONITOR) return
// 记录Application.onCreate时间
appCreateTime = System.currentTimeMillis()
// 监听首帧渲染(冷启动终点)
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
firstFrameTime = System.currentTimeMillis()
reportLaunchData()
}
}
// 上报启动数据
private fun reportLaunchData() {
val coldLaunchTime = firstFrameTime - processStartTime // 冷启动总耗时
val appCreateDuration = appCreateTime - processStartTime // 进程→AppCreate耗时
val firstFrameDuration = firstFrameTime - appCreateTime // AppCreate→首帧耗时
val launchData = mapOf(
"launch_type" to "cold", // 可扩展区分温/热启动
"cold_launch_total" to coldLaunchTime,
"process_to_app_create" to appCreateDuration,
"app_create_to_first_frame" to firstFrameDuration,
"last_behavior" to UserBehaviorTracker.getLastBehaviors()
)
APMReporter.report("launch", launchData)
}
// 监听Activity首帧(可选,精准化页面启动耗时)
fun monitorActivityFirstFrame(window: Window, activityName: String) {
if (!APMConfig.ENABLE_LAUNCH_MONITOR) return
val activityStartTime = System.currentTimeMillis()
window.decorView.viewTreeObserver.addOnDrawListener(object : android.view.ViewTreeObserver.OnDrawListener {
override fun onDraw() {
window.decorView.viewTreeObserver.removeOnDrawListener(this)
val activityLaunchTime = System.currentTimeMillis() - activityStartTime
val data = mapOf(
"activity_name" to activityName,
"launch_time" to activityLaunchTime
)
APMReporter.report("activity_launch", data)
}
})
}
}
}
// 用于捕获进程启动时间的ContentProvider(进程启动时优先初始化)
class LaunchProvider : ContentProvider() {
override fun onCreate(): Boolean {
LaunchMonitor.processStartTime = System.currentTimeMillis()
return true
}
override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?): Cursor? = null
override fun getType(uri: Uri): String? = null
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = 0
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int = 0
}
2. 卡顿监控(JankMonitor.kt)
监控 FPS 和主线程阻塞:
kotlin
import android.os.Looper
import android.view.Choreographer
import java.lang.Exception
import java.lang.StringBuilder
class JankMonitor {
private var frameCount = 0
private var fpsStartTime = 0L
private val mainLooper = Looper.getMainLooper()
// 初始化(在Application中调用)
fun init() {
if (!APMConfig.ENABLE_JANK_MONITOR) return
// 监控FPS
startFPSMonitor()
// 监控主线程阻塞
startMainThreadBlockMonitor()
}
// 1. FPS监控(基于Choreographer)
private fun startFPSMonitor() {
val choreographer = Choreographer.getInstance()
choreographer.postFrameCallback(object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
if (fpsStartTime == 0L) {
fpsStartTime = System.currentTimeMillis()
}
frameCount++
val currentTime = System.currentTimeMillis()
if (currentTime - fpsStartTime >= 1000) {
// 计算FPS
val fps = frameCount / ((currentTime - fpsStartTime) / 1000.0)
val isJank = fps < APMConfig.FPS_LOW_THRESHOLD
// 上报FPS数据
val fpsData = mapOf(
"fps" to fps,
"is_jank" to isJank,
"timestamp" to System.currentTimeMillis()
)
APMReporter.report("fps", fpsData)
// 重置计数
frameCount = 0
fpsStartTime = currentTime
}
// 继续监听下一帧
choreographer.postFrameCallback(this)
}
})
}
// 2. 主线程阻塞监控(基于Looper消息拦截)
private fun startMainThreadBlockMonitor() {
val originalLogging = mainLooper.messageLogging
mainLooper.setMessageLogging { msg ->
// 拦截Looper消息日志,捕获消息开始/结束
if (msg.startsWith(">>>>> Dispatching to")) {
// 消息开始执行
BlockTrace.beginTrace(msg)
} else if (msg.startsWith("<<<<< Finished to")) {
// 消息执行结束,计算耗时
val costTime = BlockTrace.endTrace(msg)
if (costTime > APMConfig.JANK_THRESHOLD_MS) {
// 卡顿,上报堆栈和耗时
val stackTrace = getMainThreadStackTrace()
val blockData = mapOf(
"cost_time" to costTime,
"stack_trace" to stackTrace,
"message" to msg,
"last_behavior" to UserBehaviorTracker.getLastBehaviors()
)
APMReporter.report("main_thread_block", blockData)
}
}
// 保留原有日志逻辑
originalLogging?.let { it.println(msg) }
}
}
// 获取主线程堆栈
private fun getMainThreadStackTrace(): String {
return try {
val stackTrace = mainLooper.thread.stackTrace
val sb = StringBuilder()
stackTrace.forEach {
sb.append(it.toString()).append("\n")
}
sb.toString()
} catch (e: Exception) {
e.stackTraceToString()
}
}
}
// 辅助类:记录消息执行时间
object BlockTrace {
private val traceMap = mutableMapOf<String, Long>()
fun beginTrace(msg: String) {
val key = msg.substringAfter("Dispatching to ").substringBefore(":")
traceMap[key] = System.currentTimeMillis()
}
fun endTrace(msg: String): Long {
val key = msg.substringAfter("Finished to ").substringBefore(":")
val startTime = traceMap.remove(key) ?: return 0
return System.currentTimeMillis() - startTime
}
}
3. ANR 监控与上报(AnrMonitor.kt)
监听 /data/anr/traces.txt 捕获 ANR:
kotlin
import android.os.FileObserver
import java.io.File
import java.lang.Exception
class AnrMonitor {
// 初始化(在Application中调用)
fun init() {
if (!APMConfig.ENABLE_ANR_MONITOR) return
startAnrFileMonitor()
}
// 监听ANR日志文件
private fun startAnrFileMonitor() {
val anrFile = File("/data/anr/traces.txt")
if (!anrFile.exists()) return
val fileObserver = object : FileObserver(anrFile.absolutePath, FileObserver.MODIFY or FileObserver.CREATE) {
override fun onEvent(event: Int, path: String?) {
if (event == FileObserver.MODIFY && path == "traces.txt") {
// ANR发生,读取日志并上报
executor.submit {
val anrLog = readAnrLog(anrFile)
val anrData = mapOf(
"anr_log" to anrLog,
"last_behavior" to UserBehaviorTracker.getLastBehaviors(),
"memory_info" to getMemoryInfo()
)
APMReporter.report("anr", anrData)
}
}
}
}
fileObserver.startWatching()
}
// 读取ANR日志
private fun readAnrLog(file: File): String {
return try {
file.readText()
} catch (e: Exception) {
"Read ANR log failed: ${e.message}"
}
}
// 获取ANR时的内存信息
private fun getMemoryInfo(): String {
val runtime = Runtime.getRuntime()
val usedMem = (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024)
val maxMem = runtime.maxMemory() / (1024 * 1024)
return "Used: $usedMem MB, Max: $maxMem MB"
}
private val executor = java.util.concurrent.Executors.newSingleThreadExecutor()
}
4. Crash 监控与上报(CrashHandler.kt)
捕获 Java/Native Crash,还原堆栈:
kotlin
import android.content.Context
import android.os.Build
import android.os.Process
import java.io.PrintWriter
import java.io.StringWriter
import java.lang.Thread.UncaughtExceptionHandler
class CrashHandler : UncaughtExceptionHandler {
private lateinit var context: Context
private val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
// 初始化(在Application中调用)
fun init(context: Context) {
this.context = context
if (APMConfig.ENABLE_CRASH_MONITOR) {
Thread.setDefaultUncaughtExceptionHandler(this)
}
}
override fun uncaughtException(t: Thread, e: Throwable) {
// 1. 收集Crash信息
val crashInfo = collectCrashInfo(t, e)
// 2. 上报Crash
APMReporter.report("crash", crashInfo)
// 3. 交给默认处理器(避免应用卡死)
defaultHandler?.uncaughtException(t, e)
// 4. 退出进程(可选)
Process.killProcess(Process.myPid())
}
// 收集Crash上下文信息
private fun collectCrashInfo(thread: Thread, e: Throwable): Map<String, Any> {
val stackTrace = getStackTraceString(e)
return mutableMapOf<String, Any>().apply {
put("thread_name", thread.name)
put("thread_id", thread.id)
put("exception_type", e.javaClass.simpleName)
put("exception_msg", e.message ?: "null")
put("stack_trace", stackTrace)
put("last_behavior", UserBehaviorTracker.getLastBehaviors())
put("android_version", Build.VERSION.SDK_INT)
put("is_debug", APMConfig.DEBUG)
// 可选:添加内存/CPU信息
put("memory_info", getMemoryInfo())
}
}
// 拼接完整堆栈
private fun getStackTraceString(e: Throwable): String {
val sw = StringWriter()
val pw = PrintWriter(sw)
e.printStackTrace(pw)
var cause = e.cause
while (cause != null) {
cause.printStackTrace(pw)
cause = cause.cause
}
pw.close()
return sw.toString()
}
// 获取内存信息
private fun getMemoryInfo(): String {
val runtime = Runtime.getRuntime()
val used = (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024)
val max = runtime.maxMemory() / (1024 * 1024)
return "Used: $used MB, Max: $max MB"
}
}
全局初始化(MyApplication.kt)
import android.app.Application
import android.content.Context
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 1. 初始化APM上报器
APMReporter.init(this)
// 2. 初始化启动监控
LaunchMonitor.init(this)
// 3. 初始化卡顿监控
JankMonitor().init()
// 4. 初始化ANR监控
AnrMonitor().init()
// 5. 初始化Crash监控
CrashHandler().init(this)
// 示例:记录应用启动行为
UserBehaviorTracker.track("Application onCreate")
}
// 全局Context(可选)
companion object {
lateinit var context: Context
private set
}
}
AndroidManifest.xml 配置
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<!-- 启动监控:捕获进程启动时间的ContentProvider -->
<provider
android:name=".LaunchProvider"
android:authorities="${applicationId}.launch.provider"
android:exported="false"/>
<!-- 权限配置(上报需要网络) -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!-- ANR监控需要读取文件权限(Android 10+需适配) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="28"/>
</application>
使用示例(Activity 中)
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 1. 记录用户行为(用于ANR/Crash时还原路径)
UserBehaviorTracker.track("MainActivity onCreate")
// 2. 监控Activity首帧耗时
LaunchMonitor.monitorActivityFirstFrame(window, "MainActivity")
// 3. 模拟卡顿(测试用,实际项目删除)
findViewById<android.view.View>(R.id.btn_test).setOnClickListener {
UserBehaviorTracker.track("Click test button")
// 主线程阻塞300ms,触发卡顿监控
Thread.sleep(300)
}
}
}
关键注意事项
权限适配:
- Android 10+ 读取
/data/anr/traces.txt需要MANAGE_EXTERNAL_STORAGE权限(仅测试用,线上建议用 Bugly/Matrix 等成熟方案); - 网络上报需要
INTERNET权限,弱网下会自动缓存到本地。
- Android 10+ 读取
混淆配置(proguard-rules.pro):
# 保留Crash堆栈 -keepattributes SourceFile,LineNumberTable -renamesourcefileattribute SourceFile # 保留APM相关类 -keep class com.your.package.apm.** { *; }性能开销:
- 所有监控逻辑均在子线程执行,主线程仅做轻量级埋点;
- 批量上报间隔 30 秒,避免频繁网络请求。
Native Crash 补充:
上述代码仅捕获 Java Crash,Native Crash 需集成 Breakpad/Bugly:
// 集成Bugly Native Crash监控 implementation 'com.tencent.bugly:crashreport:4.0.4' implementation 'com.tencent.bugly:nativecrashreport:4.0.4'
后台接口接收示例(JSON 格式)
上报的数据为 JSON 数组,单条数据格式示例:
[
{
"metric_type": "launch",
"timestamp": 1719888888888,
"device_model": "Pixel 8",
"os_version": "14",
"cold_launch_total": 2800,
"process_to_app_create": 1200,
"app_create_to_first_frame": 1600
},
{
"metric_type": "main_thread_block",
"cost_time": 300,
"stack_trace": "android.os.Handler.dispatchMessage(Handler.java:102)...",
"last_behavior": "1719888888888: Application onCreate\n1719888888999: MainActivity onCreate"
}
]
总结
APM 不是 “一次性工具”,而是贯穿项目全生命周期的体系:
- 开发阶段:埋点采集核心指标,提前发现潜在问题;
- 测试阶段:压测验证性能,确保达标;
- 线上阶段:全量监控,快速定位故障,闭环优化;
- 迭代阶段:通过数据对比,持续提升 App 稳定性和用户体验。