前言:
上一篇文章 主要介紹了埋點的基本概念以及幾種埋點技術(shù)實現(xiàn)方式的原理和差異,本篇文章是自動化埋點技術(shù)探索的第一篇,主要介紹的是頁面瀏覽事件、APP在前臺還是在后臺 這兩類事件在無埋點技術(shù)的理論分析和實踐
無埋點技術(shù) - 處理頁面瀏覽事件
AppViewScreen 事件,即頁面瀏覽事件。在 Android 中,頁面瀏覽,其實就是指切換不同的 Activity。生命周期對于一個 Activity的意義非凡,那什么是生命周期?生命周期通俗地理解為“從搖籃到墳墓”(Cradle-to-Grave)的整個過程 。通過對 Activity 的生命周期了解可知,頁面的瀏覽事件其實就是指的Activity中的onResume()。因為Activity執(zhí)行了onResume()就表示 已經(jīng)出現(xiàn)在前臺并開始活動,那么如何監(jiān)聽頁面的瀏覽事件?因為埋點是基于可點擊事件來執(zhí)行內(nèi)部邏輯,因此只有首先找到當前正在運行的Activity才可以進行后續(xù)的操作。
問題:
那有沒有一種方案,可以對 Activity 的所有生命周期事件進行集中處理(或者叫監(jiān)聽)?
解決思路:
ActivityLifecycleCallbacks 是 Application 的一個內(nèi)部接口,從 API 14 開始提供的。Application 通過此接口提供了一套回調(diào)方法,用于讓開發(fā)者可以對 Activity 的所有生命周期事件進行集中處理,首先看一下ActivityLifecycleCallbacks 的內(nèi)部源碼
public interface ActivityLifecycleCallbacks {
void onActivityCreated(Activity activity, Bundle savedInstanceState);
void onActivityStarted(Activity activity);
void onActivityResumed(Activity activity);
void onActivityPaused(Activity activity);
void onActivityStopped(Activity activity);
void onActivitySaveInstanceState(Activity activity, Bundle outState);
void onActivityDestroyed(Activity activity);
}
ActivityLifecycleCallbacks 的簡單使用如下:
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
this.registerActivityLifecycleCallbacks(new StuActivityLifecycle());
}
private class StuActivityLifecycle implements ActivityLifecycleCallbacks {
@Override
public void onActivityCreated(Activity activity, Bundle bundle) {
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
}
}
你可能會問,為什么Activity生命周期方法只要調(diào)用了,就會觸發(fā)Application內(nèi)部ActivityLifecycleCallbacks對應的方法 ?
解決問題最好的方式就是去探索Activity以及Application的系統(tǒng)源碼。舉例,以Activity的onCreate(),下面是它的源碼:
@MainThread
@CallSuper
protected void onCreate(@Nullable Bundle savedInstanceState) {
if (DEBUG_LIFECYCLE) Slog.v(TAG, "onCreate " + this + ": " + savedInstanceState);
if (mLastNonConfigurationInstances != null) {
mFragments.restoreLoaderNonConfig(mLastNonConfigurationInstances.loaders);
}
if (mActivityInfo.parentActivityName != null) {
if (mActionBar == null) {
mEnableDefaultActionBarUp = true;
} else {
mActionBar.setDefaultDisplayHomeAsUpEnabled(true);
}
}
if (savedInstanceState != null) {
mAutoFillResetNeeded = savedInstanceState.getBoolean(AUTOFILL_RESET_NEEDED, false);
mLastAutofillId = savedInstanceState.getInt(LAST_AUTOFILL_ID,
View.LAST_APP_AUTOFILL_ID);
if (mAutoFillResetNeeded) {
getAutofillManager().onCreate(savedInstanceState);
}
Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
mFragments.restoreAllState(p, mLastNonConfigurationInstances != null
? mLastNonConfigurationInstances.fragments : null);
}
mFragments.dispatchCreate();
getApplication().dispatchActivityCreated(this, savedInstanceState);
if (mVoiceInteractor != null) {
mVoiceInteractor.attachActivity(this);
}
mRestoredFromBundle = savedInstanceState != null;
mCalled = true;
}
由于這里主要是分析Activity與Application之間的關(guān)系,那么,快速定位到這樣一行代碼: getApplication().dispatchActivityCreated(this, savedInstanceState);
可以看到,這里調(diào)用了Application的dispatchActivityCreated方法,dispatchActivityCreated源碼如下:
void dispatchActivityCreated(Activity activity, Bundle savedInstanceState) {
Object[] callbacks = collectActivityLifecycleCallbacks();
if (callbacks != null) {
for (int i=0; i<callbacks.length; i++) {
((ActivityLifecycleCallbacks)callbacks[i]).onActivityCreated(activity,
savedInstanceState);
}
}
}
這里出現(xiàn)了collectActivityLifecycleCallbacks(),那Application內(nèi)部的collectActivityLifecycleCallbacks這個方法又是做什么的,繼續(xù)點進源碼:
private Object[] collectActivityLifecycleCallbacks() {
Object[] callbacks = null;
synchronized (mActivityLifecycleCallbacks) {
if (mActivityLifecycleCallbacks.size() > 0) {
callbacks = mActivityLifecycleCallbacks.toArray();
}
}
return callbacks;
}
原來,這里做了一個賦值數(shù)組的操作,而且進行了同步代碼塊的操作,同步代碼塊的目的是為了解決并發(fā)操作可能造成的異常(因為監(jiān)聽的是所有的Activity)。源碼出現(xiàn)的 mActivityLifecycleCallbacks,實際上是Application內(nèi)部定義的一個私有成員變量:
private ArrayList<ActivityLifecycleCallbacks> mActivityLifecycleCallbacks =
new ArrayList<ActivityLifecycleCallbacks>();
大家都知道,ArrayList 是 java 集合框架中比較常用的數(shù)據(jù)結(jié)構(gòu)。它繼承自 AbstractList,實現(xiàn)了 List 接口。底層是基于數(shù)組實現(xiàn)容量大小動態(tài)變化,允許 null 的存在。那么,這里定義的ArrayList 又是如何進行添加和刪除的?
這是Application內(nèi)部的一段代碼,一目了然:
public void registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback) {
synchronized (mActivityLifecycleCallbacks) {
mActivityLifecycleCallbacks.add(callback);
}
}
public void unregisterActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback) {
synchronized (mActivityLifecycleCallbacks) {
mActivityLifecycleCallbacks.remove(callback);
}
}
綜上所述,由于Activity每一個生命周期都對應 ActivityLifecycleCallbacks 接口中的一個方法,如上面提到的 onActivityCreated 回調(diào)是在 Activity 的 onCreate 方法中調(diào)用 getApplication().dispatchActivityCreated(this, savedInstanceState) 完成對 Activity 生命周期跟蹤監(jiān)聽;另外,Application 也是可以 register 多個ActivityLifecycleCallbacks 這也是源碼提到的
值得一提的是,ActivityLifecycleCallbacks 還有以下功能:
- 應用新開進程假重啟處理(低內(nèi)存回收、修改權(quán)限)
- 管理 Activity 頁面棧
- 獲取當前 Activity 頁面
- 判斷應用前后臺
- 保存恢復狀態(tài)值 savedInstanceState
- 頁面分析統(tǒng)計埋點
解決方案:
那么,針對頁面瀏覽事件在無埋點技術(shù)上的實現(xiàn),就可以有以下步驟:
- 在應用程序自定義的 Application 對象的 onCreate() 方法中初始化埋點 SDK,并傳入當前的 Application 對象。
- SDK 拿到 Application 對象之后,通過registerActivityLifecycleCallback 方法注冊 Application.ActivityLifecycleCall-backs。這樣 SDK 就能對 App 中所有的 Activity 的生命周期事件進行集中處理(監(jiān)控)了。
- 在注冊的 Application.ActivityLifecycleCallbacks 的 onActivityRe-
sumed 回調(diào)方法中,就可以拿到當前正在顯示的 Activity 對象,接著調(diào)用
SDK 的相關(guān)接口,來觸發(fā)頁面瀏覽事件
無埋點技術(shù) - 處理AppStart、AppEnd事件
所謂的AppStart、AppEnd事件實際上就是判斷當前 App 是處于前臺還是處于后臺。而 Android 系統(tǒng)本身沒有給 App 提供相關(guān)的接口來判斷這些狀態(tài)。
問題:
一般來說,判斷當前 App 是處于前臺還是后臺首先必須要面對2個問題:
- App 有多個進程該如何判斷?
- App 崩潰或者被強殺該如何判斷?
解決思路:
ContentProvider (內(nèi)容提供者)屬于 Android四大組件之一,它的主要作用是進程間進行數(shù)據(jù)交互 & 共享,即跨進程通信。ContentProvider的底層是采用 Binder機制,設(shè)計成Binder機制的作用前面也提到了就是解決跨進程的數(shù)據(jù)共享問題。另外,Android系統(tǒng)也提供了 ContentProvider 的數(shù)據(jù)回調(diào)監(jiān)聽 ,即 ContentObserver,這樣的設(shè)計,讓跨進程間的數(shù)據(jù)通信更加完善。
對于 App 崩潰或者應用進程被強殺的場景,神策數(shù)據(jù)技術(shù)團隊給出的解決辦法是引入了Session 的概念。注意:此Session非彼Session,關(guān)于Session的官方概念,可以參考筆者的另外一篇文章:Cookie、Session、Token那點事兒 神策數(shù)據(jù)團隊認為:對于App,當一個頁面退出,如果 30s 之內(nèi)沒有打開新的頁面,就認為 App 處于后臺;當一個頁面位于顯示狀態(tài),如果與上一個頁面退出時間的間隔超過 30s,就認為 App 重新處于前臺了。
針對神策數(shù)據(jù)技術(shù)團隊這種通過時間戳來計算判斷的方式,可以解決大多數(shù)情況的數(shù)據(jù)采集,但還是會有個別采集不到的情況,具體的原因就是30s的設(shè)置。舉個反例,如果設(shè)置25s、35s、40s 采集是否會更加精準?因為頁面的內(nèi)容不同、用戶的客觀條件(硬件、知識架構(gòu)、大腦靈敏度)不同等等,可能更加科學實現(xiàn)的一種方式是針對內(nèi)容的不同,動態(tài)去下發(fā)不同的設(shè)置時間,當然這是我個人的一些想法。還有一種可能,30s的設(shè)置是他們在統(tǒng)計了大量的用戶行為、進行數(shù)據(jù)判斷計算后得到的一個算術(shù)平均值,30s相較于其他值可能是較好的設(shè)置。
解決方案:
綜上,針對App處于前臺還是后臺的事件在無埋點技術(shù)上的實現(xiàn),就可以有以下步驟:
首先注冊 ActivityLifecycleCallbacks 回調(diào),來監(jiān)聽應用程序內(nèi)所有 Activity 的生命周期。處理業(yè)務時涉及到標記位的保存以及跨進程間的數(shù)據(jù)通信, 采用ContentProvider + SharedPreferences 的方式實現(xiàn)進程間數(shù)據(jù)共享,同時注冊 ContentObserver 來監(jiān)聽跨進程間的數(shù)據(jù)通信。
在頁面退出的時候也就是 onPause(),啟動一個倒計時 30s 定時器,如果 30s 之內(nèi)沒有新的頁面顯示,則觸發(fā) AppEnd 事件;如果有新的頁面進來,存儲一個標記位來標記新頁面進來。需要注意的是,由于Activity 之間可能是跨進程的,所以標記位需要實現(xiàn)進程間的共享,也就是通過 ContentProvider + SharedPreferences 進行存儲。
接著,通過 ContentObserver 監(jiān)聽到新頁面進來的標記位改變,然后取消定時器。如果 30s 之內(nèi)沒有新的頁面進來(如用戶按 Home 鍵 / 返回鍵退出 App、App 崩潰、App 被強殺),我們會下次啟動的時候補發(fā)一個 AppEnd 事件。
說完了應用在后臺的埋點處理在談談應用在前臺的埋點處理:
- 頁面啟動的時候也就是 onStart(),首先進行邏輯判斷,判斷與上個頁面的退出時間間隔是否超過了 30s,如果沒有超過 30s,則無需補發(fā) AppEnd 事件,直接觸發(fā) AppScreen(頁面瀏覽) 事件。接下來判斷是否已觸發(fā) AppEnd 事件的標記位,如果標記位為 true,則觸發(fā) AppStart 事件,反之不觸發(fā);如果超過了 30s,邏輯判斷是否已經(jīng)觸發(fā)了 AppEnd 事件,如果沒有, 則先觸發(fā) AppEnd 事件,然后再觸發(fā) AppStart 和 AppScreen 事件。
文章部分內(nèi)容選自:神策數(shù)據(jù)用戶行為洞察研究院《安卓全埋點技術(shù)白皮書》,感謝技術(shù)分享!
如果這篇文章對您有開發(fā)or學習上的些許幫助,希望各位看官留下寶貴的star,謝謝。
Ps:著作權(quán)歸作者所有,轉(zhuǎn)載請注明作者, 商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處(開頭或結(jié)尾請?zhí)砑愚D(zhuǎn)載出處,添加原文url地址),文章請勿濫用,也希望大家尊重筆者的勞動成果!