Fragment時長統計那些事

woodWu發表於2020-09-12

注:本文同步釋出於微信公眾號:stringwu的網際網路雜談 frament時長統計那些事
頁面停留時長作為應用統計的北極星指標裡的重要指標之一,統計使用者在某個頁面的停留時長則變得很重要。而Fragment作為Android中頁面的重要組成部分,其停留時長的統計就顯得非常重要。目前業界能搜尋到的方案,主要有兩種方案:

  • 業務繼承於某一個特定的 Fragment
  • 直接通過Fragment的生命週期方法來統計頁面的時長;

方案一對於業務的侵入性過高,業務只有接入特定的Fragment,才能統計其時長。方案二適用面太小,只適於於沒有預載入的場景,如果存在預載入行為,則統計出來的時長是不準的。
本文主要根據筆者對Fragment的理解,從對業務侵入性和相容性角度出來,.

1 Fragment簡介

ActivityAndroid的介面組成元素,一個Activity就是一個頁面。而Fragment則允許將Activity拆分成多個完全獨立封裝的可重用的元件,從而構建出靈活的UI介面。目前市場上的多個TAB的UI一般都是通過Fragment去組裝完成的,如某應用渠道的TAB:fragment_tab

具體的Fragment的簡介可引數官方文件官方文件 ,本文不再詳細介紹;

2 Fragment的生命週期

Fragmennt不能單獨使用,始終需要依賴於Activity,因此,儘管Fragment擁有自己的生命週期,但還是會受到Activity的生命週期的影響,如Activity被 銷燬後,Fragment也會跟著銷燬。FragmentActivity的“寄生蟲”。
Fragment的生命週期可參考圖:fragment_lifecycle
一般在實際應用過程中,只需要對Fragment的關鍵生命週期方法進行復寫就可以:

  • onCreateView : 首次繪製Fragment時會呼叫這個方法,需要從些方法中返回Fragment的根View;
  • onActivityCreated : Fragment所在的Activity啟動完成時回撥;
  • onResume : 當前Fragment變成可互動狀態時回撥;
  • onPause : 使用者離開Fragment的回撥方法;
    甚至於只需要複寫onCreateView 就能完成一個Fragment的開發了。

3 Fragment時長統計

3.1 背景

Android中最常用的兩種頁面的形態:ActivityFragmentActivity做為Android中最原始的頁面,同一時刻只允許有一個Activity處於活躍狀態(onResume),因此Activity頁面的時長可以直接通過Activity的生命週期方法來進行統計:
完整的頁面週期:

  • onResume :頁面開始時間;
  • onPause: 頁面結束時間;

Fragment不一樣,Fragment是可以存在預載入和多層巢狀的行為的,同一時刻會有多個Fragment執行了 onResume方法,但真正對於使用者可互動的可能就只有一個(多層巢狀時會有多個),如果單純的使用Fragment的生命週期方法來統計Fragment的頁面時長顯然會造成統計不準。因此需要對Fragment的頁面時長尋找獨立的統計解決方案。

本文不討論對業務侵入性比較大的方案,如自定義的Fragment等方式,只討論對業務侵入性最小的方案。

3.2 方案

備註:本文討論的方案預設是使用support包或者androixFragment來進行統計的;

Fragment其本身是有生命週期的, 因此整個方案會基於Fragment的生命週期來做進一步的處理:

  • 註冊Fragment的生命週期的監聽
  • 通過Fragment的getUserVisibleHint方法來判斷頁面的可見性
  • 維持兩種Fragment的List:當前可見的Fragment List 和 已執行onResume方法的Fragment List;

3.2.1 生命週期的監聽

