無埋點統計SDK實踐

背景

埋點模塊是一個完整的系統不可獲取的一部分,不管是移動端,Web端仍是後端(後端可能傾向於叫日誌系統)。固然,如今也有不少第三方的埋點SDK,如友盟,接入也很簡單,只須要幾行代碼便可使用。但大多都是侵入式,也就是說,在每一個須要埋點的地方手動添加代碼,這樣耦合性太大,雖然可經過二次封裝的方式,下降對這些SDK的依賴,但埋點統計模塊耦合性仍然很大,爲了解決這個問題,咱們可經過無埋點方案來實現數據的收集過程。android

埋點系統類型

目前的埋點系統,主要分爲2種:侵入式和無埋點。還有一種可視化的埋點方案,可認爲是無埋點的一種,只是將設置埋點配置信息的過程作成了可視化而已。git

侵入式埋點方案

在每一個須要埋點的地方手動添加代碼,優勢是埋點準確,缺點也很明顯,代碼耦合度高,後期難以維護,不須要的埋點須要手動刪除。github

無埋點方案

無埋點方式是經過全局監聽或AOP技術添加埋點的一種實現方案,開發者不須要在每一個須要埋點的地方添加代碼,只須要根據服務器分發的配置,獲取相應的埋點數據便可。一方面代碼耦合度低,同時靈活度也高,埋點數據直接由服務器控制。缺點就是沒有侵入式埋點精準。數據庫

須要收集的數據

埋點的主要做用就是用於統計,對於埋點系統而言,最起碼須要收集如下數據:後端

  • 首次使用APP的新設備信息(精確控制還須要後端的配合);
  • 頁面的停留時長;
  • View的交互事件(點擊,滑動等);
  • 輔助運營的各類數據(渠道號,地理位置,設備信息等)

埋點系統介紹

一個完整的埋點系統,應該至少包含如下三個模塊:緩存

網絡模塊

負責從服務器獲取配置信息,上傳埋點數據;服務器

存儲模塊

緩存埋點配置信息,保存產生的埋點數據;網絡

核心處理模塊

負責收集埋點數據,並保存在存儲模塊中,根據配置在指定的時間上傳數據。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包中提供的全局監聽中並無,因此還須要特殊處理一下。這裏提供兩種解決方案:

  • 提供一個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的點擊事件的統計,對於其餘的交互事件還未集成,一些細節方面也還有待改進,隨後會進一步完善。

源碼地址

Tracker

參考文章

Android埋點技術分析

Android無埋點數據收集SDK關鍵技術

網易HubbleData之Android無埋點實踐

相關文章
相關標籤/搜索