ASM 无痕埋点

2021-08-09  本文已影响0人  馒Care

ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

ASM 能够通过改造既有类,直接生成需要的代码。增强的代码是硬编码在新生成的类文件内部的,没有反射带来性能上的付出。同时,ASM 与 Proxy 编程不同,不需要为增强代码而新定义一个接口,生成的代码可以覆盖原来的类,或者是原始类的子类。它是一个普通的 Java 类而不是 proxy 类,甚至可以在应用程序的类框架中拥有自己的位置,派生自己的子类。

2、字节码介绍

源代码中的各种变量,关键字和运算符号的语义最终都会编译成多条字节码命令。而字节码命令所能提供的语义描述能力是要明显强于Java本身的,所以有其他一些同样基于JVM的语言能提供许多Java所不支持的语言特性。


image.png

3、插件开发

(1)、可以创建一个plugin插件,来做字节码修改,以下是插件的目录:

  groovy目录,一般就是存放代码的目录,代码一般插件用groovy语言来编写,也可以用java,或者kotlin,因为他们最终都是变成字节码文件,可以互相兼容的,这里更推荐用java来写,groovy主要是不熟悉,kotlin的话会有很多意想不到的坑出现。

  resources目录的话,是用来存放plugin的命名等信息的,用来指定到具体的插件名字
image.png

(2)、创建插件主入口,一个plugin必须实现Plugin<Project>这个接口,否则此插件就不可用。

image.png

4、埋点

我们的埋点,主要是针对点击位,来做的,一般的点击位有,那我们需要埋点的地方有各个控件的click事件,比如text,button等。

一个textview可以有多种方式 去设置点击方式,比如setOnClick,xml中,rxview等,或者数据绑定的方式。在我们的项目中,通常我们设置一个控件的点击事件,我们可以这么设置


image.png

追踪到具体的binding中,可以发现里面其实是用rxview来做的绑定

image.png

接下来看rxview中做了什么事,实际上,他也是拿到view,去做了相对应的click事件

源码.png

5、因此,如果我们要把相对应的埋点入口埋入到这边的话,我们可以通过asm字节码来实现编译时注入代码。

在字节码中,方法通常会有几个属性,方法签名,方法名,我们可以通过这些属性,来找到我们所要匹配的方法,并埋入进去。比如下面的onclick方法,就有以下那些参数


image.png

在asm中,会对所有的方法进行扫描,然后在匹配到的方法中,都会进行方法的埋入,以达到埋点的效果,因为这个过程是不会修改到原始的代码逻辑,所以这个也是无侵入式的。

我们可以将一些控件的描述符和签名等信息收集起来,存放到map中,以便在asm扫描的时候,可以直接进行匹配。

上图的traceViewOnClick就是我们所要埋入的方法。以下是具体的代码

public static void trackViewOnClick(View view) {
        try {
            //获取Activity
            Activity activity = AutoTrackUtil.getActivityFromView(view);

            if (isDeBounceTrackForView(view)) {
                return;
            }
            JSONObject properties = new JSONObject();
            // 1、获取当前点击控件的全路径
            String viewPath = AutoTrackUtil.getViewPath(view);
            if (!TextUtils.isEmpty(viewPath)) {
                properties.put(LogConstants.AutoTrack.ELEMENT_VIEWPATH, viewPath);
            }

            // 2、获取Activity的标题名
            if (activity != null) {
                String activityTitle = AutoTrackUtil.getActivityTitle(activity);
                if (!TextUtils.isEmpty(activityTitle)) {
                    properties.put(LogConstants.AutoTrack.EVENT_SCAN_PAGE_TITLE, activityTitle);
                }
            }

            // 3、获取当前页面
            String screenName = activity.getClass().getSimpleName();
            if (!TextUtils.isEmpty(screenName)) {
                properties.put(LogConstants.AutoTrack.SCREEN_NAME, screenName);
            }

            // 4、获取ExpandableListView的控件名:ViewId
            String idString = AutoTrackUtil.getViewId(view);
            if (!TextUtils.isEmpty(idString)) {
                properties.put(LogConstants.AutoTrack.ELEMENT_ID, idString);
            }

            // 6、控件的类型
            properties.put(LogConstants.AutoTrack.ELEMENT_TYPE, view.getClass().getSimpleName());

            // 7、获取当前控件内容
            try {
                String viewText;
                if (view instanceof ViewGroup) {
                    StringBuilder stringBuilder = new StringBuilder();
                    viewText = AutoTrackUtil.traverseView(stringBuilder, (ViewGroup) view);
                    if (!TextUtils.isEmpty(viewText)) {
                        viewText = viewText.substring(0, viewText.length() - 1);
                        properties.put(LogConstants.AutoTrack.ELEMENT_CONTENT, viewText);
                    }
                } else {
                    CharSequence viewTextOnly = AutoTrackUtil.traverseViewOnly(view);
                    if (!TextUtils.isEmpty(viewTextOnly) && viewTextOnly != null) {
                        properties.put(LogConstants.AutoTrack.ELEMENT_CONTENT, viewTextOnly.toString());
                    }
                }

            } catch (Exception e) {
                e.printStackTrace();
            }

            // 8、获取 View 自定义属性
            JSONObject p = (JSONObject) view.getTag(R.id.auto_track_tag_view_properties);
            if (p != null) {
                AutoTrackUtil.mergeJsonObject(p, properties);
            }
            //执行上传操作
            VvTrackManager.getInstance().insertVvTrack2FireBaseAuto(properties, currentPageName, previewPageName);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

通过当前的view可以获取到页面信息,包括控件的id,所属的页面等,因此可以直接上传这些数据。

上一篇 下一篇

猜你喜欢

热点阅读