Android 自动埋点(页面打开 / 关闭 + 点击事件)完整方案

目录

介绍

核心原理

Activity监听

[步骤 1:Application 中统一初始化](#步骤 1:Application 中统一初始化)

[步骤 2:创建全局埋点配置类(统一管理上报)](#步骤 2:创建全局埋点配置类(统一管理上报))

[步骤 3:创建自动点击埋点代理(核心)](#步骤 3:创建自动点击埋点代理(核心))

[步骤 4:创建页面生命周期监听(页面打开 / 关闭)](#步骤 4:创建页面生命周期监听(页面打开 / 关闭))

[步骤 4:效果展示(运行后自动输出)](#步骤 4:效果展示(运行后自动输出))

Fragment监听

[步骤 1:Fragment 生命周期监听类](#步骤 1:Fragment 生命周期监听类)

窗口监听

[步骤一:先明确:Android 里 "窗口" 都包括啥](#步骤一:先明确:Android 里 “窗口” 都包括啥)

步骤二:工作流程(超简单理解)

[Android 窗口监听 5 大类方式(全覆盖)](#Android 窗口监听 5 大类方式(全覆盖))

[✅ 方式 1:Application + ActivityLifecycleCallbacks(官方、最稳)](#✅ 方式 1:Application + ActivityLifecycleCallbacks(官方、最稳))

[✅ 方式 2:反射 Hook WindowManagerGlobal(现在用的!弹窗全覆盖)](#✅ 方式 2:反射 Hook WindowManagerGlobal(现在用的!弹窗全覆盖))

[✅ 方式 3:WindowManager.addOnWindowAddedListener(Android R+ 官方)](#✅ 方式 3:WindowManager.addOnWindowAddedListener(Android R+ 官方))

[✅ 方式 4:AccessibilityService 辅助服务(系统级、全窗口)](#✅ 方式 4:AccessibilityService 辅助服务(系统级、全窗口))

[✅ 方式 5:ViewRootImpl 反射 + 轮询(小众、不推荐)](#✅ 方式 5:ViewRootImpl 反射 + 轮询(小众、不推荐))


介绍

无侵入式自动打点:只需在Application初始化,实现整个App全局埋点。不用在每个页面、每个按钮手动写埋点代码,自动统计:

  1. 页面打开(onResume)
  2. 页面关闭(onPause/onDestroy)
  3. 页面内所有点击事件(Button、TextView、ImageView 等)

核心原理

  • 页面:Activity 生命周期 + Fragment 全局生命周期监听
  • 弹窗 :监听Dialog/DialogFragment/自定义弹窗视图加载,注入点击监听
  • 点击 :统一用AccessibilityDelegate全局注入,Activity/Fragment/ 弹窗内控件全覆盖
  • 无侵入,不用改业务点击代码

Activity监听

步骤 1:Application 中统一初始化

复制代码
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        // Activity页面监听
        registerActivityLifecycleCallbacks(ActivityTrackLifecycle())
        
        // 全局Fragment监听(所有AppCompatActivity下Fragment都生效)
        androidx.fragment.app.FragmentManager.enableDebugLogging(false)
        supportFragmentManager.registerFragmentLifecycleCallbacks(FragmentTrackCallback(), true)
    }
}

步骤 2:创建全局埋点配置类(统一管理上报)

复制代码
import android.content.Context
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

// 埋点工具类:替换成你的真实上报接口
object TrackHelper {
    private const val TAG = "AutoTrack"

    // 上报事件通用方法
    fun trackEvent(context: Context?, eventName: String, params: Map<String, Any?> = emptyMap()) {
        context ?: return
        // ======================
        // 在这里写你的埋点上报逻辑
        // ======================
        Log.d(TAG, "【埋点上报】事件:$eventName,参数:$params")
    }
}

步骤 3:创建自动点击埋点代理(核心)

自动监听所有页面内可点击控件的点击:

复制代码
import android.view.View
import android.view.View.AccessibilityDelegate
import android.view.ViewGroup
import android.view.accessibility.AccessibilityNodeInfo
import widget.TextView

// 自动点击埋点代理:Hook 所有 View 的点击
object AutoClickTrackDelegate {

    private val DELEGATE = object : AccessibilityDelegate() {
        override fun performClick(host: View?) {
            super.performClick(host)
            host ?: return
            // 点击埋点上报
            trackClickEvent(host)
        }
    }

    // 给一个 View 和所有子 View 设置点击埋点
    fun inject(view: View) {
        if (view.isClickable) {
            setDelegate(view)
        }
        if (view is ViewGroup) {
            for (i in 0 until view.childCount) {
                inject(view.getChildAt(i))
            }
        }
    }

    private fun setDelegate(view: View) {
        if (view.accessibilityDelegate !== DELEGATE) {
            view.accessibilityDelegate = DELEGATE
        }
    }

    // 上报点击事件
    private fun trackClickEvent(view: View) {
        val pageName = (view.context as? AppCompatActivity)?.javaClass?.simpleName ?: "未知页面"
        val viewName = view::class.java.simpleName
        val viewId = view.resources?.getResourceEntryName(view.id) ?: "无ID"

        TrackHelper.trackEvent(
            view.context,
            "页面点击",
            mapOf(
                "页面" to pageName,
                "控件类型" to viewName,
                "控件ID" to viewId
            )
        )
    }
}

步骤 4:创建页面生命周期监听(页面打开 / 关闭)

复制代码
import android.app.Activity
import android.app.Application
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

// 全局页面生命周期监听:自动上报页面打开/关闭
class ActivityTrackLifecycle : Application.ActivityLifecycleCallbacks {

    override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
        // 页面创建完成后,注入点击埋点
        if (activity is AppCompatActivity) {
            val contentView = activity.findViewById<View>(android.R.id.content)
            AutoClickTrackDelegate.inject(contentView)
        }
    }

    override fun onActivityResumed(activity: Activity) {
        // 页面打开
        TrackHelper.trackEvent(
            activity,
            "页面打开",
            mapOf("页面名称" to activity.javaClass.simpleName)
        )
    }

    override fun onActivityPaused(activity: Activity) {
        // 页面关闭(切后台/跳转)
        TrackHelper.trackEvent(
            activity,
            "页面关闭",
            mapOf("页面名称" to activity.javaClass.simpleName)
        )
    }

    override fun onActivityStarted(activity: Activity) {}
    override fun onActivityDestroyed(activity: Activity) {}
    override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
    override fun onActivityStopped(activity: Activity) {}
}

步骤 4:效果展示(运行后自动输出)

复制代码
【埋点上报】事件:页面打开,参数:{页面名称=MainActivity}
【埋点上报】事件:页面点击,参数:{页面=MainActivity, 控件类型=Button, 控件ID=btn_login}
【埋点上报】事件:页面关闭,参数:{页面名称=MainActivity}
【埋点上报】事件:页面打开,参数:{页面名称=HomeActivity}

Fragment监听

步骤 1:Fragment 生命周期监听类

复制代码
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks

class FragmentTrackCallback : FragmentLifecycleCallbacks() {

    override fun onFragmentResumed(fm: FragmentManager, f: Fragment) {
        super.onFragmentResumed(fm, f)
        // Fragment页面打开
        TrackHelper.trackEvent(
            f.context,
            "Fragment打开",
            mapOf("fragment名称" to f.javaClass.simpleName)
        )
        // 注入Fragment内所有点击监听
        f.view?.let { AutoClickTrackDelegate.inject(it) }
    }

    override fun onFragmentPaused(fm: FragmentManager, f: Fragment) {
        super.onFragmentPaused(fm, f)
        // Fragment页面关闭
        TrackHelper.trackEvent(
            f.context,
            "Fragment关闭",
            mapOf("fragment名称" to f.javaClass.simpleName)
        )
    }
}

窗口监听

步骤一:先明确:Android 里 "窗口" 都包括啥

✅ 系统 Dialog

AlertDialog

DialogFragment

PopupWindow

✅ 自定义弹窗

✅ 第三方 SDK 弹窗(如广告、分享、登录弹窗)

✅ 悬浮窗

✅ Toast(可选)

系统里所有界面 (Activity、Dialog、PopupWindow、DialogFragment、悬浮窗、Toast、输入法窗口...) 最终都会走到 同一个系统单例:WindowManagerGlobal

它里面维护了3 个核心列表

  • mViews:所有窗口的根 View
  • mParams:所有窗口的参数(是否是弹窗)
  • mRoots:所有窗口的管理器

通过反射拿到这 3 个列表,监控里面新增的 View,从而自动发现 "弹窗" 并注入埋点

优点:
  1. 不用在每个弹窗手动调用 inject
  2. 完全无侵入,业务代码一行不用改
  3. 全局自动捕获所有弹窗(100% 覆盖)
  4. 兼容所有 Android 版本
  5. WindowManager.addOnWindowAddedListener 更稳定、兼容更广
缺点:

用到了反射(但属于 Android 埋点界标准用法)

步骤二:工作流程(超简单理解)

  1. 反射拿到系统唯一的 WindowManagerGlobal 实例

  2. 反射拿到它内部的 mViews(所有窗口 View)

  3. 监听这个列表新增 View

  4. 判断这个 View 是不是弹窗类型

  5. 如果是 → 自动注入点击埋点

    object DialogHookManager {

    复制代码
     // 1. 获取系统单例 WindowManagerGlobal
     private val wmGlobal: Any? by lazy {
         runCatching {
             Class.forName("android.view.WindowManagerGlobal")
                 .getMethod("getInstance")
                 .invoke(null)
         }.getOrNull()
     }
    
     // 2. 所有窗口的根View列表
     private val mViewsField: Field? by lazy {
         runCatching {
             Class.forName("android.view.WindowManagerGlobal")
                 .getDeclaredField("mViews").apply { isAccessible = true }
         }.getOrNull()
     }
    
     // 3. 所有窗口的参数(判断是不是弹窗)
     private val mParamsField: Field? by lazy {
         runCatching {
             Class.forName("android.view.WindowManagerGlobal")
                 .getDeclaredField("mParams").apply { isAccessible = true }
         }.getOrNull()
     }
    
     // 开始监听弹窗
     fun startListen() {
         val global = wmGlobal ?: return
         val views = mViewsField?.get(global) as? ArrayList<View> ?: return
         val params = mParamsField?.get(global) as? ArrayList<WindowManager.LayoutParams> ?: return
    
         // 开启监控:每次添加新View,就检查是不是弹窗
         Thread {
             while (true) {
                 try {
                     Thread.sleep(100)
                     checkNewDialog(views, params)
                 } catch (e: Exception) {
                 }
             }
         }.start()
     }
    
     // 检查新增的弹窗并注入点击
     private fun checkNewDialog(views: ArrayList<View>, params: ArrayList<WindowManager.LayoutParams>) {
         for (i in views.indices) {
             val view = views[i]
             val param = params[i]
             
             // 判断是不是弹窗(type 是系统弹窗类型)
             if (param.type == WindowManager.LayoutParams.TYPE_APPLICATION ||
                 param.type == WindowManager.LayoutParams.TYPE_APPLICATION_PANEL ||
                 param.type == WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG
             ) {
                 // 未注入过 → 自动注入点击埋点
                 if (view.tag != "injected") {
                     view.tag = "injected"
                     AutoClickTrackDelegate.inject(view) // 你的点击注入
                     TrackHelper.trackEvent(view.context, "弹窗弹出", emptyMap())
                 }
             }
         }
     }

    }

Android 窗口监听 5 大类方式(全覆盖)

方式 能监听 Activity 能监听 Dialog / 弹窗 官方 API 兼容性 适合埋点?
ActivityLifecycleCallbacks 全版本 页面埋点
WindowManagerGlobal 反射 100% ❌(反射) 7~14 首选(你现在方案)
addOnWindowAddedListener ✅(大部分) 11+ ❌(兼容差)
AccessibilityService ✅(全手机) ✅(全手机) 全版本 ❌(要授权、隐私)
ViewRootImpl 反射 ❌(不稳定)
  • WindowManagerGlobal 反射

    • Android 埋点界标准方案
    • 唯一能无侵入、全覆盖所有弹窗 的方案
    • 完全适合你要做的:页面 + 所有弹窗 + 弹窗内点击
  • ActivityLifecycleCallbacks :只做页面,不能监听弹窗

✅ 方式 1:Application + ActivityLifecycleCallbacks(官方、最稳)

原理

在 Application 注册生命周期回调:

复制代码
application.registerActivityLifecycleCallbacks(...)

拿到每个 Activity 的 onCreate/onResume/onPause/onDestroy

优点

  • 官方 API,100% 兼容、无反射、不闪退
  • 最简单、最稳定
  • 适合做页面级埋点

缺点

  • 完全监听不到任何弹窗(Dialog、PopupWindow 都不行)

适合

  • 页面打开 / 关闭埋点
  • 不做弹窗监听

✅ 方式 2:反射 Hook WindowManagerGlobal(现在用的!弹窗全覆盖)

监听范围App 内 100% 窗口

  • Activity

  • Dialog / AlertDialog

  • DialogFragment

  • PopupWindow

  • 自定义弹窗

  • 第三方 SDK 弹窗(广告、登录、分享)

  • 输入法窗口、Toast(可选)

    WindowManagerGlobal → mViews(所有窗口根View)、mParams(窗口参数)

  • 反射拿到系统单例 WindowManagerGlobal

  • 反射拿到 mViewsmParams

  • 轮询 / 监听新增 View → 判断是否弹窗 → 注入点击埋点

优点(埋点首选)

  • 唯一能 100% 覆盖所有弹窗的方案
  • 全局无侵入,业务代码不用改一行
  • 稳定、成熟,大厂埋点 SDK 都用这个
  • 兼容 Android 7~14

缺点

  • 用了反射(但系统内部 API,不会随便改)
  • 不能监听系统级别的其他 App 窗口(只能监听自己 App)

适合

  • 你现在要做的:页面 + 所有弹窗 + 弹窗内点击埋点
  • 自动打点、无侵入埋点

✅ 方式 3:WindowManager.addOnWindowAddedListener(Android R+ 官方)

监听范围 :Android 11(R)+,能监听大部分窗口

  • Activity

  • Dialog

  • PopupWindow

  • 悬浮窗

    windowManager.addOnWindowAddedListener(listener)

窗口被添加 / 移除时回调。

优点

  • 官方 API,无反射
  • 简单、干净

缺点

  • 只能用在 Android 11+
  • 覆盖不全:某些 DialogFragment、系统窗口监听不到
  • 低版本(≤10)完全不能用

适合

  • 只做高版本、简单弹窗监听
  • 不适合生产环境埋点(兼容差、覆盖不全)

✅ 方式 4:AccessibilityService 辅助服务(系统级、全窗口)

监听范围整个手机所有 App 的所有窗口(包括系统 UI、通知栏、别人 App 弹窗)

原理

系统级辅助功能,开启后能监听:

  • 窗口创建 / 销毁
  • View 点击
  • 焦点变化
  • 弹窗弹出

优点

  • 最强监听能力:所有窗口、所有点击都能抓
  • 不需要反射

缺点(致命)

  • 必须用户手动授权(系统设置 → 辅助功能)
  • 用户拒绝就完全用不了
  • 隐私敏感,上架可能被拒
  • 会监听全手机,不是只监听自己 App

适合

  • 无障碍工具、自动点击工具
  • 绝对不适合 App 内埋点

✅ 方式 5:ViewRootImpl 反射 + 轮询(小众、不推荐)

监听范围:App 内窗口,类似 WindowManagerGlobal,但更底层

原理

ViewRootImpl 是每个窗口的根管理器,反射获取 mViewmWindowManager 等。

优点

  • 实时性高

缺点

  • 兼容性极差,版本一变就崩
  • 比 WindowManagerGlobal 更不稳定
  • 代码更复杂、难维护

适合

  • 研究框架
  • 不建议生产使用
相关推荐
私人珍藏库2 小时前
【Android】小小最新AI--千变万化扮演任何角色--沉浸式互动
android·app·工具·软件·多功能
zh_xuan2 小时前
Android MVI架构
android·mvi
测试开发-学习笔记3 小时前
Airtest+Poco快速上手
android·其他
李斯维3 小时前
Android Jetpack 简介:由来和演进
android·android studio·android jetpack
阿巴斯甜3 小时前
ARouter 的使用:
android
沐言人生3 小时前
ReactNative 源码分析9——Native View初始化
android·react native
程序员陆业聪4 小时前
当AI学会了混淆代码:LLM辅助混淆 vs R8,Android安全的下一个十字路口
android
yubin12855709234 小时前
mysql正则函数REGEXP
android·数据库·mysql
我命由我123455 小时前
Android Framework P2 - 开机启动 Zygote 进程、Zygote 的预加载机制
android·java·开发语言·python·java-ee·intellij-idea·zygote