注:本文同步釋出於微信公眾號:stringwu的網際網路雜談 frament時長統計那些事
頁面停留時長作為應用統計的北極星指標裡的重要指標之一,統計使用者在某個頁面的停留時長則變得很重要。而Fragment
作為Android
中頁面的重要組成部分,其停留時長的統計就顯得非常重要。目前業界能搜尋到的方案,主要有兩種方案:
- 業務繼承於某一個特定的
Fragment
; - 直接通過
Fragment
的生命週期方法來統計頁面的時長;
方案一對於業務的侵入性過高,業務只有接入特定的Fragment
,才能統計其時長。方案二適用面太小,只適於於沒有預載入的場景,如果存在預載入行為,則統計出來的時長是不準的。
本文主要根據筆者對Fragment的理解,從對業務侵入性和相容性角度出來,.
1 Fragment簡介
Activity
是 Android
的介面組成元素,一個Activity
就是一個頁面。而Fragment
則允許將Activity
拆分成多個完全獨立封裝的可重用的元件,從而構建出靈活的UI介面。目前市場上的多個TAB
的UI一般都是通過Fragment
去組裝完成的,如某應用渠道的TAB:
具體的Fragment
的簡介可引數官方文件官方文件 ,本文不再詳細介紹;
2 Fragment的生命週期
Fragmennt
不能單獨使用,始終需要依賴於Activity
,因此,儘管Fragment
擁有自己的生命週期,但還是會受到Activity
的生命週期的影響,如Activity
被 銷燬後,Fragment
也會跟著銷燬。Fragment
是 Activity
的“寄生蟲”。
Fragment
的生命週期可參考圖:
一般在實際應用過程中,只需要對Fragment
的關鍵生命週期方法進行復寫就可以:
- onCreateView : 首次繪製
Fragment
時會呼叫這個方法,需要從些方法中返回Fragment
的根View; - onActivityCreated :
Fragment
所在的Activity
啟動完成時回撥; - onResume : 當前
Fragment
變成可互動狀態時回撥; - onPause : 使用者離開
Fragment
的回撥方法;
甚至於只需要複寫onCreateView
就能完成一個Fragment
的開發了。
3 Fragment時長統計
3.1 背景
Android
中最常用的兩種頁面的形態:Activity
和Fragment
。Activity
做為Android
中最原始的頁面,同一時刻只允許有一個Activity
處於活躍狀態(onResume),因此Activity
頁面的時長可以直接通過Activity
的生命週期方法來進行統計:
完整的頁面週期:
- onResume :頁面開始時間;
- onPause: 頁面結束時間;
而Fragment
不一樣,Fragment
是可以存在預載入和多層巢狀的行為的,同一時刻會有多個Fragment
執行了 onResume
方法,但真正對於使用者可互動的可能就只有一個(多層巢狀時會有多個),如果單純的使用Fragment
的生命週期方法來統計Fragment
的頁面時長顯然會造成統計不準。因此需要對Fragment
的頁面時長尋找獨立的統計解決方案。
本文不討論對業務侵入性比較大的方案,如自定義的Fragment
等方式,只討論對業務侵入性最小的方案。
3.2 方案
備註:本文討論的方案預設是使用support
包或者androix
的Fragment
來進行統計的;
Fragment
其本身是有生命週期的, 因此整個方案會基於Fragment
的生命週期來做進一步的處理:
- 註冊Fragment的生命週期的監聽
- 通過Fragment的
getUserVisibleHint
方法來判斷頁面的可見性 - 維持兩種Fragment的List:當前可見的Fragment List 和 已執行onResume方法的Fragment List;
3.2.1 生命週期的監聽
Fragment
的生命週期方法通過在Activity
的onResume方法裡註冊一個
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
還是可見的,這顯然是不符合邏輯的;如:
整個頁面由四個一級的Fragment
組成,其中標籤為THREAD
的fragment嵌入了三個子的Fragment
;
如果點選外層的FOUR
tab,則 標籤為EIRST
的 fragment
的可見性是不會發生變化的(仍是可見的),但實際上,該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
存在的預載入行為和多層巢狀的使用功能達到精準統計的功能。