目录
  1. 1. 一、核心原理
  2. 2. 二、代理实现
  3. 3. 三、全局拦截方案
  4. 4. 四、局限性
  5. 5. 面试常考问题
【全埋点方案系列】AppClick全埋点之View.OnClickListener代理

全埋点(自动埋点)的核心诉求是:不修改业务代码,全局采集用户点击行为。View.OnClickListener 代理方案是最直观的思路——在 View 设置点击监听时,用一个 Wrapper 拦截所有 onClick 调用,先执行埋点逻辑,再转发给原始监听器。

一、核心原理

Android 中所有点击事件最终通过 View.setOnClickListener() 注册。如果能在这个入口做一层代理,就能捕获所有点击。

实现方式:在 Application 或 BaseActivity 中,通过反射或 Hook 替换 WMS 层的点击分发,但更简单的方式是利用 LayoutInflater.Factory2 在布局解析阶段拦截所有 View 创建。

二、代理实现

class TrackingOnClickListener(
private val origin: View.OnClickListener?,
private val view: View
) : View.OnClickListener {

override fun onClick(v: View) {
// 1. 先埋点
trackClick(v)

// 2. 再转发原始事件
origin?.onClick(v)
}

private fun trackClick(view: View) {
val info = buildString {
append("view_id=${view.id}")
append("&text=${(view as? TextView)?.text}")
append("&class=${view.javaClass.simpleName}")
append("&path=${getViewPath(view)}")
}
// 发送到埋点 SDK
AnalyticsSDK.track("app_click", info)
}
}

三、全局拦截方案

配合 LayoutInflater Hook,在 View 创建时自动替换 OnClickListener:

class WrapperFactory(private val delegate: LayoutInflater.Factory2) : LayoutInflater.Factory2 {
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
val view = delegate.onCreateView(parent, name, context, attrs)
view?.let { wrapOnClickListener(it) }
return view
}

private fun wrapOnClickListener(view: View) {
// 反射读取 mOnClickListener,替换为 TrackingOnClickListener
val field = View::class.java.getDeclaredField("mOnClickListener")
field.isAccessible = true
val origin = field.get(view) as? View.OnClickListener ?: return
field.set(view, TrackingOnClickListener(origin, view))
}
}

在 Application 中注册:

// 在 Application.onCreate 中通过 registerActivityLifecycleCallbacks
// 在 Activity.onCreate 时替换 LayoutInflater
activity.layoutInflater.factory2 = WrapperFactory(factory2)

四、局限性

  1. 动态设置的 Listener:通过 setOnClickListener 在运行时动态设置的监听器无法被 LayoutInflater Factory 拦截,需额外 Hook View.setOnClickListener()
  2. Dialog/Toast/PopupWindow:这些窗口不经过 Activity 的 LayoutInflater,需单独处理。
  3. Lambda 表达式:Kotlin 中 view.setOnClickListener { } 会在编译期生成匿名内部类,反射替换时需注意类型兼容性。
  4. 性能影响:每次点击多一层代理调用,开销可忽略不计。

面试常考问题

Q1:为什么不用 AspectJ 直接 Hook onClick 方法?

OnClickListener 代理方案的优势是纯运行时方案,无需 Gradle 插件、AOP 编译器或字节码修改。AspectJ 是编译期方案,对项目构建流程有侵入性。两者选型取决于团队技术栈:代理方案轻量但覆盖度有限;AspectJ 覆盖全面但维护成本高。

Q2:如何处理 RecyclerView item 的点击?

RecyclerView 的 item 点击通常在 Adapter 的 onBindViewHolder 中设置,代理方案需确保 Adapter 设置监听器时走代理流程。可以在 RecyclerView 的 setAdapter 上做一层 Hook,或直接在 ViewHolder 的 itemView.setOnClickListener 拦截。

Q3:View 的内部 clickable 属性与代理的关系?

代理不改变 View 的 clickable 状态。如果 View 没有设置 OnClickListener 且 clickable=true,点击事件仍会触发但不会被代理拦截,因为它们没有注册监听器。全埋点方案通常配合触摸事件代理(dispatchTouchEvent)覆盖此类场景。AOSP 源码入口:View.java 中的 setOnClickListener() 方法(frameworks/base/core/java/android/view/View.java)。

打赏
  • 微信
  • 支付宝

评论