目录
[步骤 1:Application 中统一初始化](#步骤 1:Application 中统一初始化)
[步骤 2:创建全局埋点配置类(统一管理上报)](#步骤 2:创建全局埋点配置类(统一管理上报))
[步骤 3:创建自动点击埋点代理(核心)](#步骤 3:创建自动点击埋点代理(核心))
[步骤 4:创建页面生命周期监听(页面打开 / 关闭)](#步骤 4:创建页面生命周期监听(页面打开 / 关闭))
[步骤 4:效果展示(运行后自动输出)](#步骤 4:效果展示(运行后自动输出))
[步骤 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全局埋点。不用在每个页面、每个按钮手动写埋点代码,自动统计:
- 页面打开(onResume)
- 页面关闭(onPause/onDestroy)
- 页面内所有点击事件(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:所有窗口的根 ViewmParams:所有窗口的参数(是否是弹窗)mRoots:所有窗口的管理器
通过反射拿到这 3 个列表,监控里面新增的 View,从而自动发现 "弹窗" 并注入埋点
优点:
- 不用在每个弹窗手动调用 inject
- 完全无侵入,业务代码一行不用改
- 全局自动捕获所有弹窗(100% 覆盖)
- 兼容所有 Android 版本
- 比
WindowManager.addOnWindowAddedListener更稳定、兼容更广
缺点:
用到了反射(但属于 Android 埋点界标准用法)
步骤二:工作流程(超简单理解)
-
反射拿到系统唯一的
WindowManagerGlobal实例 -
反射拿到它内部的
mViews(所有窗口 View) -
监听这个列表新增 View
-
判断这个 View 是不是弹窗类型
-
如果是 → 自动注入点击埋点
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 -
反射拿到
mViews、mParams -
轮询 / 监听新增 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 是每个窗口的根管理器,反射获取 mView、mWindowManager 等。
优点
- 实时性高
缺点
- 兼容性极差,版本一变就崩
- 比 WindowManagerGlobal 更不稳定
- 代码更复杂、难维护
适合
- 研究框架
- 不建议生产使用