JJEvent 一个可靠的Android端数据埋点SDK



jjEvent.gif

项目背景

统计数据 是BI作大数据,智能推荐,千人千面,机器学习的 数据源和依据. 在这个或有资讯类app都是千人千面的时代, 一个能够根据BI部门的需求, 能够自有定制的 数据统计上报, 就显得很是重要.java

目前, 市面上 作统计的第三方平台有不少, 好比最出名的Google的GTM统计,友盟统计等等.android

可是 这些统计, 第一点,就是上传的频率,比较固定, 难以知足要求不一样的频次需求. 第二点,须要统计到的字段和规则都是死板的,没法定制.git

目前GitHub上, 没有一个 自定义的 统计SDK 思路和源码, 我想,在这里分享下,个人思路和代码.github

这里有几个要点sql

  • 统计分类:统计分为屏幕值,事件两种,后续可能扩展.
  • 统计规则: 支持简单Google统计方式,支持自定义字段.
  • 推送方式:每两分钟上传到服务器,
  • 做为sdk,能够单独集成,独立运行.

这是一个什么样的统计SDK?

作统计SDK的方式有这两种数据库

1.用AOP的处理方式, 在方法内,插入统计代码. 这种方式虽然在.java文件里 没有代码侵入,可是可定制行不高,只适合简单的 统计需求.编程

2.用普通的方法样式,使用GTM.event(xxx)方式,代码侵入极高, 可是能够实现高度自定义.json

现阶段, 我会采用第二种方式,为了数据的精确要求,采用侵入式.api

后续, 我会继续思考,更好的实现方式. 也请你们一块儿分享本身的思路.缓存

由于统计规则业务定制性很强,没法对传送数据进行统一的抽象管理, 该项目就不单独发布到jcenter,
若是须要,能够参考源码思路, 本身修改源码,修改数据载体,实现需求便可.


JJEvent设计初衷为:一个统计SDK, 能够单独发布到仓库,单独被项目依赖而不产生冲突,拥有本身的数据存储,网络请求.


1.上传规则

这些都是能够自定义的,修改源码便可

  • 固定周期进行上传: 好比每2分钟,进行一次数据上传.数据为 触发推送的时间节点 以前的数据.用于大部分统计.
  • 固定条数进行上传: 好比每100条,进行一次数据上传.数据为 触发 触发100条推送开始 以前的数据.用于大部分统计.
  • 实时上传:每次点击就进行push操做.数据为 触发推送的时间节点 以前的数据.用于特定统计.

2.统计分类

这里, 能够根据BI的业务需求而定, 你们能够在此基础上修改.

1.PV(PageView) 屏幕事件
  • sn(screen) 屏幕名称 遵循旧策略(Android/好价/好价详情页/title).
  • ltp 屏幕加载方式 下拉刷新=一、翻页=二、标签切换=三、局部弹屏四、筛选刷新=5.
  • ecp 自定义事件 ,json map存储.
2.Event 点击事件
  • ec(event category) 事件类别
  • ea(event action) 事件操做
  • el(event lable) 事件标签
  • ecp 自定义事件 ,json map存储.
3.expose曝光 事件
  • url 曝光url
  • ecp 自定义事件 ,json map存储.
4. 其余事件

支持自定义扩展

SDK抽象过程

面向对象语言的特色: 就是要面向对象编程,面向接口编程.当你在抽象的过程当中,只关注某个对象是什么,而后他拥有什么属性,什么功能便可.不须要考虑其中的实现.这也就是Java乃至面向对象语言,为啥这么多类的缘由,这其中有单一职责原则,接口分隔原则.

模块之间的依赖,应该最大程度的依赖抽象.

要想完整的把整个过程抽象清楚,须要对整个流程有个最大的认知.

判断逻辑,技术选型

思考:确定会想到这些东西,只不过想到的过程可能不一样,并且每一个设计者,想法都不会同样,实现过程也不同.

首先须要一个配置类Constant ,对常量,开关进行管理.

一个sdk有事件统计,那么必需要有一个Event类来进行屏幕值,事件两种统计动做.

