埋點模塊是一個完整的系統不可獲取的一部分,不管是移動端,Web端仍是後端(後端可能傾向於叫日誌系統)。固然,如今也有不少第三方的埋點SDK,如友盟,接入也很簡單,只須要幾行代碼便可使用。但大多都是侵入式,也就是說,在每一個須要埋點的地方手動添加代碼,這樣耦合性太大,雖然可經過二次封裝的方式,下降對這些SDK的依賴,但埋點統計模塊耦合性仍然很大,爲了解決這個問題,咱們可經過無埋點方案來實現數據的收集過程。android
目前的埋點系統,主要分爲2種:侵入式和無埋點。還有一種可視化的埋點方案,可認爲是無埋點的一種,只是將設置埋點配置信息的過程作成了可視化而已。git
在每一個須要埋點的地方手動添加代碼,優勢是埋點準確,缺點也很明顯,代碼耦合度高,後期難以維護,不須要的埋點須要手動刪除。github
無埋點方式是經過全局監聽或AOP技術添加埋點的一種實現方案,開發者不須要在每一個須要埋點的地方添加代碼,只須要根據服務器分發的配置,獲取相應的埋點數據便可。一方面代碼耦合度低,同時靈活度也高,埋點數據直接由服務器控制。缺點就是沒有侵入式埋點精準。數據庫
埋點的主要做用就是用於統計,對於埋點系統而言,最起碼須要收集如下數據:後端
一個完整的埋點系統,應該至少包含如下三個模塊:緩存
負責從服務器獲取配置信息,上傳埋點數據;服務器
緩存埋點配置信息,保存產生的埋點數據;網絡
負責收集埋點數據,並保存在存儲模塊中,根據配置在指定的時間上傳數據。app
在APP啓動時,對無埋點SDK進行初始化,初始化的時候系統會先從配置中設置的URL請求埋點配置信息,而後對Activity,Fragment,View進行全局監聽,當有相應的事件產生時,經過與配置信息比對,將須要收集的事件先將其保存在數據庫中,到上傳時機時,從數據庫中獲取數據,而後上傳到服務器,上傳成功後刪除數據庫的已上傳的內容。ide
無埋點系統的主要目標是下降開發人員對埋點過程的參與度,其核心在於如何對事件進行全局監聽以及如何生成埋點配置列表。
Android應用中的頁面,也就Activity,Fragment兩種。對於Activity,系統了全局的生命週期監聽的方法,只須要在onResume中記錄頁面顯示時的時間,在onPause中計算顯示的時長,在onDestroy中將停留時長事件添加到數據庫便可:
application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
private Map<Context, Long> durationMap = new WeakHashMap<>();
private Map<Context, Long> resumeTimeMap = new WeakHashMap<>();
@Override
public void onActivityCreated(Activity activity, Bundle bundle) {
durationMap.put(activity, 0L);
}
@Override
public void onActivityResumed(Activity activity) {
resumeTimeMap.put(activity, System.currentTimeMillis());
}
@Override
public void onActivityPaused(Activity activity) {
durationMap.put(activity, durationMap.get(activity)
+ (System.currentTimeMillis() - resumeTimeMap.get(activity)));
}
@Override
public void onActivityDestroyed(Activity activity) {
long duration = durationMap.get(activity);
if (duration > 0) {
// 將事件添加到數據庫
}
resumeTimeMap.remove(activity);
durationMap.remove(activity);
}
// 其餘生命週期方法
});
複製代碼
而對於Fragment,雖然com.app包中的Fragment沒有提供生命週期的全局監聽,但25.1.0以後的v4包中提供了全局監聽,考慮到一般狀況下都使用v4包中的Fragment,因此這裏就直接使用了v4包中提供的方法來實現頁面停留時長的監聽。
FragmentManager fm = getSupportFragmentManager();
fm.registerFragmentLifecycleCallbacks(new FragmentManager.FragmentLifecycleCallbacks() {
private Map<Fragment, Long> resumeTimeMap = new WeakHashMap<>();
private Map<Fragment, Long> durationMap = new WeakHashMap<>();
@Override
public void onFragmentAttached(@NonNull FragmentManager fm, @NonNull Fragment f, @NonNull Context context) {
super.onFragmentAttached(fm, f, context);
resumeTimeMap.put(f, 0L);
}
@Override
public void onFragmentResumed(@NonNull FragmentManager fm, @NonNull Fragment f) {
super.onFragmentResumed(fm, f);
resumeTimeMap.put(f, System.currentTimeMillis());
}
@Override
public void onFragmentPaused(@NonNull FragmentManager fm, @NonNull Fragment f) {
super.onFragmentPaused(fm, f);
durationMap.put(f, durationMap.get(f) + System.currentTimeMillis() - resumeTimeMap.get(f));
}
@Override
public void onFragmentDetached(@NonNull FragmentManager fm, @NonNull Fragment f) {
super.onFragmentDetached(fm, f);
long duration = durationMap.get(f);
if (duration > 0) {
// 將事件添加到數據庫
}
resumeTimeMap.remove(f);
durationMap.remove(f);
}
}, true);
複製代碼
上面的代碼只是對Fragment生命週期的監聽,但Fragment的可見性與生命週期並不老是一一對應的,如:Fragment show/hide或者ViewPager中的Fragment在切換時生命週期中的方法並不老是執行的,因此還須要監聽與這兩種狀況對應的onHiddenChanged和setUserVisibleHint,但這兩個方v4包中提供的全局監聽中並無,因此還須要特殊處理一下。這裏提供兩種解決方案:
其中的處理邏輯與onResume和onPause中一致,具體參考後面的源碼。
若是要對com.app包中的Fragment實現生命週期的全局監聽,可採用如下兩種方式:
因爲Fragment老是依賴於Activity存在的,因此其監聽範圍也是Activity級別的。在Activity的onCreate中對Fragment設置監聽便可。
View點擊事件的監聽可經過兩種方式來實現:
這裏以Aspect爲例,實現onClick的全局監聽:
@Aspect
public class ViewClickedEventAspect {
@After("execution(* android.view.View.OnClickListener.onClick(android.view.View))")
public void viewClicked(final ProceedingJoinPoint joinPoint) {
/**
* 保存點擊事件
*/
}
}
複製代碼
關於setAccessibilityDelegate咱們可先看一下View點擊事件被執行的源碼:
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
複製代碼
從代碼中能夠看出,View的onClick被執行時,有個sendAccessibilityEvent被執行,咱們再看一下sendAccessibilityEvent方法的代碼:
public void sendAccessibilityEvent(int eventType) {
if (mAccessibilityDelegate != null) {
mAccessibilityDelegate.sendAccessibilityEvent(this, eventType);
} else {
sendAccessibilityEventInternal(eventType);
}
}
複製代碼
從代碼能夠看出,只須要爲View設置了mAccessibilityDelegate,咱們就能夠監聽View的onClick事件了。而設置View mAccessibilityDelegate的方法恰好是公開的,因此咱們可以使用此方式對View的點擊事件進行監聽,核心代碼以下:
public class ViewClickedEventListener extends View.AccessibilityDelegate {
/**
* 設置Activity頁面中View的事件監聽
* @param activity
*/
public void setActivityTracker(Activity activity) {
View contentView = activity.findViewById(android.R.id.content);
if (contentView != null) {
setViewClickedTracker(contentView, null);
}
}
/**
* 設置Fragment頁面中View的事件監聽
* @param fragment
*/
public void setFragmentTracker(Fragment fragment) {
View contentView = fragment.getView();
if (contentView != null) {
setViewClickedTracker(contentView, fragment);
}
}
private void setViewClickedTracker(View view, Fragment fragment) {
if (needTracker(view)) {
if (fragment != null) {
view.setTag(FRAGMENT_TAG_KEY, fragment);
}
view.setAccessibilityDelegate(this);
}
if (view instanceof ViewGroup) {
int childCount = ((ViewGroup) view).getChildCount();
for (int i = 0; i < childCount; i++) {
setViewClickedTracker(((ViewGroup) view).getChildAt(i), fragment);
}
}
}
@Override
public void sendAccessibilityEvent(View host, int eventType) {
super.sendAccessibilityEvent(host, eventType);
if (AccessibilityEvent.TYPE_VIEW_CLICKED == eventType && host != null) {
// 添加事件到數據庫
}
}
}
複製代碼
而後在Activity和Fragment的onResume中添加View的監聽便可。
事件的全局監聽已經實現了,理論上APP開發人員不須要參與埋點的過程,但後臺的統計並不須要全部的數據,因此這裏還須要添加埋點配置信息的收集。這裏提供了埋點數據實時上傳的功能,在APP上線前,將數據上傳策略修改爲實時上傳,便可將全部的事件信息經過Socket發送給後臺,而後將須要的數據導入到埋點配置信息列表中,APP上線後,會從服務器獲取埋點配置信息,在產生數據後,根據獲取的配置信息,保存須要的數據,到指定上傳時間時,將數據提交給服務器。
在Application的onCreate中進行初始化便可:
TrackerConfiguration configuration = new TrackerConfiguration()
.openLog(true)
.setUploadCategory(Constants.UPLOAD_CATEGORY.REAL_TIME.getValue())
.setConfigUrl("http://m.baidu.com") // 埋點配置信息的URL
.setHostName("127.0.0.1") // 接收實時埋點數據的IP和端口
.setHostPort(10001)
.setNewDeviceUrl("http://m.baidu.com") // 保存新設備信息的URL
.setUploadUrl("http://m.baidu.com"); // 保存埋點數據的URL
Tracker.getInstance().init(this, configuration);
複製代碼
在發佈版本以前,將上傳策略設置成Constants.UPLOAD_CATEGORY.REAL_TIME收集埋點配置信息,APP上線時務必將數據上傳策略改爲其餘的,避免耗電。
對於埋點數據的上傳,提供瞭如下策略:
REAL_TIME(0), // 實時傳輸,用於收集配置信息
NEXT_LAUNCH(-1), // 下次啓動時上傳
NEXT_15_MINUTER(15), // 每15分鐘上傳一次
NEXT_30_MINUTER(30), // 每30分鐘上傳一次
NEXT_KNOWN_MINUTER(-1); // 使用服務器下發的上傳策略(間隔時間由服務器決定)
複製代碼
目前此SDK只集成了新設備信息,頁面(Activity/Fragment)的停留事件,View的點擊事件的統計,對於其餘的交互事件還未集成,一些細節方面也還有待改進,隨後會進一步完善。