前端埋点方案研究及在Android项目中的实践

前言

数据埋点的质量直接关系到前端监控、数据分析结果的准确性,对应用的用户行为分析、数据分析决策、数据化运营、错误分析来说都是基础的存在,在前端监控与数据分析中是第一个重要的步骤。中本文聚焦于目前主流埋点方案的探究,以及其在Android项目中的具体实践。

埋点数据

数据埋点首先必须解决的,是需要埋什么的问题。

  • 站在产品的角度,埋点应该是页面停留时长、用户分享、用户支付等具体的用户行为,或者如网络连接变化、用户上下线等用户状态的变化等
  • 在前端开发的角度,埋点就是按钮点击、页面开启、页面关闭、错误信息捕获、网络状态变化广播接收等一系列的事件
  • 在数据分析的角度,埋点则是对这些事件的点进行清洗、筛选、分析,进而形成一系列的事件线,也就是如页面停留时长、用户支付路径等一系列产品或业务需要的用户行为事件。
    通常一个完整的事件组成结构如下:
    在这里插入图片描述
    在Android项目实践中,由于国家与各平台的隐私合规相关规定,ip、定位信息、Android设备唯一标记(mac、androidId等)已不再使用,可改为使用有盟OAID、工信部统一推送ID、自定义算法计算等方式,确定Android设备的唯一性。
    需要注意的是,卸载重装App可能会改变此类方法获取的设备唯一标识,因此只适合用来做确定设备唯一性的指标之一。

Android元素事件监听

在正式开始分析主流埋点方式在Android中的实践前,我们需要预先了解一下Android主流的用于埋点的事件监听机制,主要有Listener代理、Hook、AccessibilityDelegate三种监听方式,对于埋点而言,业务代码侵入性越低越能接受。下面以点击事件为例简单描述:

Listener代理

最简单的方式就是直接将元素的点击事件监听器等替换为自定义监听器。

view.setOnClickListener(object : ProxyListener() {
   @Override
   public void doOnClick(View view) {
      // 原View.OnClickListener.onClick事件
   }
})

abstract class ProxyListener : View.OnClickListener{
    @Override
    public void onClick(View view) {
        track() // 点击埋点逻辑
        doOnClick(view) 
    }
   
    abstract void doOnClick(View view)
}

反射Hook

通过反射获取activity的Rootview,遍历View树,将元素的OnClickListenr对象,替换为包裹了了原OnClickListener逻辑的OnClickListener对象,进而实现自动注入埋点逻辑。Hook时机可选择在Application启动时使用生命周期注入ActivityLifecycleCallbacks,获取activity对象,并在渲染完时完成注入。

{
    ...
	// 反射得到ListenerInfo对象
    val getListenerInfo = View::class.java.getDeclaredMethod("getListenerInfo")
    getListenerInfo.isAccessible = true
    val mListenerInfo = getListenerInfo.invoke(view)
    // 获取原始的OnClickListener
    val listenerInfoClz = Class.forName("android.view.View\$ListenerInfo")
    val mOnClickListener = listenerInfoClz.getDeclaredField("mOnClickListener")
    mOnClickListener.isAccessible = true
    val originOnClickListener: View.OnClickListener =  mOnClickListener.get(mListenerInfo) as View.OnClickListener
    // 用Hook代理类替换原始的OnClickListener
    val hookedOnClickListener = HookedClickListenerProxy(originOnClickListener)
    mOnClickListener.set(mListenerInfo, hookedOnClickListener)
    ....
}

class HookedClickListenerProxy(val originOnClickListener: View.OnClickListener):View.OnClickListener {
    override fun onClick(v: View?) {
        track() // 点击埋点逻辑
        originOnClickListener.onClick(v)
    }
}

此种Hook可以做到无需代码开发实现埋点,同时使用ActivityLifecycleCallbacks提供了配置化埋点的选择。

AccessibilityDelegate

利用辅助功能可检测控件点击,选中,滑动,文本等属性变化,具体流程与反射Hook基本一致,不同的是,将反射替换元素的OnClickListenr,改为设置元素的AccessibilityDelegate。
setAccessibilityDelegate同样可以做到无需代码开发实现埋点,同时使用ActivityLifecycleCallbacks提供了配置化埋点的选择,且没有反射Hook的属性反射消耗。

object TrackAccessibilityDelegate : View.AccessibilityDelegate() {
    override fun sendAccessibilityEvent(host: View?, eventType: Int) {
        super.sendAccessibilityEvent(host, eventType)
        if (eventType == AccessibilityEvent.TYPE_VIEW_CLICKED) {
            host?.let {
                track() // 点击埋点逻辑
            }
        }
    }
}