统计事件发生后, 须要一个持久化过程DbHelper,即须要一个数据库支持存取.

如何推送呢? 须要创建一个后台服务JJService,对数据进行推送.

用什么推送呢?确定须要网络啊, 须要一个网络模块NetHelper从数据库中拿数据,进行推送.

推送的是什么呢? 须要建一个任务Task,让task承载推送的过程.

如何将模块进行链接,统一管理?

SDK总体架构

1.统计客户端SDK架构图

整体流程.png

2.服务端数据收集采用的是

  • openresty实现客户端日志上报接口
  • flume实现日志采集发送kafka
  • 最终落地到硬盘

3. 大数据端

通过抓取数据库数据快照 ,进行数据清洗,而后提供给机器学习,或者千人千面.

模块建设

这里若是有兴趣,请配合源代码.

1.JJEventManager管理模块

首先,sdk的生命周期是整个application的周期,因此我让sdk 持有application 上下文,不会存在内存泄漏.因此,我考虑将全局上下文放在这里管理.当其余位置须要的时候到JJEventManager .getContext() 取值.

做为管理类,须要拥有控制sdk完整生命周期的功能.即init(),cancelPush(),destroy()等方法.让各个模块的生命周期在这里管理.

而后考虑到,让用户能够动态配置各类参数,好比周期,是不是debug模式,主动推送周期等等.因此在内部使用buider模式,进行动态构建.

JJEventManager.Builder builder =new JJEventManager.Builder(this);
        builder.setHostCookie("s test=cookie String;")//cookie
                .setDebug(false)//是不是debug
                .setSidPeriodMinutes(15)//sid改变周期
                .setPushLimitMinutes(0.10)//多少分钟 push一次
                .setPushLimitNum(100)//多少条 就主动进行push
                .start();//开始
    }

2.Event动做模块

动做类,统计只有两个动做,即两个方法screen (),event(),以及一些重载方法.

由于是公开类,因此要作到简洁,注释要到位..(导入项目中的jar包,没有Java document..由于doc生成在本地..云端没有)

因为是数据入口类,全部坚定不能存在崩溃的状况发生.
因此在相应的地方加上了try catch处理.

/**
 * 统计入口
 * Created by chenchangjun on 18/2/8.
 */
public final class JJEvent {
    /**
     * pageview 屏幕值
     * @param sn  screen 屏幕值,例`Android/主页/推荐`
     * @param ltp 屏幕加载方式
     */
    public static void screen(String sn, LTPType ltp) {
        screen(sn, ltp, null);
    }
   /**
     * pageview 屏幕值
    * @param sn  screen 屏幕值,例`Android/主页/推荐`
     * @param ltp 屏幕加载方式
     * @param ecp event custom Parameters 自定义参数Map<key,value>
     */

