無埋點統計SDK實踐

tinycoder發表於2018-12-10

背景

埋點模組是一個完整的系統不可獲取的一部分,無論是移動端,Web端還是後端(後端可能傾向於叫日誌系統)。當然,現在也有很多第三方的埋點SDK,如友盟,接入也很簡單,只需要幾行程式碼即可使用。但大多都是侵入式,也就是說,在每個需要埋點的地方手動新增程式碼,這樣耦合性太大,雖然可通過二次封裝的方式,降低對這些SDK的依賴,但埋點統計模組耦合性仍然很大,為了解決這個問題,我們可通過無埋點方案來實現資料的收集過程。

埋點系統型別

目前的埋點系統,主要分為2種:侵入式和無埋點。還有一種視覺化的埋點方案,可認為是無埋點的一種,只是將設定埋點配置資訊的過程做成了視覺化而已。

侵入式埋點方案

在每個需要埋點的地方手動新增程式碼,優點是埋點準確,缺點也很明顯,程式碼耦合度高,後期難以維護,不需要的埋點需要手動刪除。

無埋點方案

無埋點方式是通過全域性監聽或AOP技術新增埋點的一種實現方案,開發者不需要在每個需要埋點的地方新增程式碼,只需要根據伺服器分發的配置,獲取相應的埋點資料即可。一方面程式碼耦合度低,同時靈活度也高,埋點資料直接由伺服器控制。缺點就是沒有侵入式埋點精準。

需要收集的資料

埋點的主要作用就是用於統計,對於埋點系統而言,最起碼需要收集以下資料:

  • 首次使用APP的新裝置資訊(精確控制還需要後端的配合);
  • 頁面的停留時長;
  • View的互動事件(點選,滑動等);
  • 輔助運營的各種資料(渠道號,地理位置,裝置資訊等)

埋點系統介紹

一個完整的埋點系統,應該至少包含以下三個模組:

網路模組

負責從伺服器獲取配置資訊,上傳埋點資料;

儲存模組

快取埋點配置資訊,儲存產生的埋點資料;

核心處理模組

負責收集埋點資料,並儲存在儲存模組中,根據配置在指定的時間上傳資料。

無埋點系統的工作原理

在APP啟動時,對無埋點SDK進行初始化,初始化的時候系統會先從配置中設定的URL請求埋點配置資訊,然後對Activity,Fragment,View進行全域性監聽,當有相應的事件產生時,通過與配置資訊比對,將需要收集的事件先將其儲存在資料庫中,到上傳時機時,從資料庫中獲取資料,然後上傳到伺服器,上傳成功後刪除資料庫的已上傳的內容。

無埋點系統的實現

無埋點系統的主要目標是降低開發人員對埋點過程的參與度,其核心在於如何對事件進行全域性監聽以及如何生成埋點配置列表。

頁面停留時長的監聽

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包中提供的全域性監聽中並沒有,所以還需要特殊處理一下。這裡提供兩種解決方案:

  • 提供一個LifycycleFragment, 對onHiddenChanged和setUserVisibleHint方法進行監聽,業務層的Fragment繼承此Fragment;
  • 使用AOP,監聽onHiddenChanged和setUserVisibleHint;

其中的處理邏輯與onResume和onPause中一致,具體參考後面的原始碼。

如果要對com.app包中的Fragment實現生命週期的全域性監聽,可採用以下兩種方式:

  • 寫一個LifycycleFragment, 在其中實現生命週期的監聽,業務層的Fragment實現時繼承此Fragment;
  • 使用透明的Fragment,透明的Fragment由於沒有UI,其生命週期會與當前Fragment生命週期一致;

由於Fragment總是依賴於Activity存在的,所以其監聽範圍也是Activity級別的。在Activity的onCreate中對Fragment設定監聽即可。

監聽View的點選事件

View點選事件的監聽可通過兩種方式來實現:

基於AOP監聽onClick方法;

這裡以Aspect為例,實現onClick的全域性監聽:

@Aspectpublic class ViewClickedEventAspect { 
@After("execution(* android.view.View.OnClickListener.onClick(android.view.View))") public void viewClicked(final ProceedingJoinPoint joinPoint) {
/** * 儲存點選事件 */
}
}複製程式碼
通過setAccessibilityDelegate實現:

關於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");
// 儲存埋點資料的URLTracker.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的點選事件的統計,對於其他的互動事件還未整合,一些細節方面也還有待改進,隨後會進一步完善。

原始碼地址

Tracker

參考文章

Android埋點技術分析

Android無埋點資料收集SDK關鍵技術

網易HubbleData之Android無埋點實踐

來源:https://juejin.im/post/5c0e4117518825369c566f07

相關文章