1. 程式人生 > >無埋點統計SDK實踐

無埋點統計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的點選事件的統計,對於其他的互動事件還未整合,一些細節方面也還有待改進,隨後會進一步完善。

原始碼地址

Tracker

參考文章

Android埋點技術分析

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

網易HubbleData之Android無埋點實踐