view.setAccessibilityDelegate(TrackAccessibilityDelegate)

前端主流埋点方案

有盟统计、博睿数据、神策数据、GrowingIO等一众数据采集分析平台,提供了方便快捷的应用监控与分析功能,而对数据安全比较重视,业务又相对复杂的公司则通常会有自己的一套前端监控与分析平台。无论是自研还是各分析平台,其主要的前端埋点技术无外乎三种:代码埋点、可视化埋点、无埋点。

代码埋点

最常用的埋点方式,由前端开发手动将事件数据采集的代码,加入到原有的业务代码中。
一般的代码埋点方式是直接在业务代码中,插入埋点代码段:

class XXActivity : BaseActivity() {
	// 页面开启埋点
	override fun onResume() {
		super.onResume()
		addTrackCode(
		    code = TrackCode.CODE_1,
		    eventType = "page",
		    visitType = VISIT_TYPE_OPEN
		)
	}
	
	// 页面离开埋点
	override fun onPause() {
		super.onPause()
		
		addTrackCode(
		    code = TrackCode.CODE_1,
		    eventType = "page",
		    visitType = VISIT_TYPE_LEAVE
		)
	}
	
	// 点击按钮、状态改变等业务埋点
	fun onStateChanged() {
		addTrackCode(
		    code = TrackCode.CODE_12,
		    eventType = "btn",
		    eventParam = arrayOf(mapOf("code" to "11112"))
		)
	}
}

鉴于代码埋点侵入性较强,在Android实际应用中,可以利用生命周期注入ActivityLifecycleCallbacks与注解,将部分埋点逻辑抽取,以减少代码侵入:

// 页面进出埋点被简化
@TrackPage(code = TrackCode.CODE_1)
class XXActivity : BaseActivity() {
	// 点击按钮、状态改变等业务埋点
	fun onXXXClick() {
		addTrackCode(
		    code = TrackCode.CODE_12,
		    eventType = "btn",
		    eventParam = arrayOf(mapOf("code" to "11112"))
		)
	}
}

/**
 * activity、Fragment页面埋点(进入、离开)
 * 在activity onCreated前插入Fragment的页面埋点插入
 */
object TrackActivityLifecycleCallbacksImpl : ExtActivityLifecycleCallbacks() {
    override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
        // 利用生命周期函数,在Lifecycle.Event.ON_CREATE时主动读取@TrackPage注解,完成页面进入埋点
        // 在Lifecycle.Event.ON_DESTROY时完成页面离开埋点
        (activity as? LifecycleOwner)?.lifecycle?.addObserver(TrackLifecycleObserver)
        // Fragment页面也一样处理
        (activity as? FragmentActivity)?.supportFragmentManager?.registerFragmentLifecycleCallbacks(
            TrackFragmentLifecycleCallbacksImpl, true
        )
    }
}

/**
 * 注入activity公共逻辑
 */
object TrackLifecycleObserver : LifecycleObserver {
    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun onLifecyclePause(owner: LifecycleOwner?) {
        owner?.apply {
            addTrackPageCode(owner, false)
        }
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun onLifecycleResume(owner: LifecycleOwner?) {
        owner?.apply {
            addTrackPageCode(owner, true)
        }
    }

    /**
     * 使用声明的TrackPage code,插入埋点
     */
    private fun addTrackPageCode(obj: Any, isOnResumed: Boolean) {
        obj::class.java.getAnnotation(TrackPage::class.java)?.code?.let {
            addTrackCode(
                code = it,
                eventType = "page",
                visitType = if (isOnResumed) VISIT_TYPE_OPEN else VISIT_TYPE_LEAVE
            )
        }
    }
}

代码埋点需要开发人员根据埋点需求,侵入性地在业务代码中埋点,此方法埋点位置精确,可以携带丰富的业务参数,但另一方面埋点开发工作量大,同时随着版本的迭代,部分页面或逻辑调整后,业务埋点的增删改会也会导致代码维护的困难。

可视化埋点

可视化埋点方案一般采用第三方平台sdk,可支持原生、web、react native等多平台,集成之后,通过可视化工具配置采集节点,在App/Web端解析配置,查找节点,监听节点产生的事件并上报到平台。神策数据可视化埋点是比较典型可视化埋点应用:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可视化埋点的实现由两个重要步骤实现:

