一、埋点架构设计
埋点的核心逻辑抽象:将“APP生产”的“用户数据”组织“发送给服务器”。
1.Producer是APP,生产各种用户数据。
2.Consumer是埋点系统的数据上传模块,把各种用户数据上传给服务器。
3.MetaData是对用户数据的抽象。
4.Queue是存储用户数据的队列。
将抽象逻辑分解为各个子模块并拼装,形成最终的架构。
1.MetaData模块:将用户行为抽象为数据描述。
2.Producers模块:生产用户数据,将用户行为转化为数据集。
3.Consumers模块:消费用户数据,将用户数据上传给服务器。
4.Storage模块:存储用户数据,将用户数据暂存在文件中。
5.Broker模块:管理用户数据。
二、MetaData模块
关键是特征值的提取。
设备基础属性:
设备id:udid=12345678910
APP基础属性:
版本:v=8.5.0
渠道:c=xiaomi
用户基础属性:
用户id:ucid=12345678910
页面元素描述:
属于哪个APP:pid=bigc_app_xinfang
属于哪个页面:key=newhouse/homeindex
属于哪个页面元素:为页面元素定义唯一的code
页面与页面关联逻辑:
从哪个页面进入当前页面:f=newhouse/homeindex
页面停留时间:stt=1000
用户行为描述:
用户基础行为描述:evt=xxx,如APP启动/退出,页面进入/离开/滑动,页面元素点击/曝光,push到达/点击
用户扩展行为描述:action=json,如action={"project_name":"thyhwabktj", "xinfangapp_click":"10020"}
举例:
{v=1.1.6, ts=1527067845806, ucid=null, ssid=b032222c-7105-4119-9bfe-a1aec5ba9285, pid=bigc_app_xinfang, key=newhouse/project, action={"sample_mark":"","project_name":"thyhwabktj"}, longitude=0.0, latitude=0.0, cid=110000, f=newhouse/homeindex?project_name=thyhwabktj&city_id=110000, stt=685, evt=2}
LJ由于历史原因,有两套MetaData
无埋点evt定义:app启动/退出=5,页面进入/退出=6,页面滑动=8,页面元素点击=7,push到达/点击=9。
普通埋点evt定义:页面进入=1,3,页面离开=2,页面元素点击=10186,页面元素曝光=11316。
Q:无埋点进入/退出都使用6,如何区分?
A:增加了一个status字段,用status=0/1表示页面进入/退出。
建议:统一dig埋点和无埋点的evt定义
三、Storage模块
1.内存缓存(List):每生产一条数据都会先进入内存缓存
2.数据库存储(DataBase):提供对数据库的操作接口
四、Broker模块
1.接收生产者的数据,并写入数据库(对外提供put方法)
Q:如何控制数据由内存写入数据库时机?
1.1.内存数据超过一定数量(如20条)时,缺点是应用在后台被杀,最多可能丢失20条数据
1.2.生命周期onPause时,缺点是写操作相对比较频繁
1.3.利用定时器,缺点是后台定时任务可能不执行,导致丢数据
LJ现行方案:1.1+1.3
建议使用:1.1+1.2
Q:多进程写数据库怎么办?
A:多进程向sqlite插入数据不会有问题,只是插入顺序是乱序的;如果要保证插入顺序也一致,可以考虑启动一个独立进程操作sqlite,其它进程与sqlite所在进程进行通信。
2.读取数据库中的数据,并供给消费者消费(对外提供aquire/release方法)
Q:如何控制数据提供给消费者消费的时机?
A:生命周期onPause时,缺点是消费相对比较频繁
五、Consumers模块
申请(aquire)数据,消费(upload)数据,释放(release)数据
建议:LJ目前的代码可以参考此设计进行代码优化
六、Producers模块
普通埋点数据生产
Producers模块对外提供各种封装好的add方法供开发者调用,如addClickEvent(), addPageEnterEvent(), addPageLeaveEvent()等。
无埋点数据生产
Producers模块自动生产各种埋点数据,如app启动/退出,页面进入/退出/滑动,页面元素点击,push到达/点击等。
七、LJ现有埋点库
由于历史原因,LJ总共有2个埋点库:
dig库:LJ-APP&BK-APP均在使用,用于对无埋点无法处理的特殊数据进行补充
无埋点库:LJ-APP用的老版本(pid无法自定义,主工程和插件共用主工程的pid),BK-APP用的新版本(pid可以自定义,主工程和插件工程可以分别定义自己的pid)
建议:两库合并
八、无埋点实现原理
无埋点的核心是,如何通过代码自动搜集想要的信息:
1.设备、APP、用户等基础属性,直接通过api获取
2.Activity进入/离开等生命周期相关属性,直接通过LifeCycleCallback监听获取
3.Activity的唯一标记如pageId等属性,直接通过注解获取
4.UI元素点击/滑动等行为属性,需要通过hook代码才能实现
如何确定UI元素的唯一性:
方案1:为需要统计的元素定义唯一的code,写入contentDescription,然后读取这个属性
方案2:利用ViewTree中的ViewPath唯一确定一个UI元素
核心代码:
public static ViewPath getPath(View view) {
do {
//1.构造ViewPath中于view对应的节点:ViewType[index]
ViewType = view.getClass().getSimpleName();
index = view在兄弟节点中的index;
ViewPath节点 = ViewType[index];
} while ((view = view.getParent()) instanceof View);//2.将view指向上一级的节点
}
结果示例:
DecorView/LinearLayout[0]/FrameLayout[0]/ActionBarOverlayLayout[0]/ContentFrameLayout[0]/FrameLayout[0]/LinearLayout[0]/ViewPager[0]/ButtonFragment[0]/AppCompatButton[0]
ViewPath可读性问题:
可以建立一个Mapping文件,将ViewPath和描述Describe对应起来,具体实现:
写一个工具,当我们在手机上点击一个按钮的时候弹出弹窗,输入Describe描述文字,最终生成一个ViewPath<->Describe的Mappting文件。
九、注解基础知识
1.元数据metadata与注解annotation
Java中总共有4种类型:类Class、接口Interface、枚举Enum、元数据@interface(就是注解)。
元数据:是添加到包、类、方法、属性上的额外信息,对其进行描述,如@Override。
元注解:是最基本的注解:@Target、@Retention、@Documented、@Inherited
@Target取值:PACKAGE、TYPE、FIELD、METHOD
@Retention取值:SOURCE、CLASS、RUNTIME
注解的作用:编译时可获取到注解信息动态生成代码,运行时科获取到注解信息做特殊处理。
2.运行时注解
在运行时通过反射对注解进行处理,比较消耗资源,性能较差。
定义:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface MethodInfo {
String author() default "xiaoming";
String date();
int version() default 1;
}
使用:
public class App {
@MethodInfo(
author = “xiaoming@gmail.com”,
date = "2018/05/10",
version = 2)
public String getAppName() {
return "trinea";
}
}
解析:
Class cls = Class.forName("com.lianjia.test.annotation.App");
for (Method method : cls.getMethods()) {
MethodInfo methodInfo = method.getAnnotation(MethodInfo.class);
System.out.println(“method author: ” + methodInfo.author());
}
3.编译时注解
在编译时通过Java Annotation Process技术对注解进行处理,因为不使用反射,所以性能较好
模拟ButterKnife定义:
@Retention(CLASS)
@Target(FIELD)
public @interface InjectView {
int value();
}
模拟ButterKnife调用:
@InjectView(R.id.user)
EditText username;
模拟ButterKnife处理:
@SupportedAnnotationTypes({"com.lianjia.InjectView "})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class MyProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement typeElement : annotations) { // 遍历annotations获取annotation类型
for (Element element : roundEnv.getElementsAnnotatedWith(typeElement)) { // 使用roundEnv.getElementsAnnotatedWith获取所有被某一类型注解标注的元素,依次遍历
// 在元素上调用接口获取注解值
int annoValue = element.getAnnotation(TestAnnotation.class).value();
String annoWhat = element.getAnnotation(TestAnnotation.class).what();
System.out.println("value = " + annoValue);
System.out.println("what = " + annoWhat);
// 向当前环境输出warning信息
processingEnv.getMessager().printMessage(Kind.WARNING, "value = " + annoValue + ", what = " + annoWhat, element);
}
}
return false;
}
}
十、Hook代码实现搜集用户点击数据
1.Android编译运行全流程
2.在onClick(View view)方法中加入埋点逻辑
2.1.编写埋点逻辑:由埋点sdk(LianjiaAnalyticsSdk)完成
2.2.将埋点逻辑插入onClick(View view)方法中:由埋点插件(LianjiaAnalyticsPlugin)完成
3.埋点插件编写
1.插件编写流程?plugin编写。
2.如何侵入编译流程?transform库基础使用。
4.如何修改字节码?Javassist库基础使用。
4.核心代码
编写埋点逻辑:
public class AnalyticsEventsBridge {
/**
* Hook onClick(View view)方法,并调用此方法
*/
public static void onViewClick(@Nullable View view) {
// 获取view的唯一标记等相关信息
// 生成一条埋点日志并写入
}
}
编写插件:
1.插件项目目录结构
2.build.gradle修改
apply plugin: 'groovy'
dependencies {
compile gradleApi()
compile 'com.android.tools.build:gradle:2.3.3'
compile 'org.javassist:javassist:3.21.0-GA'
}
apply from: './gradle-mvn-push.gradle'
apply plugin: 'maven-publish'
publishing {
publications {
mavenJava(MavenPublication) {
groupId PROJ_GROUP
artifactId PROJ_ARTIFACTID
version PROJ_VERSION
from components.java
}
}
}
3.插件执行入口,相当于Main函数
class AnalyticsPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
InjectAndJarMergingTransform transform = new InjectAndJarMergingTransform()
android.registerTransform(transform)
}
}
transform侵入编译流程:
public class InjectAndJarMergingTransform extends Transform {
@Override public void transform(@NonNull TransformInvocation invocation)
throws TransformException, IOException {
println("LianjiaJarMergingTransform, begin");
//这里可以获取到文件的输入/输出信息,并对其做相应的更改,核心抽象为1个方法
processClass(inputStream, outputStream);
println("LianjiaJarMergingTransform, end");
}
}
Javassist修改字节码:
private void processClass(InputStream inputStream, OutputStream outputStream) throws IOException {
final ClassPool classPool = AndroidClassPool.getClassPool()
final CtClass clazz = classPool.makeClass(inputStream)
final CtMethod ctMethod
try {
ctMethod = ctClass.getMethod(targetMethodName, targetMethodDescriptor);
} catch (NotFoundException e) {
xxxxxxxx
}
//通过过滤器,找到android.view.View$OnClickListener的onClick(View view)方法,略
//Hook调用AnalyticsEventsBridge.onViewClick(view)方法
ctMethod.insertBefore("""com.lianjia.sdk.analytics.gradle.AnalyticsEventsBridge.onViewClick(\$1);""")
//其中$0=this, $1=$args[0]表示方法的第一个参数
}