讲讲android埋点那些事

原因是想体系的学习下asm相关的语法的,中间看到有asm全埋点实战这本书和神策开源的埋点项目,想着不如用一篇文章总结下我关于android埋点相关的一些理解,这篇文章的后半部分会详细的剖析下神策开源的埋点项目的相关细节完成。

埋点的效果其实是有人想获得运用运转中的一些信息,这儿的角色或许是开发,或许是产品,也或许是老板;这儿的信息或许是运用运转的事务信息,功用信息等等;埋点信息需求经过一系列杂乱的操作,转化为更易运用的信息类型,帮助咱们进行技术或事务上的剖析,在这个进程中,android客户端只是其间的一个小角色,可是咱们也试图以小窥大,测验从咱们推导出整个数据剖析的全流程版图。

其实数据剖析的全流程能够划分为几个要害的节点:

  1. 数据收集。
  2. 数据传输。
  3. 数据清洗与存储。
  4. 数据可视化展示。
  5. 数据剖析。

下面讲下我对每个节点的理解。

1.数据收集

首先是要先经过埋点产生数据,那么业界也有许多种埋点的技术手段,以应对不同的场景。

手动埋点

这个咱们公司有用到友盟的产品,这个就很好理解了,便是在咱们需求记载信息的地方,调用一个埋点api,把需求记载的信息存储起来,留到后边运用。
这个计划的优点是能够记载到最全的信息,并且关于一些要害信息,比方支付场景如订单金额,折扣状况,每个用户都不相同。并且这个计划也是最灵敏的,想记哪里记哪里。
这个计划的害处便是每次都需求加,并且只能开发加,本钱就高了,并且还需求保护。
这个计划肯定是有其价值的,并且不只客户端用,服务端更要大大的用,因为服务端能够记载的信息更多,比方数据库中的一些要害信息,服务端日志库也有许多老练的,比方log4j等等,也有第三方的能够运用。

可视化埋点

这个计划也很好理解,便是把符号埋点的工效果开发转移到相似产品,运营去做。相似一些低代码,可视化组装页面等的思路。
这儿说下基本的原理和进程:

  1. 客户端在页面加载后(如onCreate之后会初始化DecorView),经过解析控件树的办法将页面view传输到服务端。
  2. 服务端拿到数据后,前端将数据反解析,在web页面重新渲染出虚拟手机的界面。
  3. 产品或运营对界面上的元素进行手动符号,即埋点的进程。埋点数据会存储到服务端。
  4. 客户端会在下一次运转时读取埋点的装备信息,在用户点击相应控件时触发埋点事情上报。

关于可视化埋点的详细细节能够参阅以下资料:

全埋点

全埋点也叫无埋点,这种计划是在运用运转的特定场景主动收集埋点,这种办法的主动化程度最高,无需进行手动埋点,但这也造成了这种计划的灵敏性较低,只能收集一些简略的信息数据,无法收集到更细节的事务数据。
一般全埋点的运用场景有:

  1. 运用启动,退出(crash,主动退出,lmk强杀等)。
  2. 运用页面生命周期记载。
  3. 运用页面控件点击。
  4. 功用数据:cpu,内存,帧率,网络恳求质量等。

咱们后边要点剖析的便是这个计划中的运用页面控件点击事情收集,详见后边神策埋点开源项目剖析部分。

2.数据传输

埋点数据格式

经过前一步现已收集到了相应的数据埋点,这儿简略讲下埋点的数据格式:

  • 默认字段:收集的一些通用字段,如设备id或cookie(用来标识仅有用户),设备的型号,操作体系的版本号等。
  • 事情字段:最开始是事情的key,后边是事情的信息字段,这儿不同事情也能够抽取一些公共字段出来,便利后端进行一致的数据处理。

关于存储的数据格式,这儿有几点需求留意:

  1. 埋点的数据格式对应于数据剖析的Event+User模型,这样能够依照不同的用户和事情维度进行下钻。
  2. 关于不同端的同一用户,对应的用户id标识或许不同,这儿咱们选用业界通用的ID-mapping进行全端用户打通。

埋点数据存储

埋点数据的存储其实也是很有考究,因为咱们产生的数据量是十分大的,像咱们安卓运用的数据每天产生约1.2亿条日志数据,需求对数据做一些优化,不然关于存储和传输压力都比较大。