  • 录制:通过可视化工具配置采集的View,目的是保存一份需埋点页面或元素的配置到后台,一般有两种实现方式:
    1. 神策数据使用app内嵌sdk,使app需埋点页面与后台管理界面同步,同时在管理界面上选择埋点配置
    2. GrowingIO的sdk采用圈选操作,app拖动圆点到需要监测的元素上,进而设置埋点配置
  • 采集:app解析上一步产生的的配置,hook相应页面或元素的事件,并上报数据

录制配置

录制配置时app与平台后端一般都是使用WebSocket连接,用于传输当前页面层次结构与执行埋点指令。
Android app中,sdk获取通过反射获取当前activity的RootView,进而通过遍历RootView得到View树,以及所有子View的所需属性(id、大小、位置、类型、子View等),同时通过截图Api createSnapshot获取当前页面截图,将截图与View树信息一并传给平台后端,平台后端则可根据这两份数据在还原页面结构,同时保存相应的元素关键信息。
操作人员在平台后端根据埋点需求,在相应元素指定埋点配置,例如按钮btn点击时,上报点击事件,同时可以携带上页面可见元素的部分信息,如某文本框上的文字等。此时就形成了一个明确的埋点配置,简略数据结构可如下:

  • target_activity:元素所在的activity类名
  • event_type:事件类型,例如点击事件
  • event_name:事件名称
  • path:在View树中路径
    • view_class:View的类名
    • index:在父View里的下标
    • id:View在Apk中的id

根据配置采集

sdk在启动时,会先下载录制配置步骤产生的所有埋点配置,根据这份埋点配置,可以使用代码埋点中的ActivityLifecycleCallbacks,在相应页面activity渲染完成时,使用id或遍历View树等,查找到需要埋点的元素。通过使用设置AccessibilityDelegate方式,给元素设置事件以及相应参数,在相应事件触发时,埋点就能被记录下来。

小结

不同于代码埋点需要开发人员进行大量的侵入性埋点,以及后续的相应代码维护,可视化埋点可按需由非技术人员完成,可以相对自由地在基本所有页面完成页面元素加载、元素点击事件的埋点。需要注意的是,可视化埋点在各平台基本都是属于收费服务,自建平台的话,需要重点解决录制配置的web平台、各端所使用到的sdk以及两者之间的通信机制。

无埋点

无埋点是指将所有的页面开关、所需的元素事件全部记录并上报后台,由数据分析后台清洗、分析数据再进行业务埋点,其中上章中的反射Hook、AccessibilityDelegate两种动态Hook技术均可用于实现无埋点。因为动态Hook使用反射,所以对运行效率会有一定的影响。
除了动态Hook,还可以使用面向切面的AOP静态Hook技术,在编译阶段,将埋点代码段直接“写入”到业务代码中,也不需要进行额外的埋点开发。静态Hook有两种主流的方式:

  • AspectJ:Android使用aspectjx实现AspectJ,通过注解切面,将埋点代码插入元素事件或特定函数方法的前面或后面。但是aspectjx与项目所使用到的阿里热更组件sophix以及hilt框架等均有冲突,此处不考虑
  • ASM:通过在编译阶段修改字节码文件完成插入埋点逻辑,性能好,但是有一定的上手难度
    在这里插入图片描述
    ASM修改字节码所需的知识储备略多,此处以插入点击事件埋点为例,简述几个关键点:
    1、gradle插件编写
    Android项目实现ASM无埋点最假的方式是使用gradle插件,因此需要预先新建插件项目,并提供插件参数配置,以过滤需要进行埋点的代码,如排除类、符合类、埋点方法规则等。
class TrackPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        // 注册埋点插桩
        val androidComponentsExtension = project.extensions.getByType(AndroidComponentsExtension::class.java)
        androidComponentsExtension.onVariants { variant ->
            variant.instrumentation.apply {
                // 控制哪些.class需要被扫描
                transformClassesWith(TrackClassVisitorFactory::class.java, InstrumentationScope.ALL) {
                    // 此处可以对接gradle配置参数
                    it.ignoreClassM.set(
                        listOf("*.XXXBaseActivity", "*.XXXFragment")
                    )
                }
                setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
            }
        }
    }
}

2、Transform Api
Transform Api将在gradle 8.0后被移除,需要使用TransformAction替代,AGP提供的AsmClassVisitorFactory极大地方便了我们使用Transform Action对.class文件进行ASM操作:

class TrackClassVisitorFactory(override val parameters: Property<TrackParams>, override val instrumentationContext: InstrumentationContext) : AsmClassVisitorFactory<TrackParams> {
    // 对符合条件的类,执行插桩
    override fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor = TrackClassNode(nextClassVisitor)