Fragment的生命週期方法通過在ActivityonResume方法裡註冊一個Fragment`的生命週期的回撥方法:

//在Activity onResume時呼叫
public void onActivityResume(Activity activity) {
        FragmentActivity fragmentActivity;
        if (!(activity instanceof FragmentActivity)) {
            return;
        }
        fragmentActivity = (FragmentActivity) activity;
        //拿到當前Activity的fragment管理
        FragmentManager manager = fragmentActivity.getSupportFragmentManager();
        manager.registerFragmentLifecycleCallbacks(fragmentcallback, true);
}

//fragmentcallback
fragemntcallback = new FragmentManager.FragmentLifecycleCallbacks(){
  ....
   @Override
        public void onFragmentPreAttached(@NonNull FragmentManager fm, @NonNull Fragment f, @NonNull Context context) {
            super.onFragmentPreAttached(fm, f, context);
        }
     @Override
        public void onFragmentResumed(@NonNull FragmentManager fm, @NonNull Fragment f) {
            super.onFragmentResumed(fm, f);
            onFragmentResume(f);

        }
        @Override
        public void onFragmentPaused(@NonNull FragmentManager fm, @NonNull Fragment f) {
            super.onFragmentPaused(fm, f);
            onFragmentPause(f);

        }
        ....
}

3.2.2 可見性判斷

Fragment可見性判斷主要是通過getUserVisibleHint方法來判斷是否可見的。

// 可見性判斷方法
boolean curState = fragment.getUserVisibleHint();

如果Fragment沒有巢狀的情況,則直接通過其本身的getUserVisibleHint方法就能判斷當前頁面的可見性,但如果Fragment又嵌入Fragmnent,則只有其本身的getUserVisibleHint方法來判斷當前頁面的可見性是不夠的,會出現外層的Fragment不可見了,但內部的Fragment還是可見的,這顯然是不符合邏輯的;如:multi_fragment
整個頁面由四個一級的Fragment組成,其中標籤為THREAD的fragment嵌入了三個子的Fragment
如果點選外層的FOUR tab,則 標籤為EIRSTfragment的可見性是不會發生變化的(仍是可見的),但實際上,該fragment已經不可見了。
因此我們不能簡單在通過該Fragment的可見性來判斷其頁面的真實可見性,需要結合外層Fragment的可見性來判斷頁面的真實可見性:

 //完成的頁面可見性方法判斷。
    private boolean isFragmentVisible(Fragment fragment) {
        boolean curState = fragment.getUserVisibleHint();
        //本身不可見就直接返回不可見了
        if (!curState) {
            return false;
        }
        //本身可見情況下,判斷父fragment的情況
        Fragment parentFragment = fragment.getParentFragment();
        boolean parentState = true;
        while (parentFragment != null) {
            parentState = parentFragment.getUserVisibleHint();
            if (!parentState) {
                break;
            }
            parentFragment = parentFragment.getParentFragment();
        }

        return parentState;
    }

這個可見性方法生效的前提是子Fragment使用的FragmentManager是通過外層的fragment.getChildFragmentManager()拿到的。這樣子Fragment的關係裡才會獲取到其 ParentFragment

3.2.3 Fragment遍歷

通過 Fragment的生命週期方法的監聽和頁面可見性的判斷,內部需要維持兩個List:

  • 執行了onResume方法的Maps:mFragmentResumeMap;//key:fragment;value:執行onResume時間;
  • 頁面可見的Maps:mFragmentStartTimes;// key:fragment;value:頁面開始可見時間

內部通過去遍歷這兩個Maps來判斷頁面的事件(進入或者退出)。在有Fragment執行onResume或者onPause時去觸發遍歷的操作。
遍歷的虛擬碼為:

 //可見的list應該不只有一個,可能有多個
 //先遍歷FragmentResumeMap,判斷哪個Fragment變成可見了,加入到可見的List列表裡;
 //然後遍歷可見List裡哪個變成不可見了,然後就開始上報當前結束的列表;
 //onPause時,要移除掉當前的onResume的List;
 Set<Fragment> mResumeFragmentSet = mFragmentResumeMap.keySet();
 //新的可見Fragment:由不可見變成可見
 HashMap<Fragment, Long> newVisibleFragment = new HashMap<>();
 for (Fragment fragment : mResumeFragmentSet) {
     if (!isFragmentVisible(fragment)) {
         continue;
     }
    if (mFragmentStartTimes.containsKey(fragment)) {
         continue;
     }
     Long time = SystemClock.elapsedRealtime();
     //記錄下當前Fragment開始可見時間
     newVisibleFragment.put(fragment, time);
     mFragmentStartTimes.put(fragment, time);
}

//對新的可見Fragment newVisibleFragment 執行頁面進入事件的回撥;
....

//判斷由可見變為不可見的Fragment
Set<Fragment> mVisibleSet = mFragmentStartTimes.keySet();
HashMap<Fragment, Long> unVisibleFragment = new HashMap<>();
for (Fragment fragment : mVisibleSet) {
    if (isFragmentVisible(fragment)) {
        continue;
    }
    unVisibleFragment.put(fragment, SystemClock.elapsedRealtime());
}

//對於從可見變成不可見的fragment 執行頁面退出的回撥;
....

4 總結

本文通過監聽Fragment的生命週期和頁面可見性的判斷邏輯,提出了一個對於業務侵入性很小的Fragment頁面時長的統計方法。Fragment時長的精準統計方案通過在內部的邏輯來相容Fragment存在的預載入行為和多層巢狀的使用功能達到精準統計的功能。

相關文章