- 本文是原理介紹
- 這裡是如何使用傳送門
- 這裡是原始碼地址
V1.0.0 功能列表 |
是否支援 |
---|---|
介面自定義 | 支援 |
快取策略 | 支援 |
外部cookie注入 | 支援 |
推送週期設定 | 支援 |
強制推送 | 支援 |
自定義埋點事件 | 支援 |
獨立執行 | 支援 |
多執行緒寫入 | 支援 |
後臺執行緒服務 | 支援 |
注1:程式碼已經經過線上專案驗證, 橫向Google統計對比,統計資料無丟失,效能穩定
.
注2:可修改資料庫連線EDBHelper
等,作為Java服務端埋點統計
使用.
專案背景
統計資料 是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/