埋点数据存储有两种计划:

  • 数据库(sqlite)。
    这种计划合适数据量比较小的状况,因为数据库存储的本钱仍是比较高的,比方友盟这种手动埋点的计划一般将数据存入数据库中。 存入数据库中的一个优点是数据的处理比较灵敏,能够自由挑选相关的战略进行上传,比方上传某一时间段的数据,上传失利后也能够针对失利的数据进行回滚。
  • 文件。
    这种计划合适数据量比较大的状况,能够将日志数据进行压缩加密后依照二进制存储获得更高的压缩率。这儿一般会选用一些高功用日志的计划,能够自己完成,也能够参阅一些开源的计划。这儿咱们公司选用的是二次开发wechat的xlog。
    选用文件存储的一个害处是上传或许不那么灵敏,因为按文件维度要比按数据库维度要大,这儿咱们尽量将文件切小分片上传,这样失利后也便利进行重传。

埋点数据传输

埋点数据传输基本上选用https协议栈即可,在上传时留意尽量不要运用运用基础网络库组件,避免日志记载和上传循环调用。另外能够约好一些上传的战略,比方前后台上传的间隔能够不相同,尽量平衡功用和上传时效的平衡。

3. 数据清洗与存储。

这儿首要是客户端上传数据后,服务端所做的一些作业,这一部分我没有过多的研究,在这儿略述一二。

  • 数据接收:运用nginx等服务器接收。
  • 数据流处理:kafka等,作为数据接入和数据处理两个流程之间的缓冲。
  • 数据存储:运用hdfs或clickhouse等。
  • 数据查询:首要是运用sql进行查询,能够辅佐开发一些前台看板来可视化查询,咱们运用的是grafana。

4. 数据可视化展示。

可视化展示没有很固定的思路,首要便是画图,满意咱们后边数据剖析的需求或是满意可观测性,能够让咱们主动化进行事务ops即可。
这儿我运用运用全埋点相关的场景举几个比如,代码相关的解析详见后边神策埋点开源项目剖析部分。

  • 运用控件点击收集。
    将收集到的运用控件点击数据,以页面为维度进行聚类,简略的能够绘制页面控件点击百分比扇形图,杂乱的能够参阅前面可视化埋点的思路,绘制页面区域点击数热力求,这样产品或运转能够依据不同区域点击的占比调整要害事务的方位。

  • 运用页面生命周期事情收集。
    能够将一个事务的页面流通绘制成桑基图办法展现,以方针事情为起点结尾进行用户行为路径剖析,能够对事务的转化进行剖析改进。

5.数据剖析

这儿我觉得是埋点数据真实起效果的地方,只有对数据进行剖析,才干对事务产生正向反应,但这一部分也恰恰是我的短板,这儿共享我在学习数据剖析进程中的一些总结。
这儿的数据剖析既包括传统基于统计学的一些数据剖析办法,也有基于用户画像的引荐,机器学习和ai的一些新式数据剖析办法。

  • 行为事情剖析模型
    这个模型是最靠近咱们收集的原始数据的模型,因为咱们收集的单条数据便是一个Event。
    行为事情剖析一般有以下几个阶段。
  1. 事情界说与挑选。
    收集信息包括:who(经过userId,设备id等标识仅有用户),when(记载事情产生的时间戳),where(事情产生的地点,这个能够是客户端将用户ip上报,服务端进行地址库解析),how(产生事情的来源,如端标识等),what(记载事情的key和事情数据)。
  2. 多维度下钻剖析。
    这儿咱们的事情剖析体系要支撑恣意下钻剖析和精细化条件筛选。
  3. 解说与定论。
  • 漏斗剖析模型
    该模型能够反映用户行为状况以及从起点到结尾各阶段用户转化率状况。
    能够经过比照不同条件下的转化率,以及进行a/b test进行线上的转化率调优测验。
  • 留存剖析模型
  • 用户路径剖析模型
    这个和漏斗剖析模型比较像,只是它的分布是多对多的。
  • 分群剖析模型
    该模型区别于传统数据剖析模型,选用用户画像等算法对用户进行聚类,进行精细化的推送和召回等操作。

把握了上述的事情剖析模型,进行简略的数据剖析时,能够直接运用体系进行可视化的查询,但在进行一些杂乱的数据查询或绘制看板时,作为一个android客户端开发,也需求把握必定的sql才能。

神策埋点开源项目剖析

埋点sdk:github.com/sensorsdata…
埋点插件sdk:github.com/sensorsdata…

这儿咱们或许需求一些编写gradle插件的前置基础常识,大多数读者应该都已把握。需求留意的是gradle在7.3之后将transform修改class的办法符号为过时,这儿咱们需求用gradle供给的新api,在杂乱状况下,咱们或许还需求运用task来辅佐解决。

class V73Impl(project: Project, override val asmWrapperFactory: AsmCompatFactory) :
    AGPCompatInterface {
    init {
        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
        V73AGPContextImpl.asmCompatFactory = asmWrapperFactory
        androidComponents.onVariants { variant: Variant ->
            variant.instrumentation.transformClassesWith(
                SensorsDataAsmClassVisitorFactory::class.java,
                InstrumentationScope.ALL
            ) {
                ...
            }
            variant.instrumentation
                .setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
        }
    }
}

