應(yīng)用時(shí)長的計(jì)算友盟早期做法計(jì)算每個(gè)Activity的時(shí)長温兼,然后全部相加就是App的使用時(shí)長。后來的做法是在客戶端計(jì)算武契,如果應(yīng)用離開小于30秒內(nèi)又切回就將切走的時(shí)間也算入App的使用時(shí)長內(nèi)募判。
本人覺得既然是計(jì)算時(shí)長就應(yīng)該是應(yīng)用的實(shí)際使用時(shí)長,對(duì)于不在App內(nèi)的時(shí)長就不應(yīng)該統(tǒng)計(jì)在內(nèi)咒唆。猜測可能是友盟考慮到計(jì)算量的問題届垫,要服務(wù)端來計(jì)算使用時(shí)長至少是浪費(fèi)資源的,客戶端相對(duì)來說是比較容易計(jì)算出使用時(shí)長的钧排。iOS由于系統(tǒng)給AppDelegate提供了前后臺(tái)切換的接口所以很容易計(jì)算敦腔,而Android的前后臺(tái)卻沒有App級(jí)別的均澳,只有針對(duì)Activity生命周期的回調(diào)恨溜。在整個(gè)計(jì)算方法的實(shí)現(xiàn)上還是經(jīng)歷一些波折符衔,我把整個(gè)思路做了個(gè)梳理。(以下方案都針對(duì)ApiLevel14+, ApiLevel14以前的版本還是由服務(wù)端計(jì)算每個(gè)頁面的使用時(shí)長相加畢竟這類設(shè)備已經(jīng)很少了糟袁,很多應(yīng)用都不支持14以前的版本了)
方案一:
通過onStart, onStop來統(tǒng)計(jì)前臺(tái)Activity數(shù)量是否是0->1, 1->0來判斷是否到前臺(tái)或者后臺(tái)判族。(網(wǎng)上大多采用這個(gè)方案)
private int foregroundActivityCount = 0;
@Override
public void onActivityStarted(Activity activity) {
if (foregroundActivityCount == 0) {
Log.i(TAG, "switch to foreground");
}
foregroundActivityCount += 1;
}
@Override
public void onActivityStopped(Activity activity) {
foregroundActivityCount -= 1;
if(foregroundActivityCount == 0){
Log.i(TAG, "switch to background");
}
}
本方案基本解決了Activity之間切換以及一些常規(guī)狀態(tài)的處理。不過當(dāng)遇到在最上層Activity有重建邏輯(比如:橫豎屏旋轉(zhuǎn))時(shí)會(huì)有問題项戴,Activity走的流程onPause->onStop->onDestory->onStart->onResume形帮。這過程中onStop時(shí)前臺(tái)Activity數(shù)量為0的情況,所以會(huì)有無緣無故多了一次前后天切換的邏輯周叮,解決方法看方案二辩撑。
方案二:
在方案一的基礎(chǔ)上,在onStop時(shí)檢查Activity是否在changingConfiguration來決定是否計(jì)入前臺(tái)Activity數(shù)量仿耽。
private int foregroundActivityCount = 0;
private boolean isChangingConfigActivity = false;
@Override
public void onActivityStarted(Activity activity) {
if (foregroundActivityCount == 0) {
Log.i(TAG, "switch to foreground");
}
if(isChangingConfigActivity){
isChangingConfigActivity = false;
return;
}
foregroundActivityCount += 1;
}
@Override
public void onActivityStopped(Activity activity) {
if(activity.isChangingConfigurations()){
isChangingConfigActivity = true;
return;
}
foregroundActivityCount -= 1;
if(foregroundActivityCount == 0){
Log.i(TAG, "switch to background");
}
}
此方案基本就能解決屏幕旋轉(zhuǎn)造成的誤判合冀,不過在進(jìn)行鎖屏測試時(shí)又發(fā)現(xiàn)了新的問題,對(duì)于豎屏狀態(tài)下鎖屏方案二沒有什么問題项贺。但是對(duì)于支持橫豎屏旋轉(zhuǎn)的Activity先轉(zhuǎn)成橫屏再進(jìn)行鎖屏這時(shí)候的Activity流程onPause->onStop->onStart->onResume->onPause君躺,也就是Activity先進(jìn)入后臺(tái),又重新創(chuàng)建進(jìn)入前臺(tái)开缎,同時(shí)只到onPause沒有再觸發(fā)onStop棕叫。導(dǎo)致我們以為應(yīng)用還在前臺(tái),這時(shí)候通過前臺(tái)Activity的數(shù)量來判斷是否真正在前臺(tái)就不準(zhǔn)確了奕删。在分析這個(gè)流程的過程中發(fā)現(xiàn)onResume的時(shí)候屏幕已經(jīng)關(guān)掉了俺泣。正常情況下onResume一定是在屏幕還亮著的情況下進(jìn)行的根,據(jù)這點(diǎn)就有了方案三完残。
方案三:
通過onResume是否處在屏幕可操作來決定是否處在前臺(tái)砌滞,之前方案的做法是在onStart的時(shí)候已經(jīng)能判斷App是否進(jìn)入前臺(tái),而我們需要延時(shí)這個(gè)判斷時(shí)機(jī)坏怪,不廢話直接上代碼贝润。
private int foregroundActivityCount = 0;
private boolean isChangingConfigActivity = false;
private boolean willSwitchToForeground = false;
private boolean isForegroundNow = false;
@Override
public void onActivityStarted(Activity activity) {
if (foregroundActivityCount == 0 || !isForegroundNow) {
willSwitchToForeground = true;
}
if(isChangingConfigActivity){
isChangingConfigActivity = false;
return;
}
foregroundActivityCount += 1;
}
@Override
public void onActivityResumed(Activity activity) {
if (willSwitchToForeground && isInteractive(activity)) {
isForegroundNow = true;
Log.i("TAG", "switch to foreground");
}
if (isForegroundNow) {
willSwitchToForeground = false;
}
}
@Override
public void onActivityStopped(Activity activity) {
if(activity.isChangingConfigurations()){
isChangingConfigActivity = true;
return;
}
foregroundActivityCount -= 1;
if(foregroundActivityCount == 0){
isForegroundNow = false;
Log.i(TAG, "switch to background");
}
}
private boolean isInteractive(Context context) {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
return pm.isInteractive();
} else {
return pm.isScreenOn();
}
}
方案三通過willSwitchToForeground將判斷是否進(jìn)入前臺(tái)的時(shí)機(jī)延后到onResume來做,同時(shí)添加一個(gè)當(dāng)前狀態(tài)isForegroundNow防止出現(xiàn)誤判铝宵。這樣App前后臺(tái)切換基本就算ok了打掘。在進(jìn)行更多細(xì)節(jié)時(shí)發(fā)現(xiàn)部分手機(jī)比如oppo呼起語音助手、錘子的閃念膠囊都會(huì)只執(zhí)行一個(gè)onPause而不會(huì)有后續(xù)的其他生命周期回調(diào)鹏秋。而這種場景可能經(jīng)常會(huì)出現(xiàn)尊蚁,為了更精細(xì)的計(jì)算App的前臺(tái)時(shí)間我們還是應(yīng)該把這部分時(shí)長也去除,一開始想能否用方案三類似的手段將判斷提前侣夷?這個(gè)邏輯上其實(shí)是不可行的横朋,只能延后判斷不能提前判斷。如果無法做我們是否可以直接將這部分時(shí)間從總的App使用時(shí)長中減去呢百拓?也就有了方案四琴锭。
方案四:
由于呼出系統(tǒng)應(yīng)用后App可能會(huì)有兩種生命周期onResume或者onStop我們根據(jù)時(shí)間間隔大于1秒(以誤差為1秒計(jì)晰甚,本身頁面切換需要時(shí)間),認(rèn)為不在當(dāng)前App中活躍决帖。
private int foregroundActivityCount = 0;
private boolean isChangingConfigActivity = false;
private boolean willSwitchToForeground = false;
private boolean isForegroundNow = false;
private String lastPausedActivityName;
private int lastPausedActivityHashCode;
private long lastPausedTime;
private long appUseReduceTime = 0;
@Override
public void onActivityStarted(Activity activity) {
if (foregroundActivityCount == 0 || !isForegroundNow) {
willSwitchToForeground = true;
}
if (isChangingConfigActivity) {
isChangingConfigActivity = false;
return;
}
foregroundActivityCount += 1;
}
@Override
public void onActivityResumed(Activity activity) {
addAppUseReduceTimeIfNeeded(activity);
if (willSwitchToForeground && isInteractive(activity)) {
isForegroundNow = true;
Log.i("TAG", "switch to foreground");
}
if (isForegroundNow) {
willSwitchToForeground = false;
}
}
@Override
public void onActivityPaused(Activity activity) {
lastPausedActivityName = getActivityName(activity);
lastPausedActivityHashCode = activity.hashCode();
lastPausedTime = System.currentTimeMillis();
}
@Override
public void onActivityStopped(Activity activity) {
addAppUseReduceTimeIfNeeded(activity);
if (activity.isChangingConfigurations()) {
isChangingConfigActivity = true;
return;
}
foregroundActivityCount -= 1;
if (foregroundActivityCount == 0) {
isForegroundNow = false;
Log.i(TAG, "switch to background (reduce time["+appUseReduceTime+"])");
}
}
private void addAppUseReduceTimeIfNeeded(Activity activity) {
if (getActivityName(activity).equals(lastPausedActivityName)
&& activity.hashCode() == lastPausedActivityHashCode) {
long now = System.currentTimeMillis();
if (now - lastPausedTime > 1000) {
appUseReduceTime += now - lastPausedTime;
}
}
lastPausedActivityHashCode = -1;
lastPausedActivityName = null;
lastPausedTime = 0;
}
private boolean isInteractive(Context context) {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
return pm.isInteractive();
} else {
return pm.isScreenOn();
}
}
private String getActivityName(final Activity activity) {
return activity.getClass().getCanonicalName();
}
方案四基本上解決了語音助手等系統(tǒng)App的使用時(shí)長問題厕九,對(duì)于正常App的時(shí)長統(tǒng)計(jì)基本上比較OK了。這時(shí)候我們還遇到了一個(gè)問題地回,那就是應(yīng)用崩潰導(dǎo)致統(tǒng)計(jì)時(shí)長缺失扁远,怎么計(jì)算這部分時(shí)長?首先崩潰是不可預(yù)知的刻像,簡單的方法就是使用心跳畅买,每個(gè)固定時(shí)間檢查應(yīng)用是否在前臺(tái),并將時(shí)間戳記下细睡,正常關(guān)閉時(shí)清除這個(gè)時(shí)間戳皮获,下次打開時(shí)發(fā)現(xiàn)有這個(gè)時(shí)間戳,說明上一次是異常關(guān)閉纹冤。這樣的方案本身沒有問題洒宝,但是消耗手機(jī)資源。由于android的奔潰很多都是jvm層面的萌京,于是我靈光一現(xiàn)想到只要在頁面打開雁歌、關(guān)閉、崩潰catch時(shí)對(duì)當(dāng)前時(shí)間進(jìn)行記錄不就可以了嗎知残?
方案五:
優(yōu)化應(yīng)用異常退出造成的統(tǒng)計(jì)時(shí)長誤差的問題靠瞎。
private AppLifecyclePersistentManager persistentMgr;
private int foregroundActivityCount = 0;
private boolean isChangingConfigActivity = false;
private boolean willSwitchToForeground = false;
private boolean isForegroundNow = false;
private String lastPausedActivityName;
private int lastPausedActivityHashCode;
private long lastPausedTime;
private long appUseReduceTime = 0;
private long foregroundTs;
@Override
public void onActivityStarted(Activity activity) {
if (foregroundActivityCount == 0 || !isForegroundNow) {
willSwitchToForeground = true;
}
if (isChangingConfigActivity) {
isChangingConfigActivity = false;
return;
}
foregroundActivityCount += 1;
}
@Override
public void onActivityResumed(Activity activity) {
persistentMgr.saveActiveTs(System.currentTimeMillis());
addAppUseReduceTimeIfNeeded(activity);
if (willSwitchToForeground && isInteractive(activity)) {
if(persistentMgr.isLastAppLifecycleAbnormal()){
long activeTs = persistentMgr.findActiveTs();
long reduceTime = persistentMgr.findReduceTs();
long foregroundTs = persistentMgr.findForegroundTs();
Log.i("TAG", "last switch to background abnormal terminal");
persistentMgr.clearAll();
}
isForegroundNow = true;
foregroundTs = System.currentTimeMillis();
persistentMgr.saveForegroundTs(foregroundTs);
Log.i("TAG", "switch to foreground[" + foregroundTs + "]");
}
if (isForegroundNow) {
willSwitchToForeground = false;
}
}
@Override
public void onActivityPaused(Activity activity) {
persistentMgr.saveActiveTs(System.currentTimeMillis());
lastPausedActivityName = getActivityName(activity);
lastPausedActivityHashCode = activity.hashCode();
lastPausedTime = System.currentTimeMillis();
}
@Override
public void onActivityStopped(Activity activity) {
addAppUseReduceTimeIfNeeded(activity);
if (activity.isChangingConfigurations()) {
isChangingConfigActivity = true;
return;
}
foregroundActivityCount -= 1;
if (foregroundActivityCount == 0) {
isForegroundNow = false;
persistentMgr.clearAll();
Log.i(TAG, "switch to background (reduce time[" + appUseReduceTime + "])");
}
}
private void addAppUseReduceTimeIfNeeded(Activity activity) {
if (getActivityName(activity).equals(lastPausedActivityName)
&& activity.hashCode() == lastPausedActivityHashCode) {
long now = System.currentTimeMillis();
if (now - lastPausedTime > 1000) {
appUseReduceTime += now - lastPausedTime;
}
}
lastPausedActivityHashCode = -1;
lastPausedActivityName = null;
lastPausedTime = 0;
persistentMgr.saveReduceTs(appUseReduceTime);
}
private boolean isInteractive(Context context) {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
return pm.isInteractive();
} else {
return pm.isScreenOn();
}
}
private String getActivityName(final Activity activity) {
return activity.getClass().getCanonicalName();
}
private Thread.UncaughtExceptionHandler mDefaultHandler;
public void register() {
if (Thread.getDefaultUncaughtExceptionHandler() == this) {
return;
}
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
}
@Override
public void uncaughtException(Thread t, Throwable e) {
AppLifecyclePersistentManager.getInstance().saveActiveTs(System.currentTimeMillis());
if (mDefaultHandler != null && mDefaultHandler != Thread.getDefaultUncaughtExceptionHandler()) {
mDefaultHandler.uncaughtException(t, e);
}
}
利用uncaughtException來記錄最后活躍時(shí)間,這樣一個(gè)相對(duì)完美的使用時(shí)長方案就誕生了求妹。同時(shí)對(duì)于某些有特殊需求乏盐,需要知道應(yīng)用何時(shí)切后臺(tái)也是實(shí)現(xiàn)了。