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集合,这样可以防止同一个页面重复查找。

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

相关推荐
顾北川_野几秒前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
hairenjing11232 分钟前
在 Android 手机上从SD 卡恢复数据的 6 个有效应用程序
android·人工智能·windows·macos·智能手机
小黄人软件28 分钟前
android浏览器源码 可输入地址或关键词搜索 android studio 2024 可开发可改地址
android·ide·android studio
dj15402252031 小时前
group_concat配置影响程序出bug
android·bug
周全全1 小时前
MySQL报错解决:The user specified as a definer (‘root‘@‘%‘) does not exist
android·数据库·mysql
- 羊羊不超越 -2 小时前
App渠道来源追踪方案全面分析(iOS/Android/鸿蒙)
android·ios·harmonyos
wk灬丨2 小时前
Android Kotlin Flow 冷流 热流
android·kotlin·flow
千雅爸爸2 小时前
Android MVVM demo(使用DataBinding,LiveData,Fresco,RecyclerView,Room,ViewModel 完成)
android
晨曦_子画3 小时前
编程语言之战:AI 之后的 Kotlin 与 Java
android·java·开发语言·人工智能·kotlin