JJEvent 一個可靠的Android端數據埋點SDK
- 本文是原理介紹
- 這裏是如何使用傳送門
- 這裏是源碼地址
V1.0.0 功能列表 |
是否支持 |
---|---|
接口自定義 | 支持 |
緩存策略 | 支持 |
外部cookie註入 | 支持 |
推送周期設定 | 支持 |
強制推送 | 支持 |
自定義埋點事件 | 支持 |
獨立運行 | 支持 |
多線程寫入 | 支持 |
後臺線程服務 | 支持 |
註:代碼已經經過線上項目驗證, 橫向Google統計對比,統計數據無丟失,性能穩定.
項目背景
統計數據 是BI做大數據,智能推薦,千人千面,機器學習的 數據源和依據.
在這個app都是千人千面,智能推薦,ab流量測試的時代, 一個可以根據BI部門的需求, 可以自有定制的 數據統計上報, 就顯得非常重要.
目前, 市面上 做統計的第三方平臺有很多, 比如最出名的Google的GTM統計,友盟統計等等.
但是 這些統計, 第一點,就是上傳的頻率,比較固定, 難以滿足要求不同的頻次需求. 第二點,需要統計到的字段和規則都是死板的,無法定制.
目前GitHub上, 沒有一個 自定義的 統計SDK 思路和源碼.
我想,在這裏分享下,我的思路和代碼.
這裏有幾個要點
- 統計分類:統計分為屏幕值,事件兩種,後續可能擴展.
- 統計規則: 支持簡單Google統計方式,支持自定義字段.
- 推送方式:每兩分鐘上傳到服務器,
- 作為sdk,可以單獨集成,獨立運行.
這是一個什麽樣的統計SDK?
做統計SDK的方式有這兩種
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 屏幕加載方式 下拉刷新=1、翻頁=2、標簽切換=3、局部彈屏4、篩選刷新=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架構圖
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的邏輯.
經過測試部和線上數據驗證, 數據量統計無誤,沒有重復數據,沒有遺漏數據.
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
的處理方式,
避免出現在業務層 查看調用出處的時候,造成誤解.比如
後期,在我們做自己的業務線的時候,大家也可以采用這種方法.
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")
當然, 也可以將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.**{*;}
總結思考
-
在本sdk中,
由於所有動作的生命周期,是全局周期,所以,選擇了sdk持有applicatin
上下文進行操作.
對於需要上下文的地方,直接用持有applicatin
,可以考慮
DBHelper中方法是靜態的,由於依賴於其中Java靜態方法,不能被靜態實現..,所以依賴的實現.後期可以采用單例進行處理. - 無從下手的感覺...無從下手的感覺的根本原因就是你沒有下手去做..寫寫,畫畫,慢慢就會了然於胸.
後期優化
為了操作方便,直接讓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/
JJEvent 一個可靠的Android端數據埋點SDK