    public static void screen(String sn, LTPType ltp, Map ecp) {

         try {
                  ScreenTask screenTask =new ScreenTask(sn,ltp,ecp);
                  JJPoolExecutor.getInstance().execute(new FutureTask<Object>(screenTask,null));
              } catch (Exception e) {
                  e.printStackTrace();
                  ELogger.logWrite(EConstant.TAG, "expose " + e.getMessage());

              }

    }

将处理细节交给其余类处理,这里我用了一个 Event包装类EventDecorator来作EventBean中统一的数据缓存,参数值处理.遵循单一职责原则.

注意:

在修改数据体EventBean来知足业务需求时, 请在EventDecorator的相关方法中进行修改.

3.DBHelper模块

刚开始想用模板方法继承来作,将CRUD的实现放在宿主中,

可是, 因为用户不太清楚sdk内部实现逻辑,用户维护sdk的成本过高.因此,我就从新裁剪了开源的XUtils中的dbUtils,而后修改类名,做为db服务.

4.ThreadPool模块

为了减小UI线程的压力, 有必要将数据操做放到子线程中. 考虑到数据量时大时小, 因此须要自定义一个线程池,来管理线程和县城任务.

这里, 最主要的就是 控制好线程的对共享变量的访问锁.保证线程的原子性和可见性.

将全部Event任务,做为一个Runable,放到阻塞队列中,让线程池队列执行.注意设置runable超时时间,异常处理.尽可能保证数据录入成功.

要注意的是, Event任务 执行有快有慢, 因此,最终保存到数据库的时候, 并非按照队列的顺序.

4.1 如何保证线程安全?

对于变量
好比int eventNum=1;
线程在执行过程当中, 会将主内存区的变量,拷贝到线程内存中, 当修改完a后,再将a的值返回到主内存中.这个时候,若是两个线程同时修改该变量,第三个线程在访问的时候,颇有可能a的值尚未改变.这个时候就会让a的改变不可见.因此,能够用线程安全变量AtomicInteger,或者原子性变量volatile,让他们咋发生改变的时候,马上通知主内存中的变量.

对于方法
为了保证线程间访问方法互斥, 用synchronized对线程访问方法,进行同步.保证线程顺序执行.即要将全部共通操做,放到一个加载器方法中,用synchronized同步.

另外,避免线程滥用,性能浪费, 要仔细考量voliate,synchronized等字段的频次.

详情处理可见EventDecorator.java中的 变量处理.

4.2 sqlite数据库是否 线程安全?

目前, 统计sdk状态是

  • 多个线程同时执行数据库操做,
  • Timer拥有本身的单线程 执行数据库读取.

要保证数据库使用的安全,通常能够采用以下几种模式

SQLite 采用单线程模型,用专门的线程/队列(同时只能有一个任务执行访问) 进行访问
SQLite 采用多线程模型,每一个线程都使用各自的数据库链接 (即 sqlite3 *)
SQLite 采用串行模型,全部线程都共用同一个数据库链接。

在本SDK中,采用串行模式,在初始化过程当中,SQLiteDatabase静态单例, 来保证线程安全.

项目通过测试部门,和线上检验,线程间访问正确,数据统计正确.

5.NetHelper模块

首先,net请求,我裁剪的是volley.

NetHelper应该采用的是静态或者单例,采用单例的缘由是,他的生命周期和application同级.功能应该是 接受数据,而后推送数据,最后暴露告知结果.封装里面的请求转发逻辑.

NetHelper网络模块,应该有一个请求队列(避免请求数据错乱),,还应该提供针对不一样EventType进行不一样处理请求的方法,而后还须要一个统一的网络请求监听.

为了保证 推送不出现数据错乱,应该在上一次网络访问没有结束前,不能继续访问的锁,用锁isLoading来控制.

将 请求分发逻辑,是否正在请求,以及监听彻底封装在里面.对外只暴露OnNetResponseListener.

按照上述逻辑,调用方式是这样的.简单实用.

ENetHelper.create(JJEventManager.getContext(), new OnNetResponseListener() {
            @Override
            public void onPushSuccess() {
                //5*请求成功,返回值正确, 删除`cut_point_date`以前的数据
                EDBHelper.deleteEventListByDate(cut_point_date);
            }

            @Override
            public void onPushEorr(int errorCode) {
                //.请求成功,返回值错误,根据接口返回值,进行处理.
            }

            @Override
            public void onPushFailed() {
                //请求失败;不作处理.

            }
        }).sendEvent(EConstant.EVENT_TYPE_DEFAULT, list);

6. EPushTask模块

Push的逻辑比较复杂,因此更须要这个类,专门来作push任务.

6.1 如何保证 数据 推送不会出现重复推送,或者缺乏数据?

请看以下push的逻辑.
image.png

通过测试部和线上数据验证, 数据量统计无误,没有重复数据,没有遗漏数据.

7.EPushService模块

这应该是一个后台服务模块. 功能应该有 开启服务,周期推送,主动推送,中止推送.

需不须要用一个不会被杀死的后台服务?

答案是不须要,

1.从用户体验上讲,一个系统杀不死的服务,是一个用户体验极差的处理方式.有些手机 甚至会提示,该app正在后台运行.

2.从sdk必要属性上讲, 统计sdk,只有app在前台的时候,才会有事件统计.因此推送服务没有必要一直存在.

3.当系统内存不足的时候, 会把后台推送线程杀死. 可是杀死的仅仅是周期推送 ,数据记录并不会中止. 等待知足条件 (100条记录),就会主动推送.

因此,结论是 推送服务,仅仅须要在用户可见的状况下,进行便可. 线程是否被杀死,影响的仅仅是推送到服务器是否及时.

通过考量, 采用Timer+TimerTask的方式,进行周期推送服务.由于 虽然Timer不保证任务执行的十分精确。 可是Timer类的线程安全的。

并且TimerTask是在子线程中,不会push服务不会阻塞主线程.

sdk总体框架调整

1.访问权限

sdk 对外暴露类和方法,要尽量少.只暴露用户可操做的方法.隐藏其余细节.
因此在这个sdk中,用户只须要知道 设置必要参数,开启,添加统计便可,其余无需了解.

因此,我对访问权限进行了处理,只公开如下类,以及相应方法.

