Android自動化埋點技術(shù)探索-1

前言:

上一篇文章 主要介紹了埋點的基本概念以及幾種埋點技術(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.ActivityLifecycleCallbacksonActivityRe-
    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地址),文章請勿濫用,也希望大家尊重筆者的勞動成果!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容