V1.0.0 功能列表 |
是否支持 |
---|---|
接口自定義 | 支持 |
緩存策略 | 支持 |
外部cookie注入 | 支持 |
推送週期設定 | 支持 |
強制推送 | 支持 |
自定義埋點事件 | 支持 |
獨立運行 | 支持 |
多線程寫入 | 支持 |
後臺線程服務 | 支持 |
注1:代碼已經通過線上項目驗證, 橫向Google統計對比,統計數據無丟失,性能穩定
.java
注2:可修改數據庫鏈接EDBHelper
等,做爲Java服務端埋點統計
使用.android
統計數據 是BI作大數據,智能推薦,千人千面,機器學習的 數據源和依據. 在這個app都是千人千面,智能推薦,ab流量測試的時代, 一個能夠根據BI部門的需求, 能夠自有定製的 數據統計上報, 就顯得很是重要.git
目前, 市面上 作統計的第三方平臺有不少, 好比最出名的Google的GTM統計,友盟統計等等.github
可是 這些統計, 第一點,就是上傳的頻率,比較固定, 難以知足要求不一樣的頻次需求. 第二點,須要統計到的字段和規則都是死板的,沒法定製.sql
目前GitHub上, 沒有一個 自定義的 統計SDK 思路和源碼.數據庫
我想,在這裏分享下,個人思路和代碼.編程
這裏有幾個要點json
作統計SDK的方式有這兩種api
1.用AOP的處理方式, 在方法內,插入統計代碼. 這種方式雖然在.java
文件裏 沒有代碼侵入,可是可定製行不高,只適合簡單的 統計需求.緩存
2.用普通的方法樣式,使用GTM.event(xxx)
方式,代碼侵入極高, 可是能夠實現高度自定義.
現階段, 我會採用第二種方式,爲了數據的精確要求,採用侵入式.
後續, 我會繼續思考,更好的實現方式. 也請你們一塊兒分享本身的思路.
由於統計規則業務定製性很強,沒法對傳送數據進行統一的抽象管理, 該項目就不單獨發佈到jcenter, 若是須要,能夠參考源碼思路, 本身修改源碼,修改數據載體,實現需求便可.
JJEvent設計初衷爲:一個統計SDK, 能夠單獨發佈到倉庫,單獨被項目依賴而不產生衝突,擁有本身的數據存儲,網絡請求.
這些都是能夠自定義的,修改源碼便可
固定週期進行上傳: 好比每2分鐘,進行一次數據上傳.數據爲 觸發推送的時間節點 以前的數據.用於大部分統計.
固定條數進行上傳: 好比每100條,進行一次數據上傳.數據爲 觸發 觸發100條推送開始 以前的數據.用於大部分統計.
實時上傳:每次點擊就進行push操做.數據爲 觸發推送的時間節點 以前的數據.用於特定統計.
這裏, 能夠根據BI的業務需求而定, 你們能夠在此基礎上修改.
支持自定義擴展
面嚮對象語言的特色: 就是要面向對象編程,面向接口編程.當你在抽象的過程當中,只關注某個對象是什麼,而後他擁有什麼屬性,什麼功能便可.不須要考慮其中的實現.這也就是Java乃至面嚮對象語言,爲啥這麼多類的緣由,這其中有單一職責原則,接口分隔原則.
模塊之間的依賴,應該最大程度的依賴抽象.
要想完整的把整個過程抽象清楚,須要對整個流程有個最大的認知.
複製代碼
思考:確定會想到這些東西,只不過想到的過程可能不一樣,並且每一個設計者,想法都不會同樣,實現過程也不同.
首先須要一個配置類Constant
,對常量,開關進行管理.
一個sdk有事件統計,那麼必需要有一個Event
類來進行屏幕值,事件
兩種統計動做.
統計事件發生後, 須要一個持久化過程DbHelper
,即須要一個數據庫支持存取.
如何推送呢? 須要創建一個後臺服務JJService
,對數據進行推送.
用什麼推送呢?確定須要網絡啊, 須要一個網絡模塊NetHelper
從數據庫中拿數據,進行推送.
推送的是什麼呢? 須要建一個任務Task
,讓task承載推送的過程.
如何將模塊進行鏈接,統一管理?
通過抓取數據庫數據快照 ,進行數據清洗,而後提供給機器學習,或者千人千面.
這裏若是有興趣,請配合源代碼.
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();//開始
}
複製代碼
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
的相關方法中進行修改.
剛開始想用模板方法
和繼承
來作,將CRUD
的實現放在宿主中,
可是, 因爲用戶不太清楚sdk內部實現邏輯,用戶維護sdk的成本過高.因此,我就從新裁剪了開源的XUtils
中的dbUtils
,而後修改類名,做爲db服務.
爲了減小UI線程的壓力, 有必要將數據操做放到子線程中. 考慮到數據量時大時小, 因此須要自定義一個線程池,來管理線程和縣城任務.
這裏, 最主要的就是 控制好線程的對共享變量的訪問鎖.保證線程的原子性和可見性.
將全部Event
任務,做爲一個Runable
,放到阻塞隊列中,讓線程池隊列執行.注意設置runable超時時間,異常處理.儘可能保證數據錄入成功.
要注意的是, Event
任務 執行有快有慢, 因此,最終保存到數據庫的時候, 並非按照隊列的順序.
對於變量 好比int eventNum=1;
線程在執行過程當中, 會將主內存區的變量,拷貝到線程內存中, 當修改完a
後,再將a的值返回到主內存中.這個時候,若是兩個線程同時修改該變量,第三個線程在訪問的時候,頗有可能a的值尚未改變.這個時候就會讓a的改變不可見
.因此,能夠用線程安全變量AtomicInteger
,或者原子性變量volatile
,讓他們咋發生改變的時候,馬上通知主內存中的變量.
對於方法 爲了保證線程間訪問方法互斥, 用synchronized
對線程訪問方法,進行同步.保證線程順序執行.即要將全部共通操做,放到一個加載器方法中,用synchronized
同步.
另外,避免線程濫用,性能浪費, 要仔細考量voliate
,synchronized
等字段的頻次.
詳情處理可見EventDecorator.java
中的 變量處理.
sqlite
數據庫是否 線程安全?目前, 統計sdk狀態是
多個線程同時執行數據庫操做,
Timer
擁有本身的單線程 執行數據庫讀取.
要保證數據庫使用的安全,通常能夠採用以下幾種模式
SQLite 採用單線程模型,用專門的線程/隊列(同時只能有一個任務執行訪問) 進行訪問 SQLite 採用多線程模型,每一個線程都使用各自的數據庫鏈接 (即 sqlite3 *) SQLite 採用串行模型,全部線程都共用同一個數據庫鏈接。
在本SDK中,採用串行模式,在初始化過程當中,SQLiteDatabase
靜態單例, 來保證線程安全.
項目通過測試部門,和線上檢驗,線程間訪問正確,數據統計正確.
首先,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);
複製代碼
Push
的邏輯比較複雜,因此更須要這個類,專門來作push任務.
請看以下push的邏輯.
通過測試部和線上數據驗證, 數據量統計無誤,沒有重複數據,沒有遺漏數據.
複製代碼
這應該是一個後臺服務模塊. 功能應該有 開啓服務,週期推送,主動推送,中止推送.
需不須要用一個不會被殺死的後臺服務?
答案是不須要,
1.從用戶體驗上講,一個系統殺不死的服務,是一個用戶體驗極差的處理方式.有些手機 甚至會提示,該app正在後臺運行.
2.從sdk必要屬性上講, 統計sdk,只有app在前臺的時候,纔會有事件統計.因此推送服務沒有必要一直存在.
3.當系統內存不足的時候, 會把後臺推送線程殺死. 可是殺死的僅僅是週期推送
,數據記錄並不會中止. 等待知足條件 (100條記錄),就會主動推送.
因此,結論是 推送服務,僅僅須要在用戶可見的狀況下,進行便可. 線程是否被殺死,影響的僅僅是推送到服務器是否及時.
通過考量, 採用Timer
+TimerTask
的方式,進行週期推送服務.由於 雖然Timer不保證任務執行的十分精確。 可是Timer類的線程安全的。
並且TimerTask
是在子線程中,不會push服務不會阻塞主線程.
sdk 對外暴露類和方法,要儘量少.只暴露用戶可操做的方法.隱藏其餘細節. 因此在這個sdk中,用戶只須要知道 設置必要參數,開啓,添加統計便可,其餘無需瞭解.
因此,我對訪問權限進行了處理,只公開如下類,以及相應方法.
JJEventManager
事件管理
JJEventManager.init()
初始化
JJEventManager.cancelEventPush()
取消推送
JJEventManager.destoryEventService()
終止全部服務
JJEvent
統計入口
JJEvent.event(String ec, String ea, String el)
事件
JJEvent.screen(String sn, LTPType ltp)
屏幕值
爲了保證sdk命名惟一性,採用全部必要模塊加前綴E
表明Event
的處理方式, 避免出如今業務層 查看調用出處的時候,形成誤解.好比
後期,在咱們作本身的業務線的時候,你們也能夠採用這種方法.
本身在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()
.就避免了後期牽一髮而動全身的問題.
===
CSDN:http://blog.csdn.net/ccj659/article/