为什么进行全埋点?
以往手动模式埋点
以往的埋点形式都是人为进行定义名称和选择性埋点,版本迭代屡次后造成埋点数量继续减少。
- 在各个代码块进行基本相同的代码调用,侵入性高,如果前期进行更换 SDK,有可能会进行大量改变
- 手动进行埋点可能导致认为忽略造成的埋点失落
- 只能依据埋点进行用户行为回溯,有些细节和流程无奈连接上,无奈还原用户应用场景
- 每个版本迭代都须要 PM,RD 进行埋点梳理,工夫进行耗费
全埋点
- 无奈在每个按钮,页面加载调用代码,只须要在利用初始化加载即可
- 用户行为触发主动上报,无需 PM 思考应该在哪个页面进行埋点
- 可配置化,能够抉择过滤上报页面,事件,或者特定页面减少属性上报
- 版本迭代不须要从新进行埋点
如何进行?
- 页面操作:Application.ActivityLifecycleCallbacks 接口
public interface ActivityLifecycleCallbacks {void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState);
void onActivityStarted(@NonNull Activity activity);
void onActivityResumed(@NonNull Activity activity);
void onActivityPaused(@NonNull Activity activity);
void onActivityStopped(@NonNull Activity activity);
void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState);
void onActivityDestroyed(@NonNull Activity activity);
}
利用启动完结:AppStart,AppEnd
在 ActivityLifecycleCallbacks 接口中监听 start 和 pause,并应用 SP 和 ContentProvider 进行辅助记录利用的开启工夫和 pause 工夫,如果用户 App 在后盾被强杀或者手动退出,那么下次从新应用 APP 的时候会进行检测 Sp 中的工夫和以后的工夫,而后进行比照,判断用户是否为重新启动 APP,还是仅仅切换到后盾再切换回来。
留神⚠️:start 中进行检测,pause 中进行工夫数据更新。
利用点击控件
计划 1:hook 控件的点击事件接口进行代理
整体思路:依据 ActivityLifecycleCallbacks 接口监听回调,在 onActivityResume 回调中拿到以后的 Activity,而后利用 DecorView 递归遍历所有子 view 进行代理 onClickListener 办法。同时在 Activity 启动的时候进行 ViewTree 的 observer,ViewTree 改变的时候 (比方设置了 view 的不可见不可点击等) 从新进行一遍 hook。
hook:利用反射获取到 View 曾经设置的 onClickListener 对象、区别 view 的对象类型 (button,textView…) 进而设置不同的 listener。
毛病:根本每个 View 或者 Viewgroup 都会有本人的点击事件,并且点击事件接口都为 class 外部的借口,没有顶层的接口进行兼容检测,所以须要做大量的 wrapperListener,工作繁琐反复。此外,每创立一个页面就要进行一次 Hook,性能不高,效率低。
计划 2: 利用 Window 点击的回调
每次点击的事件散发函数——dispatchTouchEvent(MotionEvent event),进行 hook,利用以后 activity 的 RootView 的信息再联合 event 的信息进行埋点。
具体:判断点击的坐标是否位于 view(利用 rootView 循环判断)之中、该 view 是否处于可见状态;
毛病:每次点击都要去遍历一次 rootView,并且一一判断,效率低下。
计划 3:AOP(Aspect Oriented Programming)
面向切面编程。应用 AspectJ,
思路:在程序编译期间,在相应的 onClick 办法调用前或后插入埋点代码。
计划 4: 字节码插桩
字节码函数插桩目前有以下两种框架
ASM
思路:应用程序打包成 APK 之前会先编译成.class 文件,而后打包成 dex,最初组成 apk。所以在打包成 dex 文件和编译成.class 文件之间进行源文件的替换就行。
毛病:目前没什么毛病
Javassist
与 ASM 思路统一, 然而和 ASM 比照,效率不够高。
ASM 框架进行字节码函数插桩
通过上述计划的比照,最终采纳 ASM 进行字节码插桩。次要是对代码的侵入低,可定制化配置(过滤采集页面,过滤时长,配置页面映射等)。
下图箭头指向处就是进行函数插桩的地位。
代码侵入性低
计划实现是在代码文件编译成 class 文件之后进行办法的插入,无需在编写阶段进行。
- 应用 android 提供的 Transform API 获取 project 的文件
-
检测到文件后缀为 class 的时候进行文件批改
- ASM 框架相应 API 进行字节码读取和剖析和插入
- 先拿到类的详细信息(类名,修饰符,继承的父类,实现的接口等信息)
- 接着扫描到该类的办法,进行判断插入咱们预设的埋点代码
- 而后笼罩原来的 class 文件
- 接着 gradle 持续编译生成 dex
效率
比 java 中应用反射快,在 ASM 的官网中也有介绍。ASM 的设计和实现是尽可能的小和尽可能快,所以它非常适合在动静零碎中应用(但当然也能够以动态形式应用,例如在编译器中应用)。
更多对于框架 ASM 的远离和具体应用在这里就不赘述了。
如何应用?
在 project 的 build.gradle 增加:
buildscript {
repositories {google()
jcenter()
maven {url uri('repo')
}
}
dependencies {
classpath 'com.cage:autotrack.android:1.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
在 APP 模块中:
apply plugin: 'com.cage.plugin'
dependencies{implementation project(':cgtrack_support')
}
初始化:
//Application 中初始化
//kotlin
TrackApi.init(this)
//java
TrackApi.INSTANCE.init(this);
// 配置
ConfigOptions.INSTANCE.addTrackInfoCallBack(new TrackInfoCallback() {
@Override
public void trackInfo(String eventName, JSONObject json) {
// 这里进行埋点事件上报
// 当然回调的类型也能够从 JSONObjetc 变为 String
}
});
接入 APP 后
在 APP 中进行点击浏览页面,相应的事件进行触发:
页面点击的时候触发:
页面退出的时候触发:
进入页面的时候触发:
后续保护与迭代降级
目前曾经笼罩了 View,Dialog,CompoundButton,AdapterView,BottomNavigationView。
后续如果短少相应的控件,那么能够依据相应的控件进行增加对应的字节码形容即可:
例如在 APP 中的底部控件为 Google 的 design 控件,增加:
SDK_API_CLASS = "com/cage/cgtrack/TrackUtils"
// 一般设置点击事件
if(mInterfaces.contains('android/support/design/widget/BottomNavigationView$OnNavigationItemSelectedListener') && nameDesc == 'onNavigationItemSelected(Landroid/view/MenuItem;)Z') {
// 插入变量
methodVisitor.visitVarInsn(ALOAD, 1)
// 插入方法
methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/view/MenuItem;)V", false)
}
// 应用 Lambda 模式设置
MethodCell onNavigationItemSelected = new MethodCell(
'onNavigationItemSelected',
'(Landroid/view/ MenuItem;)Z',
'Landroid/support/design/widget/BottomNavigationView$OnNavigationItemSelectedListener',
'trackViewOnClick',
'(Landroid/view/MenuItem;)V',
1, 1,
[Opcodes.ALOAD])
LAMBDA_METHODS.put(onNavigationItemSelected.parent + onNavigationItemSelected.name + onNavigationItemSelected.desc, onNavigationItemSelected)
上述步骤的意思:
先判断该类中实现的接口是否蕴含 OnNavigationItemSelectedListener 接口,接着判断实现该接口的办法是不是 onNavigationItemSelected,如果合乎,那么代表这个类蕴含该接口并实现了办法,能够进行埋点代码的插入。
相干视频举荐:
【Android 组件化设计】字节码插桩优化框架初始化速度
本文转自 https://juejin.cn/post/6844904194445426702,如有侵权,请分割删除。