能够看出来,新的api也是固定套路,先获取到AndroidComponentsExtension这个扩展,然后在每个变体上注册一个转化类,这儿是SensorsDataAsmClassVisitorFactory,最后的lambda块里能够注入一些装备。
InstrumentationScope.ALL表明咱们剖析的代码是整个工程的,包括自己的工程和第三方的库。

这儿咱们或许还需求一些asm的基础常识,读者肯定也学习过了,那么接着往下看:

ClassVisitor:

abstract class SensorsDataAsmClassVisitorFactory :
    AsmClassVisitorFactory<ConfigInstrumentParams> {
    override fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor {
        V73AGPContextImpl.asmCompatFactory!!.onBeforeTransform()
        val classInheritance = object : ClassInheritance {
            override fun isAssignableFrom(subClass: String, superClass: String): Boolean {
                return classContext.loadClassData(subClass)?.let {
                    it.className == superClass || it.superClasses.contains(superClass) || it.interfaces.contains(superClass)
                } ?: false
            }
            override fun loadClass(className: String): ClassInfo? {
                return classContext.loadClassData(className)?.let {
                    ClassInfo(
                        it.className,
                        interfaces = it.interfaces,
                        superClasses = it.superClasses
                    )
                }
            }
        }
        return V73AGPContextImpl.asmCompatFactory!!.transform(
            nextClassVisitor, classInheritance
        )
    }
    override fun isInstrumentable(classData: ClassData): Boolean {
        return V73AGPContextImpl.asmCompatFactory!!.isInstrumentable(
            ClassInfo(
                classData.className,
                interfaces = classData.interfaces,
                superClasses = classData.superClasses
            )
        )
    }
}

完成AsmClassVisitorFactory接口即可,能够看到关于asm的兼容仍是十分便利的,只需求完成详细埋点的ClassVisitor即可。关于该项目是SAPrimaryClassVisitor。

按次序剖析一下SAPrimaryClassVisitor。

访问类

override fun visit(
        version: Int,
        access: Int,
        name: String,
        signature: String?,
        superName: String?,
        interfaces: Array<out String>?
    ) {
        super.visit(version, access, name, signature, superName, interfaces)
        classNameAnalytics = ClassNameAnalytics(name, superName, interfaces?.asList())
        shouldReturnJSRAdapter = version <= Opcodes.V1_5
        configHookHelper.initConfigCellInClass(name)
    }

类相关的元信息存储在classNameAnalytics中。
SAConfigHookHelper是插件供给的能够经过装备删去一些办法调用的功用。

类中办法调用
override fun visitMethod(
        access: Int,
        name: String?,
        descriptor: String?,
        signature: String?,
        exceptions: Array<String>?
    ): MethodVisitor? { 
        ...
        //创建一系列MethodVisitor
    }
//check whether need to delete this method. if the method is deleted,
//a new method will be created at visitEnd()
if (configHookHelper.isConfigsMethod(name, descriptor)) {
    return null
}

命中装备,有需求删去的办法调用,记载到SAConfigHookHelper的mHookMethodCells里。

if (classNameAnalytics.superClass == "android/app/Activity"
    && name == "onNewIntent" && descriptor == "(Landroid/content/Intent;)V"
) {
    isFoundOnNewIntent = true
}

命中onNewIntent办法。

下面便是创建对应的MethodVisitor,这个稍后剖析。

访问类完毕
override fun visitEnd() {
        super.visitEnd()
        //给 Activity 增加 onNewIntent,满意 push 事务需求
        if (pluginManager.isModuleEnable(SAModule.PUSH)
            && !isFoundOnNewIntent
            && classNameAnalytics.superClass == "android/app/Activity"
        ) {
            SensorsPushInjected.addOnNewIntent(classVisitor)
        }
        //为 Fragment 增加办法,满意生命周期界说
        if (pluginManager.isModuleEnable(SAModule.AUTOTRACK)) {
            FragmentHookHelper.hookFragment(
                classVisitor,
                classNameAnalytics.superClass,
                visitedFragMethods
            )
        }
        //增加需求置空的办法
        configHookHelper.disableIdentifierMethod(classVisitor)
    }