    // true时createClassVisitor才有效,此处可以过滤需要插桩的类
    override fun isInstrumentable(classData: ClassData) = parameters.get().ignoreClassM.get().indexOfFirst {  Pattern.matches(it, classData.className) } < 0
}

/**
 * 定义排除类、符合类、埋点方法规则等
 */
interface TrackParams : InstrumentationParameters {
    /**
     * 需要忽略的类,此处仅作示例
     */
    @get:Input
    val ignoreClassM: ListProperty<String>
}

简化后的埋点代码是写到onClick方法前面的,因此需要先使用ClassNode过滤出需要执行操作的类,并且过滤出onClick方法,将其交由相应的方法处理器TrackMethodVisitor:

class TrackClassNode(private val classVisitor: ClassVisitor) : ClassNode(Opcodes.ASM9) {
    // ASM Tree API会把class文件包装成ClassNode方便操作
    override fun visitMethod(access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor {
        return if (name == "onClick") {
            // 示例,在点击时插桩埋点代码
            val mv = super.visitMethod(access, name, descriptor, signature, exceptions)
            TrackMethodVisitor(mv)
        } else {
            super.visitMethod(access, name, descriptor, signature, exceptions)
        }
    }
}

方法处理器中插入埋点代码:

class TrackMethodVisitor(private val methodVisitor: MethodVisitor) : MethodVisitor(Opcodes.ASM9) {
    override fun visitCode() {
        super.visitCode()
        // 方法执行前插入埋点代码
        methodVisitor.visitFieldInsn(GETSTATIC, "com/bluemoon/configplugin/TrackUtil", "INSTANCE", "Lcom/bluemoon/configplugin/TrackUtil;")
        methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "com/bluemoon/configplugin/TrackUtil", "track", "()V", false)
    }
}

可以借助ASM Bytecode Viewer插件的ASMifield选项查看ASM操作代码,同时可以对比插入埋点代码前后的ASM操作代码,得出需要在相应方法中插入的操作码
在这里插入图片描述
3、发布插件
编译后完成后,ASM无埋点插件就做好了,可使用maven发布插件以供项目使用,此处不赘述。

小结

全埋点可以实现收集所有的页面、元素事件收集,同时对业务开发无侵入,但是庞大的埋点数据,需要强大的数据分析平台清洗筛选以及分析处理。一般支持全埋点的数据分析平台,都是有最长数据保存时限,若不及时转化成报表,会导致无法回溯历史数据。

埋点方式小结对比

埋点方式优点缺点适用场景
代码埋点1、埋点位置灵活,可精确记录业务事件,几乎可以覆盖所有数据采集场景
2、可携带丰富详细的业务参数
3、可灵活控制上报时机,可进行灵活数据预处理
1、埋点开发工作量大
2、代码侵入性大,业务变更可能需要重新开发埋点,维护困难
3、埋点数据未上传前存在客户端,非即时上传,有丢失可能(主动删除)
1、埋点需要把行为与业务数据充分结合分析,如支付
2、埋点位置不适合自动化配置,如监听业务状态变化、页面滑动等
可视化埋点1、业务可直接视图化选取埋点位置,简单方便
2、可主动开启或关闭埋点
3、开发只需要接入sdk,无需额外维护
1、埋点数据区分版本,历史数据无法回溯
2、无法采集业务相关数据与自定义数据,只能采集点击、元素展示等客户端行为基本事件
快速迭代只需简单统计PV、UV等指标,或业务场景简单的产品
无埋点1、开发维护简单,只需集成SDK,后续改版也不影响采集
2、覆盖全面,所有的点击、页面开启关闭、元素状态回调等页面元素信息都可采集
1、数据全采集,事件数据量大,后端存储压力大,可预先规定埋点数据预处理方案,压缩数据
2、工作方式决定了无埋点无法采集动态生成的页面或元素事件
3、无法采集业务相关数据与自定义数据
1、业务场景简单的产品
2、自建数据监控与分析平台,需要进行精细化业务分析,并可解决埋点数据量庞大的问题

总结

目前Android项目主要使用友盟移动统计进行用户数据基本分析,友盟APM与腾讯buggly结合进行异常监控,使用优化后的代码埋点框架进行业务埋点。由于友盟统计越来越多的功能开始收费,同时随着项目业务数据分析需求的定制化发展,我们也需要对前端数据采集、监控与分析进行一步步的迭代发展,本地数据预处理优化后的ASM无埋点+优化的代码埋点框架+第三方数据分析平台辅助是一种比较全面的选择。

参考资料

用户行为数据采集:常见埋点方案优劣势对比及选型建议
Android ASM插桩
Android埋点技术分析
Transform 现如今被废弃,ASM 该如何适配

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值