JJEvent 一個可靠的Android端數據埋點SDK(已從新開源)



V1.0.0功能列表 是否支持
接口自定義 支持
緩存策略 支持
外部cookie注入 支持
推送週期設定 支持
強制推送 支持
自定義埋點事件 支持
獨立運行 支持
多線程寫入 支持
後臺線程服務 支持

注1:代碼已經通過線上項目驗證, 橫向Google統計對比,統計數據無丟失,性能穩定.java

注2:可修改數據庫鏈接EDBHelper等,做爲Java服務端埋點統計使用.android

jjEvent.gif

項目背景

統計數據 是BI作大數據,智能推薦,千人千面,機器學習的 數據源和依據. 在這個app都是千人千面,智能推薦,ab流量測試的時代, 一個能夠根據BI部門的需求, 能夠自有定製的 數據統計上報, 就顯得很是重要.git

目前, 市面上 作統計的第三方平臺有不少, 好比最出名的Google的GTM統計,友盟統計等等.github

可是 這些統計, 第一點,就是上傳的頻率,比較固定, 難以知足要求不一樣的頻次需求. 第二點,須要統計到的字段和規則都是死板的,沒法定製.sql

目前GitHub上, 沒有一個 自定義的 統計SDK 思路和源碼.數據庫

我想,在這裏分享下,個人思路和代碼.編程

這裏有幾個要點json

  • 統計分類:統計分爲屏幕值,事件兩種,後續可能擴展.
  • 統計規則: 支持簡單Google統計方式,支持自定義字段.
  • 推送方式:每兩分鐘上傳到服務器,
  • 做爲sdk,能夠單獨集成,獨立運行.

這是一個什麼樣的統計SDK?

作統計SDK的方式有這兩種api

1.用AOP的處理方式, 在方法內,插入統計代碼. 這種方式雖然在.java文件裏 沒有代碼侵入,可是可定製行不高,只適合簡單的 統計需求.緩存

2.用普通的方法樣式,使用GTM.event(xxx)方式,代碼侵入極高, 可是能夠實現高度自定義.

現階段, 我會採用第二種方式,爲了數據的精確要求,採用侵入式.

後續, 我會繼續思考,更好的實現方式. 也請你們一塊兒分享本身的思路.

由於統計規則業務定製性很強,沒法對傳送數據進行統一的抽象管理, 該項目就不單獨發佈到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靜態方法,不能被靜態實現..,因此依賴的實現.後期能夠採用單例進行處理.

  2. 無從下手的感受...無從下手的感受的根本緣由就是你沒有下手去作..寫寫,畫畫,慢慢就會了然於胸.

後期優化

爲了操做方便,直接讓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/

相關文章
相關標籤/搜索