ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
ASM 能够通过改造既有类,直接生成需要的代码。增强的代码是硬编码在新生成的类文件内部的,没有反射带来性能上的付出。同时,ASM 与 Proxy 编程不同,不需要为增强代码而新定义一个接口,生成的代码可以覆盖原来的类,或者是原始类的子类。它是一个普通的 Java 类而不是 proxy 类,甚至可以在应用程序的类框架中拥有自己的位置,派生自己的子类。
2、字节码介绍
源代码中的各种变量,关键字和运算符号的语义最终都会编译成多条字节码命令。而字节码命令所能提供的语义描述能力是要明显强于Java本身的,所以有其他一些同样基于JVM的语言能提供许多Java所不支持的语言特性。

3、插件开发
(1)、可以创建一个plugin插件,来做字节码修改,以下是插件的目录:
groovy目录,一般就是存放代码的目录,代码一般插件用groovy语言来编写,也可以用java,或者kotlin,因为他们最终都是变成字节码文件,可以互相兼容的,这里更推荐用java来写,groovy主要是不熟悉,kotlin的话会有很多意想不到的坑出现。
resources目录的话,是用来存放plugin的命名等信息的,用来指定到具体的插件名字

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

4、埋点
我们的埋点,主要是针对点击位,来做的,一般的点击位有,那我们需要埋点的地方有各个控件的click事件,比如text,button等。
一个textview可以有多种方式 去设置点击方式,比如setOnClick,xml中,rxview等,或者数据绑定的方式。在我们的项目中,通常我们设置一个控件的点击事件,我们可以这么设置

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

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

5、因此,如果我们要把相对应的埋点入口埋入到这边的话,我们可以通过asm字节码来实现编译时注入代码。
在字节码中,方法通常会有几个属性,方法签名,方法名,我们可以通过这些属性,来找到我们所要匹配的方法,并埋入进去。比如下面的onclick方法,就有以下那些参数

在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,所属的页面等,因此可以直接上传这些数据。
网友评论