Android 埋点之点击事件上报

点击事件的埋点,需要处理两个问题:

  • 如何将业务模块跟埋点模块隔离,减少对业务模块的侵入;
  • 基于第一点,怎么才能获取获取业务模块的属性呢,利用activity 的pageId。

1. 业务模块跟埋点模块如何隔离

基本思路:利用ActivityLifecycleCallbacks对Activity的生命周期进行监听,在其创建时获取设置了点击事件的View,当触发了点击事件,则上报相关信息。 只要遍历decorView就可以找出符合条件的view,那么如何监听点击事件呢?

方式1:代理原有的点击事件监听;

方式2:设置setAccessibilityDelegate监听,这是在点击事件后会触发的事件,我采取的是这种方式;

View源码如下:

ini 复制代码
public boolean performClick() {

    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);//点击事件回调
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);//这是我们的切入点
    return result;
}

2. 业务模块如何暴露相关属性

方式1:业务模块实现自定义接口,对外暴露相关属性;

方式2:业务模块利用view的setTag函数配置相关属性,我采取的是这种方式;

3.代码实现

ClickEventHelper 是点击事件处理的工具类,负责对符合条件的view绑定监听事件以及对上报前进行业务处理;

  • 注释1:业务属性实体类,,可根据实际情况进行定义,pageId为view所属的页面id,viewId为view表示,有别于xml 的id;
  • 注释2:负责查找设置了绑定事件的view,以及设置accessibilityDelegate监听事件;
  • 注释3:适配fragment的场景;
  • 注释4:查找设置了绑定事件的view,内部用到了递归,注意ViewGroup也可能设置了点击事件;
  • 注释5:给view设置accessibilityDelegate监听;
  • 注释6:处理监听回调,通过tag提取业务属性,然后进行业务处理;
kotlin 复制代码
class ClickEventHelper {
    data class ViewParams(val pageId: Int, val viewId: Int)//1
    companion object {
        const val TAG = "ClickEventHelper"
    }
    fun bindAccessibilityDelegateToViews(activity: Activity) {//2
        val hasOnClickListenersViewList = mutableListOf<View>()
        val rootView = activity.window.decorView as ViewGroup
        rootView.forEach {
            findHasClickListenerView(it, hasOnClickListenersViewList)
        }
        hasOnClickListenersViewList.forEach {
            bindToView(it)
        }
    }

    fun bindAccessibilityDelegateToViews(fragment: Fragment) {//3
        val hasOnClickListenersViewList = mutableListOf<View>()
        val rootView = fragment.requireView() as ViewGroup
        rootView.forEach {
            findHasClickListenerView(it, hasOnClickListenersViewList)
        }
        hasOnClickListenersViewList.forEach {
            bindToView(it)
        }
    }

    private fun findHasClickListenerView(parentView: View, list: MutableList<View>) {//4
        when (parentView) {
            is ViewGroup -> {
                if (parentView.hasOnClickListeners()) {//layout 也有可能有点击事件
                    list.add(parentView)
                }
                parentView.forEach {
                    findHasClickListenerView(it, list)
                }
            }
            else -> {
                if (parentView.hasOnClickListeners()) {
                    list.add(parentView)
                }
            }
        }
    }

    private fun bindToView(view: View) {//5
        view.accessibilityDelegate = object : View.AccessibilityDelegate() {
            override fun sendAccessibilityEvent(view: View, eventType: Int) {//6
                val p = view.tag as? ViewParams
                p?.apply {
                    val pageId = p.pageId
                    val viewId = p.viewId
                    //TODO 做相应的业务处理
                    //TODO 埋点上报
                }
            }
        }
    }
}

对ActivityLifecycleCallbacks的实现就比较简单了,监听Activity的创建,调用ClickEventHelper对应的函数;因为无法监听Fragment的创建,所以需要在其生命周期函数中调用bindAccessibilityDelegateToViews函数。 大家是否注意到注释2,用到了协程,为什么呢?其实跟onActivityCreated的回调时机有关。onActivityCreated函数比Activity的onCreate先执行,使用协程是保证bindAccessibilityDelegateToViews函数在onCreate执行完再触发,因为一般都是在onCreate函数中对view setTag。

kotlin 复制代码
class AppActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks {

    private val clickEventHelper = ClickEventHelper()

    override fun onActivityCreated(p0: Activity, p1: Bundle?) {//1
        val lifecycleScope = ((p0 as? LifecycleOwner)?.lifecycleScope) ?: return
        clickEventHelper.bindAccessibilityDelegateToViews(p0)
        lifecycleScope.launchWhenCreated {//2
            clickEventHelper.bindAccessibilityDelegateToViews(p0)
        }
    }


    override fun onActivityStarted(p0: Activity) {}
    override fun onActivityResumed(p0: Activity) {}
    override fun onActivityPaused(p0: Activity) {}
    override fun onActivityStopped(p0: Activity) {}
    override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) {}
    override fun onActivityDestroyed(p0: Activity) {}
}

使用方式:在Activity或Fragment对view设置tag即可;

kotlin 复制代码
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.nameTv.setOnClickListener {
            Log.d("MainActivity","点击了")
        }
        binding.nameTv.setTag(ClickEventHelper.ViewParams(10, 1010))
    }
}

4.优化空间

  • 上述代码只对设置了OnClickListener监听的view进行了处理,如果需要处理其他点击事件,例如CheckBox、RadioButton,对findHasClickListenerView函数进行扩展即可;
  • 如果页面中view太多,可考虑异步处理查找的逻辑;
  • 可以使用HashMap缓存符合条件的view,key为activity、fragment全类名,value为view集合,这样可以防止同一个页面重复查找。

有需要的同学,可以直接拿到走哈。

相关推荐
CL_IN2 小时前
高效集成销售订单数据到MySQL的方法
android·数据库·mysql
devlei3 小时前
Android JankStats实现解析
android
Vesper633 小时前
【Android】‘adb shell input text‘ 模拟器输入文本工具使用教程
android·adb
MyhEhud4 小时前
Kotlin apply 方法的用法和使用场景
android·kotlin·kotlin apply函数
Code额4 小时前
MySQL的事务机制
android·mysql·adb
蓝莓浆糊饼干6 小时前
请简述一下String、StringBuffer和“equals”与“==”、“hashCode”的区别和使用场景
android·java
李斯维8 小时前
深入理解 Android Canvas 变换:缩放、旋转、平移全解析(一)
android·canvas·图形学
_一条咸鱼_8 小时前
Android Retrofit 框架日志与错误处理模块深度剖析(七)
android
顾林海8 小时前
Flutter Dart 面向对象编程全面解析
android·前端·flutter
去伪存真8 小时前
摸着石头过河,重新支棱起Capacitor老项目
android·前端