- 本文是原理介紹
- 這里是如何使用傳送門(mén)
- 這里是源碼地址
V1.0.0 功能列表 |
是否支持 |
---|---|
接口自定義 | 支持 |
緩存策略 | 支持 |
外部cookie注入 | 支持 |
推送周期設(shè)定 | 支持 |
強(qiáng)制推送 | 支持 |
自定義埋點(diǎn)事件 | 支持 |
獨(dú)立運(yùn)行 | 支持 |
多線程寫(xiě)入 | 支持 |
后臺(tái)線程服務(wù) | 支持 |
注:代碼已經(jīng)經(jīng)過(guò)線上項(xiàng)目驗(yàn)證, 橫向Google統(tǒng)計(jì)對(duì)比,統(tǒng)計(jì)數(shù)據(jù)無(wú)丟失,性能穩(wěn)定.
項(xiàng)目背景
統(tǒng)計(jì)數(shù)據(jù) 是BI做大數(shù)據(jù),智能推薦,千人千面,機(jī)器學(xué)習(xí)的 數(shù)據(jù)源和依據(jù).
在這個(gè)app都是千人千面,智能推薦,ab流量測(cè)試的時(shí)代, 一個(gè)可以根據(jù)BI部門(mén)的需求, 可以自有定制的 數(shù)據(jù)統(tǒng)計(jì)上報(bào), 就顯得非常重要.
目前, 市面上 做統(tǒng)計(jì)的第三方平臺(tái)有很多, 比如最出名的Google的GTM統(tǒng)計(jì),友盟統(tǒng)計(jì)等等.
但是 這些統(tǒng)計(jì), 第一點(diǎn),就是上傳的頻率,比較固定, 難以滿(mǎn)足要求不同的頻次需求. 第二點(diǎn),需要統(tǒng)計(jì)到的字段和規(guī)則都是死板的,無(wú)法定制.
目前GitHub上, 沒(méi)有一個(gè) 自定義的 統(tǒng)計(jì)SDK 思路和源碼.
我想,在這里分享下,我的思路和代碼.
這里有幾個(gè)要點(diǎn)
- 統(tǒng)計(jì)分類(lèi):統(tǒng)計(jì)分為屏幕值,事件兩種,后續(xù)可能擴(kuò)展.
- 統(tǒng)計(jì)規(guī)則: 支持簡(jiǎn)單Google統(tǒng)計(jì)方式,支持自定義字段.
- 推送方式:每?jī)煞昼娚蟼鞯椒?wù)器,
- 作為sdk,可以單獨(dú)集成,獨(dú)立運(yùn)行.
這是一個(gè)什么樣的統(tǒng)計(jì)SDK?
做統(tǒng)計(jì)SDK的方式有這兩種
1.用AOP的處理方式, 在方法內(nèi),插入統(tǒng)計(jì)代碼. 這種方式雖然在.java
文件里 沒(méi)有代碼侵入,但是可定制行不高,只適合簡(jiǎn)單的 統(tǒng)計(jì)需求.
2.用普通的方法樣式,使用GTM.event(xxx)
方式,代碼侵入極高, 但是可以實(shí)現(xiàn)高度自定義.
現(xiàn)階段, 我會(huì)采用第二種方式,為了數(shù)據(jù)的精確要求,采用侵入式.
后續(xù), 我會(huì)繼續(xù)思考,更好的實(shí)現(xiàn)方式. 也請(qǐng)大家一起分享自己的思路.
因?yàn)榻y(tǒng)計(jì)規(guī)則業(yè)務(wù)定制性很強(qiáng),無(wú)法對(duì)傳送數(shù)據(jù)進(jìn)行統(tǒng)一的抽象管理, 該項(xiàng)目就不單獨(dú)發(fā)布到j(luò)center,
如果需要,可以參考源碼思路, 自己修改源碼,修改數(shù)據(jù)載體,實(shí)現(xiàn)需求即可.
JJEvent設(shè)計(jì)初衷為:一個(gè)統(tǒng)計(jì)SDK, 可以單獨(dú)發(fā)布到倉(cāng)庫(kù),單獨(dú)被項(xiàng)目依賴(lài)而不產(chǎn)生沖突,擁有自己的數(shù)據(jù)存儲(chǔ),網(wǎng)絡(luò)請(qǐng)求.
1.上傳規(guī)則
這些都是可以自定義的,修改源碼即可
固定周期進(jìn)行上傳: 比如每2分鐘,進(jìn)行一次數(shù)據(jù)上傳.數(shù)據(jù)為 觸發(fā)推送的時(shí)間節(jié)點(diǎn) 之前的數(shù)據(jù).用于大部分統(tǒng)計(jì).
固定條數(shù)進(jìn)行上傳: 比如每100條,進(jìn)行一次數(shù)據(jù)上傳.數(shù)據(jù)為 觸發(fā) 觸發(fā)100條推送開(kāi)始 之前的數(shù)據(jù).用于大部分統(tǒng)計(jì).
- 實(shí)時(shí)上傳:每次點(diǎn)擊就進(jìn)行push操作.數(shù)據(jù)為 觸發(fā)推送的時(shí)間節(jié)點(diǎn) 之前的數(shù)據(jù).用于特定統(tǒng)計(jì).
2.統(tǒng)計(jì)分類(lèi)
這里, 可以根據(jù)BI的業(yè)務(wù)需求而定, 大家可以在此基礎(chǔ)上修改.
1.PV(PageView) 屏幕事件
- sn(screen) 屏幕名稱(chēng) 遵循舊策略(Android/好價(jià)/好價(jià)詳情頁(yè)/title).
- ltp 屏幕加載方式 下拉刷新=1、翻頁(yè)=2、標(biāo)簽切換=3、局部彈屏4矿辽、篩選刷新=5.
- ecp 自定義事件 ,json map存儲(chǔ).
2.Event 點(diǎn)擊事件
- ec(event category) 事件類(lèi)別
- ea(event action) 事件操作
- el(event lable) 事件標(biāo)簽
- ecp 自定義事件 ,json map存儲(chǔ).
3.expose曝光 事件
- url 曝光url
- ecp 自定義事件 ,json map存儲(chǔ).
4. 其他事件
支持自定義擴(kuò)展
SDK抽象過(guò)程
面向?qū)ο笳Z(yǔ)言的特點(diǎn): 就是要面向?qū)ο缶幊?面向接口編程.當(dāng)你在抽象的過(guò)程中,只關(guān)注某個(gè)對(duì)象是什么,然后他擁有什么屬性,什么功能即可.不需要考慮其中的實(shí)現(xiàn).這也就是Java乃至面向?qū)ο笳Z(yǔ)言,為啥這么多類(lèi)的原因,這其中有單一職責(zé)原則,接口分隔原則.
模塊之間的依賴(lài),應(yīng)該最大程度的依賴(lài)抽象.
要想完整的把整個(gè)過(guò)程抽象清楚,需要對(duì)整個(gè)流程有個(gè)最大的認(rèn)知.
判斷邏輯,技術(shù)選型
思考:肯定會(huì)想到這些東西,只不過(guò)想到的過(guò)程可能不同,而且每個(gè)設(shè)計(jì)者,想法都不會(huì)一樣,實(shí)現(xiàn)過(guò)程也不一樣.
首先需要一個(gè)配置類(lèi)Constant
,對(duì)常量,開(kāi)關(guān)進(jìn)行管理.
一個(gè)sdk有事件統(tǒng)計(jì),那么必須要有一個(gè)Event
類(lèi)來(lái)進(jìn)行屏幕值,事件
兩種統(tǒng)計(jì)動(dòng)作.
統(tǒng)計(jì)事件發(fā)生后, 需要一個(gè)持久化過(guò)程DbHelper
,即需要一個(gè)數(shù)據(jù)庫(kù)支持存取.
如何推送呢? 需要建立一個(gè)后臺(tái)服務(wù)JJService
,對(duì)數(shù)據(jù)進(jìn)行推送.
用什么推送呢?肯定需要網(wǎng)絡(luò)啊, 需要一個(gè)網(wǎng)絡(luò)模塊NetHelper
從數(shù)據(jù)庫(kù)中拿數(shù)據(jù),進(jìn)行推送.
推送的是什么呢? 需要建一個(gè)任務(wù)Task
,讓task承載推送的過(guò)程.
如何將模塊進(jìn)行連接,統(tǒng)一管理?
SDK整體架構(gòu)
1.統(tǒng)計(jì)客戶(hù)端SDK架構(gòu)圖
2.服務(wù)端數(shù)據(jù)收集采用的是
- openresty實(shí)現(xiàn)客戶(hù)端日志上報(bào)接口
- flume實(shí)現(xiàn)日志采集發(fā)送kafka
- 最終落地到硬盤(pán)
3. 大數(shù)據(jù)端
經(jīng)過(guò)抓取數(shù)據(jù)庫(kù)數(shù)據(jù)快照 ,進(jìn)行數(shù)據(jù)清洗,然后提供給機(jī)器學(xué)習(xí),或者千人千面.
模塊建設(shè)
這里如果有興趣,請(qǐng)配合源代碼.
1.JJEventManager
管理模塊
首先,sdk的生命周期是整個(gè)application的周期,所以我讓sdk 持有application 上下文,不會(huì)存在內(nèi)存泄漏.所以,我考慮將全局上下文放在這里管理.當(dāng)其他位置需要的時(shí)候到JJEventManager .getContext()
取值.
作為管理類(lèi),需要擁有控制sdk完整生命周期的功能.即init()
,cancelPush()
,destroy()
等方法.讓各個(gè)模塊的生命周期在這里管理.
然后考慮到,讓用戶(hù)可以動(dòng)態(tài)配置各種參數(shù),比如周期,是否是debug模式,主動(dòng)推送周期等等.所以在內(nèi)部使用buider模式,進(jìn)行動(dòng)態(tài)構(gòu)建.
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)//多少條 就主動(dòng)進(jìn)行push
.start();//開(kāi)始
}
2.Event
動(dòng)作模塊
動(dòng)作類(lèi),統(tǒng)計(jì)只有兩個(gè)動(dòng)作,即兩個(gè)方法screen ()
,event()
,以及一些重載方法.
因?yàn)槭枪_(kāi)類(lèi),所以要做到簡(jiǎn)潔,注釋要到位..(導(dǎo)入項(xiàng)目中的jar包,沒(méi)有Java document..因?yàn)閐oc生成在本地..云端沒(méi)有)
由于是數(shù)據(jù)入口類(lèi),所有堅(jiān)決不能存在崩潰的情況發(fā)生.
所以在相應(yīng)的地方加上了try catch
處理.
/**
* 統(tǒng)計(jì)入口
* Created by chenchangjun on 18/2/8.
*/
public final class JJEvent {
/**
* pageview 屏幕值
* @param sn screen 屏幕值,例`Android/主頁(yè)/推薦`
* @param ltp 屏幕加載方式
*/
public static void screen(String sn, LTPType ltp) {
screen(sn, ltp, null);
}
/**
* pageview 屏幕值
* @param sn screen 屏幕值,例`Android/主頁(yè)/推薦`
* @param ltp 屏幕加載方式
* @param ecp event custom Parameters 自定義參數(shù)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());
}
}
將處理細(xì)節(jié)交給其他類(lèi)處理,這里我用了一個(gè) Event
包裝類(lèi)EventDecorator
來(lái)做EventBean
中統(tǒng)一的數(shù)據(jù)緩存,參數(shù)值處理.遵循單一職責(zé)原則.
注意:
在修改數(shù)據(jù)體EventBean
來(lái)滿(mǎn)足業(yè)務(wù)需求時(shí), 請(qǐng)?jiān)?code>EventDecorator的相關(guān)方法中進(jìn)行修改.
3.DBHelper模塊
剛開(kāi)始想用模板方法
和繼承
來(lái)做,將CRUD
的實(shí)現(xiàn)放在宿主中,
但是, 由于用戶(hù)不太清楚sdk內(nèi)部實(shí)現(xiàn)邏輯,用戶(hù)維護(hù)sdk的成本太高.所以,我就重新裁剪了開(kāi)源的XUtils
中的dbUtils
,然后修改類(lèi)名,作為db服務(wù).
4.ThreadPool模塊
為了減少UI線程的壓力, 有必要將數(shù)據(jù)操作放到子線程中. 考慮到數(shù)據(jù)量時(shí)大時(shí)小, 所以需要自定義一個(gè)線程池,來(lái)管理線程和縣城任務(wù).
這里, 最主要的就是 控制好線程的對(duì)共享變量的訪問(wèn)鎖.保證線程的原子性和可見(jiàn)性.
將所有Event
任務(wù),作為一個(gè)Runable
,放到阻塞隊(duì)列中,讓線程池隊(duì)列執(zhí)行.注意設(shè)置runable超時(shí)時(shí)間,異常處理.盡量保證數(shù)據(jù)錄入成功.
要注意的是, Event
任務(wù) 執(zhí)行有快有慢, 所以,最終保存到數(shù)據(jù)庫(kù)的時(shí)候, 并不是按照隊(duì)列的順序.
4.1 如何保證線程安全?
對(duì)于變量
比如int eventNum=1;
線程在執(zhí)行過(guò)程中, 會(huì)將主內(nèi)存區(qū)的變量,拷貝到線程內(nèi)存中, 當(dāng)修改完a
后,再將a的值返回到主內(nèi)存中.這個(gè)時(shí)候,如果兩個(gè)線程同時(shí)修改該變量,第三個(gè)線程在訪問(wèn)的時(shí)候,很有可能a的值還沒(méi)有改變.這個(gè)時(shí)候就會(huì)讓a的改變不可見(jiàn)
.所以,可以用線程安全變量AtomicInteger
,或者原子性變量volatile
,讓他們咋發(fā)生改變的時(shí)候,立刻通知主內(nèi)存中的變量.
對(duì)于方法
為了保證線程間訪問(wèn)方法互斥, 用synchronized
對(duì)線程訪問(wèn)方法,進(jìn)行同步.保證線程順序執(zhí)行.即要將所有共通操作,放到一個(gè)加載器方法中,用synchronized
同步.
另外,避免線程濫用,性能浪費(fèi), 要仔細(xì)考量voliate
,synchronized
等字段的頻次.
詳情處理可見(jiàn)EventDecorator.java
中的 變量處理.
4.2 sqlite
數(shù)據(jù)庫(kù)是否 線程安全?
目前, 統(tǒng)計(jì)sdk狀態(tài)是
多個(gè)線程同時(shí)執(zhí)行數(shù)據(jù)庫(kù)操作,
Timer
擁有自己的單線程 執(zhí)行數(shù)據(jù)庫(kù)讀取.
要保證數(shù)據(jù)庫(kù)使用的安全源请,一般可以采用如下幾種模式
SQLite 采用單線程模型眠副,用專(zhuān)門(mén)的線程/隊(duì)列(同時(shí)只能有一個(gè)任務(wù)執(zhí)行訪問(wèn)) 進(jìn)行訪問(wèn)
SQLite 采用多線程模型荚醒,每個(gè)線程都使用各自的數(shù)據(jù)庫(kù)連接 (即 sqlite3 *)
SQLite 采用串行模型,所有線程都共用同一個(gè)數(shù)據(jù)庫(kù)連接哩牍。
在本SDK中,采用串行模式,在初始化過(guò)程中,SQLiteDatabase
靜態(tài)單例, 來(lái)保證線程安全.
項(xiàng)目經(jīng)過(guò)測(cè)試部門(mén),和線上檢驗(yàn),線程間訪問(wèn)正確,數(shù)據(jù)統(tǒng)計(jì)正確.
5.NetHelper模塊
首先,net請(qǐng)求,我裁剪的是volley.
NetHelper
應(yīng)該采用的是靜態(tài)或者單例,采用單例的原因是,他的生命周期和application同級(jí).功能應(yīng)該是 接受數(shù)據(jù),然后推送數(shù)據(jù),最后暴露告知結(jié)果.封裝里面的請(qǐng)求轉(zhuǎn)發(fā)邏輯.
NetHelper
網(wǎng)絡(luò)模塊,應(yīng)該有一個(gè)請(qǐng)求隊(duì)列(避免請(qǐng)求數(shù)據(jù)錯(cuò)亂),,還應(yīng)該提供針對(duì)不同EventType進(jìn)行不同處理請(qǐng)求的方法,然后還需要一個(gè)統(tǒng)一的網(wǎng)絡(luò)請(qǐng)求監(jiān)聽(tīng).
為了保證 推送不出現(xiàn)數(shù)據(jù)錯(cuò)亂,應(yīng)該在上一次網(wǎng)絡(luò)訪問(wèn)沒(méi)有結(jié)束前,不能繼續(xù)訪問(wèn)的鎖,用鎖isLoading
來(lái)控制.
將 請(qǐng)求分發(fā)邏輯,是否正在請(qǐng)求,以及監(jiān)聽(tīng)完全封裝在里面.對(duì)外只暴露OnNetResponseListener
.
按照上述邏輯,調(diào)用方式是這樣的.簡(jiǎn)單實(shí)用.
ENetHelper.create(JJEventManager.getContext(), new OnNetResponseListener() {
@Override
public void onPushSuccess() {
//5*請(qǐng)求成功,返回值正確, 刪除`cut_point_date`之前的數(shù)據(jù)
EDBHelper.deleteEventListByDate(cut_point_date);
}
@Override
public void onPushEorr(int errorCode) {
//.請(qǐng)求成功,返回值錯(cuò)誤,根據(jù)接口返回值,進(jìn)行處理.
}
@Override
public void onPushFailed() {
//請(qǐng)求失敗;不做處理.
}
}).sendEvent(EConstant.EVENT_TYPE_DEFAULT, list);
6. EPushTask模塊
Push
的邏輯比較復(fù)雜,所以更需要這個(gè)類(lèi),專(zhuān)門(mén)來(lái)做push任務(wù).
6.1 如何保證 數(shù)據(jù) 推送不會(huì)出現(xiàn)重復(fù)推送,或者缺少數(shù)據(jù)?
請(qǐng)看如下push的邏輯.
經(jīng)過(guò)測(cè)試部和線上數(shù)據(jù)驗(yàn)證, 數(shù)據(jù)量統(tǒng)計(jì)無(wú)誤,沒(méi)有重復(fù)數(shù)據(jù),沒(méi)有遺漏數(shù)據(jù).
7.EPushService模塊
這應(yīng)該是一個(gè)后臺(tái)服務(wù)模塊. 功能應(yīng)該有 開(kāi)啟服務(wù),周期推送,主動(dòng)推送,停止推送.
需不需要用一個(gè)不會(huì)被殺死的后臺(tái)服務(wù)?
答案是不需要,
1.從用戶(hù)體驗(yàn)上講,一個(gè)系統(tǒng)殺不死的服務(wù),是一個(gè)用戶(hù)體驗(yàn)極差的處理方式.有些手機(jī) 甚至?xí)崾?該app正在后臺(tái)運(yùn)行.
2.從sdk必要屬性上講, 統(tǒng)計(jì)sdk,只有app在前臺(tái)的時(shí)候,才會(huì)有事件統(tǒng)計(jì).所以推送服務(wù)沒(méi)有必要一直存在.
3.當(dāng)系統(tǒng)內(nèi)存不足的時(shí)候, 會(huì)把后臺(tái)推送線程殺死. 但是殺死的僅僅是周期推送
,數(shù)據(jù)記錄并不會(huì)停止. 等待滿(mǎn)足條件 (100條記錄),就會(huì)主動(dòng)推送.
所以,結(jié)論是 推送服務(wù),僅僅需要在用戶(hù)可見(jiàn)的情況下,進(jìn)行即可. 線程是否被殺死,影響的僅僅是推送到服務(wù)器是否及時(shí).
經(jīng)過(guò)考量, 采用Timer
+TimerTask
的方式,進(jìn)行周期推送服務(wù).因?yàn)?雖然Timer不保證任務(wù)執(zhí)行的十分精確。 但是Timer類(lèi)的線程安全的令漂。
而且TimerTask
是在子線程中,不會(huì)push服務(wù)不會(huì)阻塞主線程.
sdk整體框架調(diào)整
1.訪問(wèn)權(quán)限
sdk 對(duì)外暴露類(lèi)和方法,要盡可能少.只暴露用戶(hù)可操作的方法.隱藏其他細(xì)節(jié).
所以在這個(gè)sdk中,用戶(hù)只需要知道 設(shè)置必要參數(shù),開(kāi)啟,添加統(tǒng)計(jì)即可,其他無(wú)需了解.
所以,我對(duì)訪問(wèn)權(quán)限進(jìn)行了處理,只公開(kāi)以下類(lèi),以及相應(yīng)方法.
-
JJEventManager
事件管理JJEventManager.init()
初始化JJEventManager.cancelEventPush()
取消推送JJEventManager.destoryEventService()
終止所有服務(wù)
-
JJEvent
統(tǒng)計(jì)入口JJEvent.event(String ec, String ea, String el)
事件JJEvent.screen(String sn, LTPType ltp)
屏幕值
3.sdk唯一性
為了保證sdk命名唯一性,采用所有必要模塊加前綴E
代表Event
的處理方式,
避免出現(xiàn)在業(yè)務(wù)層 查看調(diào)用出處的時(shí)候,造成誤解.比如
后期,在我們做自己的業(yè)務(wù)線的時(shí)候,大家也可以采用這種方法.
2.sdk生成,版本管理,混淆打包
自己在gradle中寫(xiě)了一個(gè)打包腳本,讓打包的過(guò)程,自動(dòng)化.詳情見(jiàn)源碼.
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")
當(dāng)然, 也可以將sdk放到Nexus
Maven倉(cāng)庫(kù),或者公司私有倉(cāng)庫(kù),進(jìn)行api
依賴(lài).
2.3 sdk需不需要混淆?
這個(gè)問(wèn)題我考慮了很久, sdk給自己用,用的著混淆嘛? 混淆會(huì)不會(huì)讓同事們可讀性變差,想到最后,發(fā)現(xiàn)app上線前,也需要打包混淆.如果我在app的progurd.rules
中,添加各種規(guī)則,那么sdk用起來(lái)很繁瑣.
so~ , 我在 jar 包打包前,進(jìn)行了必要混淆,keep了兩個(gè)公開(kāi)類(lèi).
現(xiàn)在,在任何app如果想使用sdk, 那么只需要 app的progurd.rules
中添加兩句混淆規(guī)則即可.
-dontwarn com.ccj.client.android.analyticlib.**
-keep class com.ccj.client.android.analytics.**{*;}
總結(jié)思考
- 在本sdk中,
由于所有動(dòng)作的生命周期,是全局周期,所以,選擇了sdk持有applicatin
上下文進(jìn)行操作.
對(duì)于需要上下文的地方,直接用持有applicatin
,可以考慮
DBHelper中方法是靜態(tài)的,由于依賴(lài)于其中Java靜態(tài)方法,不能被靜態(tài)實(shí)現(xiàn)..,所以依賴(lài)的實(shí)現(xiàn).后期可以采用單例進(jìn)行處理.
- 無(wú)從下手的感覺(jué)...無(wú)從下手的感覺(jué)的根本原因就是你沒(méi)有下手去做..寫(xiě)寫(xiě),畫(huà)畫(huà),慢慢就會(huì)了然于胸.
后期優(yōu)化
為了操作方便,直接讓EDBHelper
,ENetHelper
直接作為靜態(tài)類(lèi)...
后期可以用單例取代.在管理類(lèi)JJEventManager
中,統(tǒng)一初始化.這樣,就可以 依賴(lài)抽象.比如持有DBDao.saveEvent()
,而不是用實(shí)現(xiàn)類(lèi)EDBHelper.saveEvent()
.就避免了后期牽一發(fā)而動(dòng)全身的問(wèn)題.
About Me
CSDN:http://blog.csdn.net/ccj659/article/