这儿做了三件事:
1.如果Activity没有完成onNewIntent办法,给 Activity 增加 onNewIntent办法。

 fun addOnNewIntent(classVisitor: ClassVisitor) {
        val mv = classVisitor.visitMethod(
            Opcodes.ACC_PROTECTED,
            "onNewIntent",
            "(Landroid/content/Intent;)V",
            null,
            null
        )
        mv.visitAnnotation("Lcom/sensorsdata/analytics/android/sdk/SensorsDataInstrumented;", false)
        mv.visitCode()
        mv.visitVarInsn(Opcodes.ALOAD, 0)
        mv.visitVarInsn(Opcodes.ALOAD, 1)
        mv.visitMethodInsn(
            Opcodes.INVOKESPECIAL,
            "android/app/Activity",
            "onNewIntent",
            "(Landroid/content/Intent;)V",
            false
        )
        mv.visitVarInsn(Opcodes.ALOAD, 0)
        mv.visitVarInsn(Opcodes.ALOAD, 1)
        mv.visitMethodInsn(
            Opcodes.INVOKESTATIC,
            PUSH_TRACK_OWNER,
            "onNewIntent",
            "(Ljava/lang/Object;Landroid/content/Intent;)V",
            false
        )
        mv.visitInsn(Opcodes.RETURN)
        mv.visitMaxs(2, 2)
        mv.visitEnd()
    }

经过这段逻辑能够学习怎样新增办法。

2.Fragment生命周期办法插桩
首要是在Fragment生命周期中刺进FragmentTrackHelper的相关调用。
这儿有一个小技巧是,结构将办法的调用封装了一下,就不必写许多模板指令办法了。

// call super
methodCell.visitMethod(mv, Opcodes.INVOKESPECIAL, superName!!)
// call injected method
methodCell.visitHookMethod(
                    mv,
                    Opcodes.INVOKESTATIC,
                    SensorsFragmentHookConfig.SENSORS_FRAGMENT_TRACK_HELPER_API
)

这儿将调用super和插桩办法封装了起来。

3.清空记载到SAConfigHookHelper的mHookMethodCells里的办法体。

办法内插桩(点击事情插桩完成)

咱们重视的点击事情插桩仍是在办法体的访问中,仍是回到visitMethod办法中,这儿创建了一系列嵌套的MethodVisitor,咱们从外向内剖析。

MethodVisitor的调用次序如下:

(visitParameter)*
[visitAnnotationDefault]
(visitAnnotation | visitAnnotableParameterCount | visitParameterAnnotation | visitTypeAnnotation | visitAttribute)*
[    visitCode    (        visitFrame |        visitXxxInsn |        visitLabel |        visitInsnAnnotation |        visitTryCatchBlock |        visitTryCatchAnnotation |        visitLocalVariable |        visitLocalVariableAnnotation |        visitLineNumber    )*    visitMaxs]
visitEnd
1.UpdateSDKPluginVersionMV
override fun visitFieldInsn(opcode: Int, owner: String, fieldName: String, descriptor: String) {
        if (mClassNameAnalytics.isSensorsDataAPI && "ANDROID_PLUGIN_VERSION" == fieldName && opcode == PUTSTATIC) {
            mMethodVisitor.visitLdcInsn(VersionConstant.VERSION)
        }
        super.visitFieldInsn(opcode, owner, fieldName, descriptor)
    }

这个类的效果是当运用设置SensorsDataAPI的ANDROID_PLUGIN_VERSION字段时,将当时版本号放在操作数栈顶,再履行该指令,即完成了替换。

2.SensorsAutoTrackMethodVisitor

这个类真实完成了点击事情插桩的功用,是咱们要点剖析的目标。

class SensorsAutoTrackMethodVisitor(
    mv: MethodVisitor,
    methodAccess: Int,
    methodName: String,
    var desc: String,
    private val classNameAnalytics: ClassNameAnalytics,
    private val visitedFragMethods: MutableSet<String>,
    lambdaMethodCells: MutableMap<String, SensorsAnalyticsMethodCell>,
    private val pluginManager: SAPluginManager
) : AdviceAdapter(
    pluginManager.getASMVersion(), mv,
    methodAccess,
    methodName,
    desc
)

能够看到这儿继承的是AdviceAdapter,在前面说的调用次序之外,还增加了两个办法:

public override fun onMethodEnter() {}
public override fun onMethodExit(opcode: Int) {}

这两个办法分别在办法调用的开始和完毕调用,便利咱们织入自己的自界说代码。
咱们依照asm遍历的次序去解析这个类。

(1)遍历注解
override fun visitAnnotation(s: String, b: Boolean): AnnotationVisitor {
        if (s == "Lcom/sensorsdata/analytics/android/sdk/SensorsDataTrackViewOnClick;") {
            isSensorsDataTrackViewOnClickAnnotation = true
        } else if (s == "Lcom/sensorsdata/analytics/android/sdk/SensorsDataIgnoreTrackOnClick;") {
            isSensorsDataIgnoreTrackOnClick = true
        } else if (s == "Lcom/sensorsdata/analytics/android/sdk/SensorsDataInstrumented;") {
            isHasInstrumented = true
        } else if (s == "Lcom/sensorsdata/analytics/android/sdk/SensorsDataTrackEvent;") {
            return object : AnnotationVisitor(pluginManager.getASMVersion()) {
                override fun visit(key: String, value: Any) {
                    super.visit(key, value)
                    if ("eventName" == key) {
                        eventName = value as String
                    } else if ("properties" == key) {
                        eventProperties = value.toString()
                    }
                }
            }
        }
        return super.visitAnnotation(s, b)
    }

