资讯专栏INFORMATION COLUMN

Android埋点系统设计

trigkit4 / 923人阅读

摘要:一埋点架构设计埋点的核心逻辑抽象将生产的用户数据组织发送给服务器。普通埋点定义页面进入,页面离开,页面元素点击,页面元素曝光。无埋点进入退出都使用,如何区分增加了一个字段,用表示页面进入退出。如何修改字节码库基础使用。

一、埋点架构设计

埋点的核心逻辑抽象:将“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 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 {

  @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]表示方法的第一个参数
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/69519.html

相关文章

  • Android 性能监控系列一(原理篇)

    摘要:全称应用性能管理监控后面我会通过一系列的文章来介绍的原理框架设计与实现等等。在应用构建期间,通过修改字节码的方式来进行字节码插桩就是实现自动化的方案之一。 showImg(https://segmentfault.com/img/bVbbRX6?w=1995&h=1273); 欢迎关注微信公众号:BaronTalk,获取更多精彩好文! 一. 前言 性能问题是导致 App 用户流失的罪魁...

    yacheng 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<