如何利用kcp和asm自动给 compose 事件(页面切换和点击)埋点

如何利用kcp和asm自动给 compose 事件(页面切换和点击)埋点

前言

最近公司同事开发了一套用户行为日志自动采集框架,可是不巧的是,这套埋点不支持compose,而当时公司基本就我在用compose开发需求,上网看了一圈没发现有关compose的埋点,于是打算自己写过 github

1、页面切换事件

compose页面切换可以NavController.addOnDestinationChangedListener 设置监听,所以这里就可以用ASM在NavController的构造方法里直接添加监听就行了,

kotlin 复制代码
private const val LISTENER_CLASS = "com/ckenergy/cet/core/compose/CETDestinationChangedListener"

//把监听加入到NavController中
mv.visitFieldInsn(
    GETSTATIC,
    LISTENER_CLASS,
    "Companion",
    "L$LISTENER_CLASS\$Companion;"
)
mv.visitVarInsn(ALOAD, 0)
mv.visitTypeInsn(CHECKCAST, "androidx/navigation/NavController")
mv.visitMethodInsn(
    INVOKEVIRTUAL,
    "$LISTENER_CLASS\$Companion",
    "register",
    "(Landroidx/navigation/NavController;)V",
    false
)
//自己定义一个监听
class CETDestinationChangedListener: NavController.OnDestinationChangedListener {

    override fun onDestinationChanged(
        controller: NavController,
        destination: NavDestination,
        arguments: Bundle?
    ) {
        AutoTrackHelper.trackViewScreen(destination.route)
    }

    companion object{
        @JvmStatic
        fun register(controller: NavController) {
            controller.addOnDestinationChangedListener(CETDestinationChangedListener())
        }
    }
}

页面切换的情况比较简单,如果不太懂ASM可以先google一下,这里不过多介绍

2、点击事件

compose设置点击事件的方法是这样的fun Modifier.clickable(...., onClick: () -> Unit) ,onClick参数类型是() -> Unit在经过kotlin的编译器后会变成Function<Unit>类型的的数据, 假如我们用ASM对onClick插桩的话,无法获取上下文的信息,我们也不知道他是哪里点击的 然后我打算反编译apk看看compose编译器会不会对clickable生成一些其他的信息,最后发现这个方法ComposerKt.traceEventStart

可以看到这里有个参数是这个方法的名字和所在的行数,这不正式我需要的吗(知道方法名和行数就知道用户点击了哪里) 然后google compose compiler 找到了编译器的源码

在这个目录下的这个类ComposableFunctionBodyTransformer

compose这里使用的是KCP(Kotlin Compiler Plugin)的方式,可以在kotlin编译的过程中修改或添加一些方法,关于KCP的介绍大家可以看看这篇文章, 我这里也不过多介绍了

kotlin 复制代码
class ComposeClickTransformer(
    private val pluginContext: IrPluginContext,
) : IrElementTransformerVoidWithContext() {

    override fun visitCall(expression: IrCall): IrExpression {
        val origin = super.visitCall(expression) as IrCall
        //判断调用的方法是否是clickable
        if (expression.symbol.owner.kotlinFqName.asString() == Constants.COMPOSE_CLICK) {
            Log.w(TAG,"StatementTransformer visitCall  ${expression.symbol.owner.kotlinFqName.asString()}:: ${expression.render()}")
            //获取最后一个参数是lambda的表达式
            val lambda = origin.getValueArgument(origin.valueArgumentsCount - 1) as? IrFunctionExpression
            if (lambda?.function?.body != null)//在lambda表达式中添加插桩
                lambda.function.body = lambda.function.addIrClickable(pluginContext, lambda.function.body!!)
        }
        return origin
    }
}

按照kcp的要求自定义一个IR Transformer来获取clickable的方法调用,获取到lambda表达式后添加我们的代码AutoTrackHelper.trackClick

kotlin 复制代码
/**
 * 调用[Constants.TRACK_CLASS].trackClick方法
 */
fun IrPluginContext.trackClickFunc(): IrSimpleFunctionSymbol? =
    kotlin.runCatching { referenceClass(FqName(Constants.TRACK_CLASS))?.functionByName("trackClick") }.getOrNull() /*?: printlnFunc()*/

/**
 * 在body中添加trackClickFunc()
 */
fun IrFunction.addIrClickable(
    pluginContext: IrPluginContext,
    irBody: IrBody
): IrBlockBody {
    return DeclarationIrBuilder(pluginContext, symbol).irBlockBody {
        addClickTrack(pluginContext, this@addIrClickable)?.let {
            +it
        } //打印目标函数信息

        +irBlock(resultType = this@addIrClickable.returnType) {
            for (statement in irBody.statements) { //原有方法体中的表达式
                +statement
            }
        }
    }
}

fun IrBuilderWithScope.addClickTrack(
    pluginContext: IrPluginContext,
    function: IrFunction
): IrCall? {
    val concat = irConcat() // 拼接目标函数信息 【方法名()】
    val name = function.kotlinFqName.asString().replaceAfterLast(Constants.LAMBDA_NAME, ".clickable")
    val file = function.file.name
    val line = function.file.fileEntry.getLineNumber(function.startOffset) + 1
    val traceInfo = "$name ($file:$line)"
    Log.w("addClickTrack", traceInfo)
    concat.addArgument(irString(traceInfo))

    val func = pluginContext.trackClickFunc()

    if (func != null)
        return irCall(func).also {
            it.putValueArgument(0, concat)
        }
    return null
}

然后我们点击就会输出

参考资料

1、深入浅出 Compose Compiler(1) Kotlin Compiler & KCP

2、Kotlin-KCP的应用-第二篇

3、compose compiler

相关推荐
哲科软件7 小时前
跨平台开发的抉择:Flutter vs 原生安卓(Kotlin)的优劣对比与选型建议
android·flutter·kotlin
jyan_敬言13 小时前
【C++】string类(二)相关接口介绍及其使用
android·开发语言·c++·青少年编程·visual studio
程序员老刘13 小时前
Android 16开发者全解读
android·flutter·客户端
福柯柯14 小时前
Android ContentProvider的使用
android·contenprovider
不想迷路的小男孩14 小时前
Android Studio 中Palette跟Component Tree面板消失怎么恢复正常
android·ide·android studio
餐桌上的王子14 小时前
Android 构建可管理生命周期的应用(一)
android
菠萝加点糖14 小时前
Android Camera2 + OpenGL离屏渲染示例
android·opengl·camera
用户20187928316715 小时前
🌟 童话:四大Context徽章诞生记
android
yzpyzp15 小时前
Android studio在点击运行按钮时执行过程中输出的compileDebugKotlin 这个任务是由gradle执行的吗
android·gradle·android studio
aningxiaoxixi15 小时前
安卓之service
android