因为咱们辨认点击的首要战略是查找调用setOnClickListener办法,而运用一些结构时,或许不叫这个名字,比方运用butterknife,databinding时,需求手动在这些点击办法上进行注解,这样才干被辨认插桩。
这儿将注解中的信息提取出来。

(2)办法入口点
public override fun onMethodEnter() {
        super.onMethodEnter()
        pubAndNoStaticAccess =
            SAUtils.isPublic(access) && !SAUtils.isStatic(
                access
            )
        protectedAndNotStaticAccess =
            SAUtils.isProtected(access) && !SAUtils.isStatic(
                access
            )
        if (pubAndNoStaticAccess) {
            if (nameDesc == "onClick(Landroid/view/View;)V") {
                isOnClickMethod = true
                variableID = newLocal(Type.getObjectType("java/lang/Integer"))
                mMethodVisitor.visitVarInsn(ALOAD, 1)
                mMethodVisitor.visitVarInsn(ASTORE, variableID)
            } else { ... }
        } else if (protectedAndNotStaticAccess) {
            if (nameDesc == "onListItemClick(Landroid/widget/ListView;Landroid/view/View;IJ)V") {
                localIds = ArrayList()
                val firstLocalId = newLocal(Type.getObjectType("java/lang/Object"))
                mMethodVisitor.visitVarInsn(ALOAD, 1)
                mMethodVisitor.visitVarInsn(ASTORE, firstLocalId)
                localIds!!.add(firstLocalId)
                val secondLocalId = newLocal(Type.getObjectType("android/view/View"))
                mMethodVisitor.visitVarInsn(ALOAD, 2)
                mMethodVisitor.visitVarInsn(ASTORE, secondLocalId)
                localIds!!.add(secondLocalId)
                val thirdLocalId = newLocal(Type.INT_TYPE)
                mMethodVisitor.visitVarInsn(ILOAD, 3)
                mMethodVisitor.visitVarInsn(ISTORE, thirdLocalId)
                localIds!!.add(thirdLocalId)
            }
        }
        ...
        if (pluginManager.isHookOnMethodEnter) {
            handleCode()
        }
    }

对办法调用中,和点击事情相关的进行处理,例如最普遍的界说一个点击事情:

private void initButton() {
        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
            }
        });
    }

这儿在public,nostatic分支中,找到onClick办法,这儿插件将View参数存放到一个新开辟的局部变量表空间中。

isOnClickMethod = true
variableID = newLocal(Type.getObjectType("java/lang/Integer"))
mMethodVisitor.visitVarInsn(ALOAD, 1)
mMethodVisitor.visitVarInsn(ASTORE, variableID)

其他分支的处理都大同小异,相似匹配AdapterView的item点击事情。
因为在实践的项目中,屡次遇到参数类型被优化的现象,所以采取的办法是在 onMethodEnter 的时分进行相关参数的保存,以便刺进代码的时分正确读取运用。

这儿需求要点重视一种状况,lambda关于办法插桩的影响。

D8/R8会对lambda语法进行脱糖处理,这儿看一个java lambda表达式的比如:
原始代码:

public class Java8 {
  interface Logger {
   void log(String s);
 }
  public static void main(String... args) {
   test(s -> System.out.println(s))
 }
  private static void test(Logger logger) {
   logger.log("hello")
 }
}

脱糖后的代码:

public class Java8 {
interface Logger {
void log(String s);
}
public static void main(String... args) {
test(s -> new Java8$1())
}
  //办法体中的内容移到这儿  
  static void lambda$main$0(String str) {
  System.out.println(str)
 }  
private static void test(Logger logger) {
logger.log("hello")
}
}
public class Java8$1 implements Java8.Logger {
  public Java8$1() {} 
  @Override
  public void log(String s) {
    Java8.lambda$main$0(s);
 }
}

脱糖后,生成了一个完成该接口的类,类调用的办法体为lambda块内的代码。
能够看出,lambdamainmain0是一个在运转时生成的办法,在编译时是不存在的,对应的字节码是invokedynamic。

invokedynamic指令

invokedynamic指令在jdk7引入,用于完成动态类型言语功用。
和该指令相关的jdk类有:

  1. MethodType