  • JJEventManager 事件管理

    • JJEventManager.init() 初始化
    • JJEventManager.cancelEventPush()取消推送
    • JJEventManager.destoryEventService()终止全部服务
  • JJEvent 统计入口

    • JJEvent.event(String ec, String ea, String el) 事件
    • JJEvent.screen(String sn, LTPType ltp)屏幕值

3.sdk惟一性

为了保证sdk命名惟一性,采用全部必要模块加前缀E表明Event的处理方式,
避免出如今业务层 查看调用出处的时候,形成误解.好比

image.png

后期,在咱们作本身的业务线的时候,你们也能够采用这种方法.

2.sdk生成,版本管理,混淆打包

本身在gradle中写了一个打包脚本,让打包的过程,自动化.详情见源码.

task release_jj_analytics_lib_aar(group:"JJPackaged",type: Copy) {
    delete('build/myaar')
    from( 'build/outputs/aar')
    into( 'build/mylibs')
    include('analytics_lib-release.aar')
    rename('analytics_lib-release.aar', 'jj-analytics-lib-v' + rootProject.ext.versionName +'-release'+ '.aar')
}
release_jj_analytics_lib_aar.dependsOn("build")

image.png

固然, 也能够将sdk放到Nexus Maven仓库,或者公司私有仓库,进行api依赖.

2.3 sdk需不须要混淆?

这个问题我考虑了好久, sdk给本身用,用的着混淆嘛? 混淆会不会让同事们可读性变差,想到最后,发现app上线前,也须要打包混淆.若是我在app的progurd.rules中,添加各类规则,那么sdk用起来很繁琐.

so~ , 我在 jar 包打包前,进行了必要混淆,keep了两个公开类.

如今,在任何app若是想使用sdk, 那么只须要 app的progurd.rules中添加两句混淆规则便可.

-dontwarn com.ccj.client.android.analyticlib.**
-keep class com.ccj.client.android.analytics.**{*;}

总结思考

  1. 在本sdk中,

因为全部动做的生命周期,是全局周期,因此,选择了sdk持有applicatin上下文进行操做.
对于须要上下文的地方,直接用持有applicatin ,能够考虑
DBHelper中方法是静态的,因为依赖于其中Java静态方法,不能被静态实现..,因此依赖的实现.后期能够采用单例进行处理.

  1. 无从下手的感受...无从下手的感受的根本缘由就是你没有下手去作..写写,画画,慢慢就会了然于胸.

后期优化

为了操做方便,直接让EDBHelper,ENetHelper直接做为静态类...

后期能够用单例取代.在管理类JJEventManager中,统一初始化.这样,就能够 依赖抽象.好比持有DBDao.saveEvent(),而不是用实现类EDBHelper.saveEvent().就避免了后期牵一发而动全身的问题.

About Me

===

CSDN:http://blog.csdn.net/ccj659/article/

简书:http://www.jianshu.com/u/94423b4ef5cf

github: https//github.com/ccj659/

相关文章
相关标签/搜索