埋点模块是一个完整的系统不可获取的一部分,现在也有很多第三方的埋点SDK:
优点:灵活、准确,可以定制化。
缺点:业务埋点量非常大,开发成本高,不易维护,如果要修改、新增埋点,需要重新发版。
实现方案:利用Gradle插件,在编译阶段在代码中插入埋点代码,进行数据采集。
代表方案:GrowingIO SDK集成文档
优点:开发效率高,无需手动埋点,编译时插入代码,性能高,支持数据可回溯。
缺点:埋点灵活性低。
像友盟、百度统计这样耦合性太大,虽然可通过二次封装的方式,降低对这些SDK的依赖,但埋点统计模块耦合性仍然很大,为了解决这个问题,我们可通过无埋点方案来实现数据的收集过程。
无埋点方式是通过全局监听或AOP技术添加埋点的一种实现方案,开发者不需要在每个需要埋点的地方添加代码,只需要根据服务器分发的配置,获取相应的埋点数据即可。一方面代码耦合度低,同时灵活度也高,埋点数据直接由服务器控制。缺点就是没有侵入式埋点精准。
Android无埋点 GrowingIO SDK集成
apply plugin: 'com.android.application'
//添加 GrowingIO 插件
apply plugin: 'com.growingio.android'
android {
defaultConfig {
resValue("string", "growingio_project_id", "您的项目ID")
resValue("string", "growingio_url_scheme", "您的URL Scheme")
}
}
dependencies {
//GrowingIO 无埋点 SDK
implementation 'com.growingio.android:vds-android-agent:autotrack-2.8.7'
}
2、添加 URL Scheme 和应用权限
URL Scheme 是您在 GrowingIO 平台创建应用时生成的该应用的唯一标识。把 URL Scheme 添加到您的项目,以便我们唤醒您的应用。
将应用的 URLScheme 和应用权限添加到你的 AndroidManifest.xml 中的 LAUNCHER Activity 下。
3、. SDK初始化配置
SDK的初始化时机必须在 Application 的 onCreate 方法中进行。
请将 GrowingIO.startWithConfiguration 加在您的 Application 的 onCreate 方法中
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
GrowingIO.startWithConfiguration(this, new Configuration()
.trackAllFragments()
// 建议使用 BuildConfig 设置
.setChannel("XXX应用商店")
);
}
}
注意事项
trackAllFragments方法如果在初始化时调用,APP 内部的 Fragment 将代替 Activity 作为页面,一个 Activity 中包含多个 Fragment 时大概率会是面积最大的 Fragment 作为当前的页面事件( Page )。
1、自定义点击事件
如果您有自定义的控件重写了View的onTouchEvent方法来判断和处理点击事件,那么必须调用它的PerformClick,并且设置相应的onClickListener。
2、设置页面别名
对于安卓应用,页面指的是Activity或者Fragment。
有些时候,对于完成某个功能的页面,统计时可能需要进一步细分。 比如,对于展示商品列表的页面,需要区分衣物类商品,以及食品类商品的两种列表的访问量。
为处理这种场景,我们提供了取别名的方法来区分这两种情况下的页面,方法如下:
GrowingIO.setPageName(Activity activity, String name)
如果您设置的的对象是Fragment,将上方的Activity替换为Fragment即可。
我们用Activity来举例,具体说明它的用法。
某个应用的商品列表页是用FeedActivity实现的,所以默认的页面名称都是FeedActivity。
现在我们想区分衣物类商品列表和食品类商品列表,分别看它们的浏览量,可以在onCreate方法中添加如下代码:
public class FeedActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GrowingIO.getInstance().setPageName(this, "Clothing");
}
}
必须在该Activity的onCreate方法中完成该属性的赋值操作。页面别名建议设置为字母、数字和下划线的组合。
为查看数据方便,请尽量对 iOS 和安卓的同功能页面取不同的名称。
除上述的用户行为数据(无埋点数据)之外,GrowingIO 还提供了多种 API 接口,供您上传一些自定义的数据指标及维度
请参考Android SDK API > 自定义数据上传API
集成了 GrowingIO 的 SDK 之后,它将会自动地为您采集一系列用户行为数据,进行数据分析。除自动收集的用户行为数据,GrowingIO 还提供了多种 API 接口,供您上传一些自定义事件和变量,下面介绍自定义事件和变量 API 使用方法,后文简称埋点事件API
1、设置登录用户ID(setUserId)
GrowingIO.getInstance().setUserId(String userId);
2、. 清除登录用户ID(clearUserId)
GrowingIO.getInstance().clearUserId();
3、设置登录用户变量(setPeopleVariable)
GrowingIO gio = GrowingIO.getInstance();
gio.setPeopleVariable(String key, String value);
gio.setPeopleVariable(String key, Number value);
gio.setPeopleVariable(String key, Boolean value);
gio.setPeopleVariable(JSONObject peopleVariables);
4、设置访问用户变量(setVisitor)
GrowingIO.getInstance().setVisitor(JSONObject visitorVar)
5、设置页面级变量(setPageVariable)
发送页面级别的信息,在添加代码之前必须在打点管理界面上声明页面级变量。
GrowingIO gio = GrowingIO.getInstance();
gio.setPageVariable(Activity activity, String key, String value);
gio.setPageVariable(Activity activity, String key, Number value);
gio.setPageVariable(Activity activity, String key, Boolean value);
gio.setPageVariable(Activity activity, JSONObject pageLevelVariables);
gio.setPageVariable(Fragment fragment, String key, String value);
gio.setPageVariable(Fragment fragment, String key, Number value);
gio.setPageVariable(Fragment fragment, String key, Boolean value);
gio.setPageVariable(Fragment fragment, JSONObject pageLevelVariables);
// SDK 2.6.6 以上支持 androidx, 增加接口:
gio.setPageVariableX(Fragment fragment, String key, String value);
gio.setPageVariableX(Fragment fragment, String key, Number value);
gio.setPageVariableX(Fragment fragment, String key, Boolean value);
gio.setPageVariableX(Fragment fragment, JSONObject pageLevelVariables);
添加代码之后,请先Clean项目,然后再进行编译,并在你的 Android App 安装了 SDK 后重新启动几次 App,保证行为采集数据自动发送给 GrowingIO,以便顺利完成检测。
在GrowingIO平台的应用创建页面继续完成应用创建的数据检测,检测成功后应用创建成功。
GrowingIO为您提供多种验证SDK是否正常采集数据的方式:
方式一: Mobile Debugger
方式二:在SDK中设置了Debug模式后,在IDE编译器控制台查看数据采集日志。
方式三:(推荐) 数据校验
WMDA SDK Android端整体架构主要分为圈选模块、事件采集上报、配置管理三部分
下面根据事件采集上报流程分别来介绍事件采集、处理、存储、上报和圈选
WMDA移动端数据采集类型主要分三种:页面浏览事件、控件点击事件和自定义事件。作为无埋点解决方案,SDK核心点就是事件的无痕采集。 其中,这三种事件又对应不同的采集处理方式,WMDA通过不同的技术方案进行采集,最后将事件统一处理,然后存储、上报。
事件采集是无埋点技术的核心,其中WMDA对Fragment和控件点击事件拦截,使用的是自己开发的gradle插件wmda plugin,编译时使用ASM以字节码插桩的方式注入代码,实现事件的采集。
对于事件拦截,首先需要确定插入时机和待修改字节码文件。这里我们使用Transform API作为插桩入口,在Java Compiler之后,class文件打包成dex文件之前修改字节码文件。由于Transform API是在Gradle插件版本1.5.0出现的,所以项目开发中Gradle插件版本不能低于1.5.0。
classpath 'com.android.tools.build:gradle:1.5.0'
然后在transform中遍历并操作字节码文件,因为现在很多大型项目,都会进行组件化操作,拆分成多个Library。所以这里除了要修改我们的应用源码,还需要对第三方库中的字节码文件进行扫描操作
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
inputs.each { TransformInput input ->
input.directoryInputs.each {
DirectoryInput directoryInput ->
//对应用源码生成的class操作
}
input.jarInputs.each { JarInput jarInput ->
//对第三方库中class操作
}
}
}
针对页面浏览事件,WMDA分两种不同的方式进行采集。
Activity采集 针对Activity,WMDA采用的方式是使用LifecycleCallback来监听页面的开启和关闭。 在页面开启时,拦截生命周期方法onResume,然后在事件处理模块处理,将其格式化成事件结构,并进行存储上报。
public void onActivityResumed(Activity activity) {
// 页面浏览事件采集处理
PageManager.getInstance().onActivityResumed(activity);
}
Fragment采集 针对Fragment,由于Android系统并没有关于Fragment生命周期的回调监听,所以这里WMDA通过Gradle插件,在编译时期,利用ASM库进行字节码操作,对Fragment注入WmdaAgent相应的页面采集方法,完成事件采集。在注入策略上,我们只需要对Fragment父类为下面两个的页面注入采集代码即可。
android/app/Fragment
android/support/v4/app/Fragment
对Fragment相关方法注入代码示例:
MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
MethodVisitor wrappedMv = mv;
if (wrappedMv != null) {
// 在onResume中插入WmdaAgent.onFragmentResumed方法
if (name.equals("onResume") && desc.equals("()V")) {
wrappedMv.visitCode()
wrappedMv.visitVarInsn(Opcodes.ALOAD, 0)
wrappedMv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/wuba/wmda/autobury/WmdaAgent",
"onFragmentResumed", "(Landroid/app/Fragment;)V")
}
}
}
WmdaAgent代码:
public static void onFragmentResumed(Fragment fragment) {
// 页面浏览事件采集处理
PageManager.getInstance().onFragmentResumed(fragment);
}
控件点击事件
关于点击事件的采集,使用Gradle插件在编译时埋点可以完美继承Mixpanel方案的各项优点,同时又可以规避其性能、数据准确性和版本适配问题。于是,在控件点击事件的采集上,我们调整了技术实现方案,从动态对View设置代理演进为编译时插入埋点代码。
WMDA对点击事件拦截支持常用的一些第三方框架,比如:
ButterKnife、databinding、AndroidAnnotations、RxBinding
具体的技术和之前的Fragment插桩埋点是一样的,编译时对onClick方法注入代码,以AOP方式对事件拦截处理。核心实现思路如下:
MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
MethodVisitor wrappedMv = mv;
if (wrappedMv != null) {
// 查找出方法名为onClick,入参为View的方法,注入WmdaAgent.onViewClick(View view)
if (name.equals("onClick") && desc.equals("(Landroid/view/View;)V")) {
wrappedMv.visitCode()
wrappedMv.visitVarInsn(ALOAD, 1);
wrappedMv.visitMethodInsn(INVOKESTATIC, "com/wuba/wmda/autobury/WmdaAgent", "onViewClick", "(Landroid/view/View;)V", false);
}
}
}
自定义事件
无埋点是WMDA的核心功能,但是由于业务场景特点,无埋点并不能完全满足所有业务场景需求,所以WMDA也提供了对手动埋点支持,使得WMDA在实际的使用中更加灵活,数据统计也更全面。
事件收集完成之后,就会发送到事件处理线程,对原生的事件进行加工,我们将页面的class全路径作为页面的特征值。APP_PAGE示例:
com.wuba.wmdademo.MainActivity
采集到页面事件后将其传入子线程处理,然后再提取出业务开发人员在页面onCreate方法中设置的页面ID及页面自定义属性,将这些数据统一格式化,构造成页面浏览事件,传给事件存储模块。
在对控件事件处理中,我们面临一个最大的问题就是,如何区分每一个控件,即如何定义控件的特征值。在这里,我们借鉴了Mixpanel的方法,即将View自身的类名及index,以及其逐级向上的所有父View的类名和index统一收集起来,然后将所有遍历信息拼接起来,当做该View在当前页面的唯一特征值。控件的唯一标识:页面APP_PAGE + ViewPath + index
ViewPath举例:
/MainWindow/LinearLayout[0]/FrameLayout[1]/ActionBarOverlayLayout[0]#decor_content_parent/ContentFrameLayout[0]/LinearLayout[0]/ScrollView[0]/LinearLayout[0]/AppCompatButton
事件处理完成之后,要交由存储模块进行本地持久化。在存储之前,先会检查存储策略,满足策略后再进行存储。
存储这里,使用的是本地SQLite存储Protobuf实例的二进制,然后使用AES-256进行加密存储。
事件存储完成之后,会触发上报请求。在上报之前,WMDA会先检查上报策略,满足策略后进行上报。
上报这里为了缩小WMDA包,只使用了HttpUrlConnection来处理网络操作。在数据上报的时候使用了GZIP+ProtoBuf来减少流量消耗,保证收集数据的同时,提升用户体验。
之前只是介绍数据采集方案,数据全量采集上报后,并不会直接分析处理,还需要一个圈选指标的过程。关于圈选的介绍,大家可以查看数据驱动增长:58无埋点用户行为分析实践之路这篇的圈选部分,这里就不做重复介绍了。
通常,我们圈选时会在一个页面停留较长时间,这时其实是不需要一直将当前页面快照数据发送给服务端的,因为页面并没有变化。这块有一个优化策略,SDK会根据当前屏幕快照生成一指纹,只有当前屏幕有变更时才会将当前页面快照数据发给用户分析平台。
当然,现阶段无埋点技术采用的字节码插桩方案还是存在一些短板,需要我们后续探索和解决。
1、click监听如果是在layout中使用android:onClick="xxxMethod"设置的暂时无法进行采集。这个设置监听的方法是利用Java的反射原理,去寻找对应的Method,在WMDA中是通过拦截OnClickListener点击事件来进行监听的,因此无法实现监听。
2、由于目前采用的是编译时插入埋点,所以不支持目前比较流行的RN框架。
3、同样因为是编译时插入埋点,所以对热更新的补丁支持可能也不到位。
58无埋点数据采集参考文章路径:https://cloud.tencent.com/developer/article/1164333