public static MethodType methodType(Class<?> rtype, Class<?>[] ptypes) {
    return makeImpl(rtype, ptypes, false);
}

MethodType代表一个办法所需的返回值类型和所有参数类型。

  1. MethodHandle

MethodHandle是办法句柄,MethodHandle依据类名,办法名,以及MethodType查找到特定办法并履行。

@RequiresApi(api = Build.VERSION_CODES.O)
    public void foo(Context context) {
        try {
            MethodType methodType = MethodType.methodType(String.class, int.class);
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            MethodHandle methodHandle = lookup.findStatic(String.class, "valueOf", methodType);
            String result = (String) methodHandle.invoke(99);
            ToastUtil.showLong(context, result);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

这儿供给一个经过MethodHandle动态履行String.valueOf办法的比如。

  1. CallSite

CallSite是办法调用点,调用点中包括了办法句柄信息,CallSite对MethodHandle进行链接,这或许有些笼统。

下面看一个invokedynamic的比如:

import java.util.Date;
import java.util.function.Consumer;
public class TestLambda {
    public void test() {
        final Date date = new Date();
        Consumer<String> consumer = s -> {
            System.out.println(s+ date);
        };
    }
}

运用javap调查字节码:

public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class java/util/Date
         3: dup
         4: invokespecial #3                  // Method java/util/Date."<init>":()V
         7: astore_1
         8: aload_1
         9: invokedynamic #4,  0              // InvokeDynamic #0:accept:(Ljava/util/Date;)Ljava/util/function/Consumer;
        14: astore_2
        15: return
      LineNumberTable:
        line 9: 0
        line 10: 8
        line 13: 15

这儿调用了invokedynamic指令,0是预留字段,#4是常量池字段。

 #4 = InvokeDynamic      #0:#30         // #0:accept:(Ljava/util/Date;)Ljava/util/function/Consumer;

这儿的#0表明是第一个引导办法,引导办法指向lambda脱糖后的代码,该办法是运转时动态生成的:

BootstrapMethods:
  0: #26 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #27 (Ljava/lang/Object;)V
      #28 invokestatic com/sensorsdata/sdk/demo/TestLambda.lambda$test$0:(Ljava/util/Date;Ljava/lang/String;)V
      #29 (Ljava/lang/String;)V

实践调用的是LambdaMetafactory.metafactory办法。

public static CallSite metafactory(MethodHandles.Lookup caller,
                                       String invokedName,
                                       MethodType invokedType,
                                       MethodType samMethodType,
                                       MethodHandle implMethod,
                                       MethodType instantiatedMethodType)

这儿要点看后三个参数,前三个是固定体系生成的:

    Method arguments:
      #27 (Ljava/lang/Object;)V
      #28 invokestatic com/sensorsdata/sdk/demo/TestLambda.lambda$test$0:(Ljava/util/Date;Ljava/lang/String;)V
      #29 (Ljava/lang/String;)V

samMethodType:函数式接口中笼统办法的签名信息,这儿对应Consumer接口的accept办法,因为泛型参数擦除,这儿是Object。
implMethod:脱糖后实践生成的静态办法,能够看到,这儿生成的办法有两个参数,这是因为lambda引用了lambda块外的final变量date。

  private static void lambda$test$0(java.util.Date, java.lang.String);
    descriptor: (Ljava/util/Date;Ljava/lang/String;)V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=3, locals=2, args_size=2
         0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #6                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
        10: aload_1
        11: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        14: aload_0
        15: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
        18: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        21: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        24: return

instantiatedMethodType:samMethodType的实践类型,这儿泛型参数还原为String。

invokedynamic插桩

有了上述的基础常识,咱们能够对invokedynamic插桩进行剖析。
运用一个简略的点击事情为样本进行剖析:

private void initLambdaButton() {
    Button button = (Button) findViewById(R.id.lambdaButton);
    button.setOnClickListener(v -> {
    });
}

结构预先增加了一个点击事情的lambda装备:

addLambdaMethod(
    SensorsAnalyticsMethodCell(
        "onClick",
        "(Landroid/view/View;)V",
        "Landroid/view/View$OnClickListener;",
        "trackViewOnClick",
        "(Landroid/view/View;)V",
        1, 1,
        listOf(Opcodes.ALOAD)
    )
)

之后对invokedynamic指令进行拦截:

override fun visitInvokeDynamicInsn(
    name1: String,
    desc1: String,
    bsm: Handle,
    vararg bsmArgs: Any
) {
    super.visitInvokeDynamicInsn(name1, desc1, bsm, *bsmArgs)
    if (!pluginManager.extension.lambdaEnabled) {
        return
    }
    try {
        val owner = bsm.owner
        if ("java/lang/invoke/LambdaMetafactory" != owner) {
            return
        }
        val desc2 = (bsmArgs[0] as Type).descriptor
        val sensorsAnalyticsMethodCell: SensorsAnalyticsMethodCell? =
            SensorsAnalyticsHookConfig.LAMBDA_METHODS.get(
                Type.getReturnType(desc1).descriptor + name1 + desc2
            )
        if (sensorsAnalyticsMethodCell != null) {
            val it = bsmArgs[1] as Handle
            mLambdaMethodCells[it.name + it.desc] = sensorsAnalyticsMethodCell
        }
    } catch (e: Exception) {
        warn("Some exception happened when call visitInvokeDynamicInsn: className: " + classNameAnalytics.className + ", error message: " + e.localizedMessage)
    }
}

这个办法的效果是生成mLambdaMethodCells这个map,它的key是脱糖后生成办法的name+desc,value是咱们上面提前预埋的lambda装备办法SensorsAnalyticsMethodCell。
为了看懂这个办法,需求用到上面提到的lambda及invokedynamic指令的常识,这个办法的name1和desc1指代invokedynamic指令的第一个参数,bsm是引导办法metafactory相关的实例,bsmArgs是引导办法相关参数,即咱们前面剖析的samMethodType,implMethod和instantiatedMethodType。

(3)办法出口点

在办法出口点,进行真实的插桩功用完成。

public override fun onMethodExit(opcode: Int) {
    super.onMethodExit(opcode)
    if (!pluginManager.isHookOnMethodEnter) {
        handleCode()
    }
}

能够看到,首要的逻辑在handleCode中。

功用1:对Fragment相关办法调用插桩。
在ClassVisitor中,关于Fragment没有完成的办法进行了生成和插桩,如果该类现已完成了Fragment的相关办法,只需求刺进埋点调用即可。

if (SAPackageManager.isInstanceOfFragment(classNameAnalytics.superClass)) {
            val sensorsAnalyticsMethodCell: SensorsAnalyticsMethodCell? =
                SensorsFragmentHookConfig.FRAGMENT_METHODS[nameDesc]
            if (sensorsAnalyticsMethodCell != null) {
                visitedFragMethods.add(nameDesc)
//                mMethodVisitor.visitVarInsn(ALOAD, 0)
                for (i in 0 until sensorsAnalyticsMethodCell.paramsCount) {
                    mMethodVisitor.visitVarInsn(
                        sensorsAnalyticsMethodCell.opcodes[i],
                        localIds!![i]
                    )
                }
                mMethodVisitor.visitMethodInsn(
                    INVOKESTATIC,
                    SensorsFragmentHookConfig.SENSORS_FRAGMENT_TRACK_HELPER_API,
                    sensorsAnalyticsMethodCell.agentName,
                    sensorsAnalyticsMethodCell.agentDesc,
                    false
                )
                isHasTracked = true
                return
            }
        }

这儿咱们刺进FragmentTrackHelper的对应办法。

功用2:对lambda调用进行处理。
前面关于lambda表达式的处理讲了一大篇,总算到了收成的时分了。

val lambdaMethodCell: SensorsAnalyticsMethodCell? = mLambdaMethodCells[nameDesc]

开始的代码看上去比较懵逼,前面存入mLambdaMethodCells的不是生成脱糖办法的namedesc吗,这儿取的时分怎样又用当时办法的nameDesc去取了?
其实是咱们剖析的维度错了,这儿现已来到脱糖办法的methodVisitor中的,而mLambdaMethodCells是在ClassVisitor维度共享的。前面做了那么多衬托,本来是为了拦截到真实脱糖办法的调用,不然脱糖办法的名字是虚拟机生成的,咱们无法知道哪个办法承载了lambda的点击调用。

for (i in paramStart until paramStart + lambdaMethodCell.paramsCount) {
    mMethodVisitor.visitVarInsn(
        lambdaMethodCell.opcodes.get(i - paramStart),
        localIds!![i - paramStart]
    )
}

先将插桩办法需求用到的变量从局部变量表加载到操作数栈。

mMethodVisitor.visitMethodInsn(
    INVOKESTATIC,
    SensorsAnalyticsHookConfig.SENSORS_ANALYTICS_API,
    lambdaMethodCell.agentName,
    lambdaMethodCell.agentDesc,
    false
)

加载了参数之后,调用SensorsDataAutoTrackHelper的trackViewOnClick办法。

功用3:关于Android Tv的特别处理。

if (isAndroidTv && SAPackageManager.isInstanceOfActivity(classNameAnalytics.superClass) && nameDesc == "dispatchKeyEvent(Landroid/view/KeyEvent;)Z") {
    mMethodVisitor.visitVarInsn(ALOAD, 0)
    mMethodVisitor.visitVarInsn(ALOAD, 1)
    mMethodVisitor.visitMethodInsn(
        INVOKESTATIC,
        SensorsAnalyticsHookConfig.SENSORS_ANALYTICS_API,
        "trackViewOnClick",
        "(Landroid/app/Activity;Landroid/view/KeyEvent;)V",
        false
    )
    isHasTracked = true
    return
}

因为android电视具有实体按键,因而需求额外拦截dispatchKeyEvent事情并插桩。

功用4:对点击事情进行处理。

这儿总算来到了我开始的本意,学习点击事情的插桩办法。

if (isOnClickMethod && classNameAnalytics.className == "android/databinding/generated/callback/OnClickListener") {
    trackViewOnClick(mMethodVisitor, 1)
    isHasTracked = true
    return
}

对databinding的点击事情进行插桩埋点。

if (isSensorsDataTrackViewOnClickAnnotation && desc == "(Landroid/view/View;)V") {
    trackViewOnClick(mMethodVisitor, 1)
    isHasTracked = true
    return
}

对运用第三方结构,运用注解标示的办法进行插桩埋点。

if (isOnClickMethod) {
    trackViewOnClick(mMethodVisitor, variableID)
    isHasTracked = true
}

运用最惯例匿名内部类click调用办法的插桩埋点。

private fun trackViewOnClick(mv: MethodVisitor, index: Int) {
    mv.visitVarInsn(ALOAD, index)
    mv.visitMethodInsn(
        INVOKESTATIC,
        SensorsAnalyticsHookConfig.SENSORS_ANALYTICS_API,
        "trackViewOnClick",
        "(Landroid/view/View;)V",
        false
    )
}

学了这么长期的asm,十分简略,不解说了。

(4)办法遍历完毕

override fun visitEnd() {
    super.visitEnd()
    if (isHasTracked) {
        if (pluginManager.extension.lambdaEnabled) {
            mLambdaMethodCells.remove(nameDesc)
        }
        visitAnnotation(
            "Lcom/sensorsdata/analytics/android/sdk/SensorsDataInstrumented;",
            false
        )
    }
}

做一些收回作业。

3. SensorsAnalyticsPushMethodVisitor

对push进行支撑,这儿首要是注入PushAutoTrackHelper相关的api,因为篇幅原因这儿不过多赘述。

4. SensorsAnalyticsWebViewMethodVisitor

对WebView的特别处理,这儿首要是将h5和app进行打通。这儿的打通是指,客户端内嵌的h5页面数据一致转发到native侧,由客户端一致进行埋点上报。打通能够一致运用客户端数据存储和传输才能,降低埋点数据的丢掉率,另外也是咱们ID-mapping功用的一部分,完成一致的用户id标识。
这个MethodVisitor的效果是主动进行JsBridge的注入。

//将局部变量表中的数据压入操作数栈中触发咱们需求刺进的办法
positionList.reversed().forEach { tmp ->
    loadLocal(tmp)
}
val newDesc = SAUtils.appendDescBeforeGiven(desc, VIEW_DESC)
mv.visitMethodInsn(INVOKESTATIC, JS_BRIDGE_API, name, newDesc, false)

将WebView的相关调用替换为JSHookAop的相关调用。
比方WebView的loadUrl办法被替换为JSHookAop的loadUrl办法。

loadUrl办法调用setupH5Bridge办法。

private static void setupH5Bridge(View webView) {
    if (isSupportJellyBean() && SensorsDataAPI.getConfigOptions() != null &&
            SensorsDataAPI.getConfigOptions().isAutoTrackWebView()) {
        setupWebView(webView);
    }
    if (isSupportJellyBean()) {
        SAModuleManager.getInstance().invokeModuleFunction(Modules.Visual.MODULE_NAME, Modules.Visual.METHOD_ADD_VISUAL_JAVASCRIPTINTERFACE, webView);
    }
}

进行jsBridge的注入:

private static void setupWebView(View webView) {
    if (webView != null && webView.getTag(com.sensorsdata.analytics.android.sdk.R.id.sensors_analytics_tag_view_webview) == null) {
        webView.setTag(com.sensorsdata.analytics.android.sdk.R.id.sensors_analytics_tag_view_webview, new Object());
        H5Helper.addJavascriptInterface(webView, new AppWebViewInterface(webView.getContext().getApplicationContext(), null, false, webView), "SensorsData_APP_New_H5_Bridge");
    }
}

总结:

到这儿,整个埋点插件就全剖析完了,能够看到不只仅有咱们开始想了解的点击事情插桩,还有关于lambda的兼容,fragment生命周期事情插桩等诸多功用,一个完善的sdk仍是要比完成一个demo杂乱的多。