無埋點統計SDK實踐
背景
埋點模組是一個完整的系統不可獲取的一部分,無論是移動端,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的全域性監聽:
@Aspect
public 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"); // 儲存埋點資料的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的點選事件的統計,對於其他的互動事件還未整合,一些細節方面也還有待改進,隨後會進一步完善。