rokevin
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
  • APM

  • 移动 APM 的核心能力
  • 移动 APM 的核心价值
  • 移动 APM 的实现方案(开源 vs 商业化)
    • 1. 开源 APM 方案(适合自研 / 定制化)
    • 2. 商业化 APM 方案(适合快速接入 / 全平台)
  • 移动 APM 核心指标与阈值(Android 参考)
  • APM 与相关工具的关系
  • APM 核心落地指南:性能监控 / 异常监控实现 + 实战案例
    • APM 如何实现性能监控?(核心:埋点采集 + 数据上报 + 分析)
      • 1. 核心性能指标采集实现(附代码模板)
      • 2. 数据上报与分析(核心流程)
    • 如何使用 APM 进行异常监控?(核心:全量捕获 + 上下文还原 + 快速定位)
      • 1. Crash 监控(Java/Native)
      • 2. ANR 监控
      • 3. OOM 监控
      • 4. 异常监控闭环
    • APM 在实际项目中的应用案例(大厂实战)
      • 案例 1:电商 App 启动耗时优化(某头部电商)
      • 案例 2:短视频 App 卡顿优化(某短视频平台)
      • 案例 3:出行 App Crash 率优化(某打车平台)
      • 案例 4:金融 App ANR 优化(某银行 App)
      • APM 落地关键注意事项
  • APM 集成代码
    • 核心依赖(build.gradle)
    • APM 核心配置类(APMConfig.kt)
    • 通用工具类
      • 1. 数据上报工具(APMReporter.kt)
      • 2. 用户行为追踪工具(UserBehaviorTracker.kt)
    • 核心监控实现
      • 1. 启动监控(LaunchMonitor.kt)
      • 2. 卡顿监控(JankMonitor.kt)
      • 3. ANR 监控与上报(AnrMonitor.kt)
      • 4. Crash 监控与上报(CrashHandler.kt)
    • 全局初始化(MyApplication.kt)
    • AndroidManifest.xml 配置
    • 使用示例(Activity 中)
    • 关键注意事项
    • 后台接口接收示例(JSON 格式)
  • 总结

APM

Bugly+Shiply 国内主流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 的核心价值

  1. 提前预警:在用户反馈前,主动发现性能劣化(如启动耗时突然增加 20%)和潜在故障(如某机型 Crash 率飙升);
  2. 快速定位:线上出现 ANR/Crash 时,提供完整上下文(如用户操作步骤、设备信息、堆栈日志、内存状态),避免 “无法复现” 的难题;
  3. 优化指导:通过性能数据对比(如优化前后启动耗时变化),验证优化效果;通过卡顿 / 慢接口排行,明确优化优先级;
  4. 版本评估:每次发版后,自动生成性能 / 稳定性报告,判断新版本是否达标(如 Crash 率是否低于 0.1%)。

移动 APM 的实现方案(开源 vs 商业化)

1. 开源 APM 方案(适合自研 / 定制化)

适合有技术团队、需要深度定制的企业,可基于开源框架二次开发:

开源方案厂商 / 社区核心优势
Matrix腾讯专注 Android,覆盖启动、卡顿、内存、网络、Crash 全维度,文档完善,接入简单
ArgusAPM阿里支持 Android/iOS,监控维度全面,内置告警机制
WatchDog社区轻量级,主打 ANR 监控和卡顿检测,适合小型项目
KOOM阿里专注内存问题(OOM / 内存泄漏),线上堆快照分析能力强

2. 商业化 APM 方案(适合快速接入 / 全平台)

适合中小型团队,无需自建服务,开箱即用:

商业化方案厂商核心优势
Firebase PerformanceGoogle免费,与 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. 采集启动各阶段耗时:进程启动(1.2s)→ Application 初始化(2.5s)→ 首帧渲染(2.3s);
    2. 定位瓶颈:Application 中 3 个第三方 SDK(埋点、推送、统计)同步初始化耗时 1.8s;
    3. 优化方案:将非必需 SDK 改为子线程懒加载,首屏布局改为 ViewStub 懒加载;
    4. 效果:冷启动耗时从 6.2s 降至 2.8s,用户留存率提升 15%。

案例 2:短视频 App 卡顿优化(某短视频平台)

  • 问题:滑动视频时频繁卡顿(FPS<40),用户投诉多;
  • APM 落地:
    1. 采集卡顿数据:主线程阻塞 > 200ms 的场景中,70% 是视频帧解码 + 图片加载阻塞;
    2. 定位根因:视频解码在主线程执行,图片加载未做内存缓存;
    3. 优化方案:视频解码移到子线程,图片加载用 LruCache 缓存,开启硬件解码;
    4. 效果:卡顿率从 8.2% 降至 1.5%,播放流畅度提升 90%。

案例 3:出行 App Crash 率优化(某打车平台)

  • 问题:部分机型 Crash 率 > 1%,主要是 OOM 和空指针;
  • APM 落地:
    1. 全量捕获 Crash:OOM 占比 60%,空指针占比 25%;
    2. 分析 OOM 根因:地图 SDK 内存泄漏 + Bitmap 未压缩;
    3. 优化方案:修复地图 SDK 引用泄漏,Bitmap 按屏幕尺寸压缩,内存不足时清空缓存;
    4. 效果:Crash 率从 1.2% 降至 0.08%,线上投诉减少 90%。

案例 4:金融 App ANR 优化(某银行 App)

  • 问题:支付页面偶现 ANR,无法复现;
  • APM 落地:
    1. 监听 ANR 日志:捕获到 ANR 时主线程阻塞在「加密算法计算」,耗时 8 秒;
    2. 定位根因:支付密码加密在主线程执行,低配机计算耗时久;
    3. 优化方案:加密算法移到子线程,增加加载动画,避免主线程阻塞;
    4. 效果:ANR 率从 0.3% 降至 0.02%,支付成功率提升 5%。

APM 落地关键注意事项

  1. 采集开销控制:
    • 性能指标采集频率不宜过高(如内存每 5 秒一次,帧率每 1 秒一次);
    • 上报采用批量 + 压缩,避免占用过多流量 / 电量;
  2. 隐私合规:
    • 不采集用户敏感信息(如手机号、身份证),上报数据脱敏;
    • 遵循 GDPR / 工信部规范,提供隐私政策说明;
  3. 灰度验证:
    • 新功能 / 优化方案先灰度发布,通过 APM 对比性能数据,验证效果后全量;
  4. 告警分级:
    • P0(紧急):Crash 率 > 1%、ANR 率 > 0.5%;
    • P1(高优先级):启动耗时 > 5 秒、卡顿率 > 5%;
    • P2(普通):内存占用略高、网络请求成功率 < 99%。

APM 集成代码

以下是 启动监控、卡顿监控、ANR 上报、Crash 上报 的完整 APM 集成代码(Android/Kotlin),包含核心采集逻辑、数据上报、工具类封装,可直接复制到项目中使用,适配 Android 9+ 全版本。

这套代码实现了 启动 / 卡顿 / ANR/Crash 四大核心监控能力,纯原生实现无第三方依赖,适合中小型项目快速集成。后续也可以集成:

  1. 集成 Native Crash 监控;
  2. 对接第三方 APM 平台(如 Bugly/Matrix);
  3. 增加内存 / 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)
        }
    }
}

关键注意事项

  1. 权限适配:

    • Android 10+ 读取 /data/anr/traces.txt 需要 MANAGE_EXTERNAL_STORAGE 权限(仅测试用,线上建议用 Bugly/Matrix 等成熟方案);
    • 网络上报需要 INTERNET 权限,弱网下会自动缓存到本地。
  2. 混淆配置(proguard-rules.pro):

    # 保留Crash堆栈
    -keepattributes SourceFile,LineNumberTable
    -renamesourcefileattribute SourceFile
    # 保留APM相关类
    -keep class com.your.package.apm.** { *; }
    
  3. 性能开销:

    • 所有监控逻辑均在子线程执行,主线程仅做轻量级埋点;
    • 批量上报间隔 30 秒,避免频繁网络请求。
  4. 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 稳定性和用户体验。