注:
以下方案都針對(duì)ApiLevel14+, ApiLevel14
以前的版本還是由服務(wù)端計(jì)算每個(gè)頁(yè)面的使用時(shí)長(zhǎng)相加畢竟這類(lèi)設(shè)備已經(jīng)很少了,很多應(yīng)用都不支持
14
以前的版本了。
方案一:
通過(guò)onStart, onStop
來(lái)統(tǒng)計(jì)前臺(tái)
Activity
數(shù)量是否是
0->1, 1->0
來(lái)判斷是否到前臺(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)的處理。不過(guò)當(dāng)遇到在最上層
Activity
有重建邏輯
(
比如:橫豎屏旋轉(zhuǎn)
)
時(shí)會(huì)有問(wèn)題,
Activity
走的流程
onPause->onStop->onDestory->onStart->onResume
。這過(guò)程中
onStop
時(shí)前臺(tái)
Activity
數(shù)量為
0
的情況,所以會(huì)有無(wú)緣無(wú)故多了一次前后天切換的邏輯,解決方法看方案二。
方案二:
在方案一的基礎(chǔ)上,在onStop
時(shí)檢查
Activity
是否在
changingConfiguration
來(lái)決定是否計(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)造成的誤判,不過(guò)在進(jìn)行鎖屏測(cè)試時(shí)又發(fā)現(xiàn)了新的問(wèn)題,對(duì)于豎屏狀態(tài)下鎖屏方案二沒(méi)有什么問(wèn)題。但是對(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
沒(méi)有再觸發(fā)
onStop
。導(dǎo)致我們以為應(yīng)用還在前臺(tái),這時(shí)候通過(guò)前臺(tái)
Activity
的數(shù)量來(lái)判斷是否真正在前臺(tái)就不準(zhǔn)確了。在分析這個(gè)流程的過(guò)程中發(fā)現(xiàn)
onResume
的時(shí)候屏幕已經(jīng)關(guān)掉了。正常情況下
onResume
一定是在屏幕還亮著的情況下進(jìn)行的根,據(jù)這點(diǎn)就有了方案三。
方案三:
通過(guò)onResume
是否處在屏幕可操作來(lái)決定是否處在前臺(tái),之前方案的做法是在
onStart
的時(shí)候已經(jīng)能判斷
App
是否進(jìn)入前臺(tái),而我們需要延時(shí)這個(gè)判斷時(shí)機(jī),不廢話(huà)直接上代碼。
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();
}
}
方案三通過(guò)willSwitchToForeground
將判斷是否進(jìn)入前臺(tái)的時(shí)機(jī)延后到
onResume
來(lái)做,同時(shí)添加一個(gè)當(dāng)前狀態(tài)
isForegroundNow
防止出現(xiàn)誤判。這樣
App
前后臺(tái)切換基本就算
ok
了。在進(jìn)行更多細(xì)節(jié)時(shí)發(fā)現(xiàn)部分手機(jī)比如
oppo
呼起語(yǔ)音助手、錘子的閃念膠囊都會(huì)只執(zhí)行一個(gè)
onPause
而不會(huì)有后續(xù)的其他生命周期回調(diào)。而這種場(chǎng)景可能經(jīng)常會(huì)出現(xiàn),為了更精細(xì)的計(jì)算
App
的前臺(tái)時(shí)間我們還是應(yīng)該把這部分時(shí)長(zhǎng)也去除,一開(kāi)始想能否用方案三類(lèi)似的手段將判斷提前?這個(gè)邏輯上其實(shí)是不可行的,只能延后判斷不能提前判斷。如果無(wú)法做我們是否可以直接將這部分時(shí)間從總的
App
使用時(shí)長(zhǎng)中減去呢?也就有了方案四。
方案四:
由于呼出系統(tǒng)應(yīng)用后App
可能會(huì)有兩種生命周期
onResume
或者
onStop
我們根據(jù)時(shí)間間隔大于
1
秒
(
以誤差為
1
秒計(jì),本身頁(yè)面切換需要時(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();
}
方案四基本上解決了語(yǔ)音助手等系統(tǒng)App
的使用時(shí)長(zhǎng)問(wèn)題,對(duì)于正常
App
的時(shí)長(zhǎng)統(tǒng)計(jì)基本上比較
OK
了。這時(shí)候我們還遇到了一個(gè)問(wèn)題,那就是應(yīng)用崩潰導(dǎo)致統(tǒng)計(jì)時(shí)長(zhǎng)缺失,怎么計(jì)算這部分時(shí)長(zhǎng)?首先崩潰是不可預(yù)知的,簡(jiǎn)單的方法就是使用心跳,每個(gè)固定時(shí)間檢查應(yīng)用是否在前臺(tái),并將時(shí)間戳記下,正常關(guān)閉時(shí)清除這個(gè)時(shí)間戳,下次打開(kāi)時(shí)發(fā)現(xiàn)有這個(gè)時(shí)間戳,說(shuō)明上一次是異常關(guān)閉。這樣的方案本身沒(méi)有問(wèn)題,但是消耗手機(jī)資源。由于
android
的奔潰很多都是
jvm
層面的,于是我靈光一現(xiàn)想到只要在頁(yè)面打開(kāi)、關(guān)閉、崩潰
catch
時(shí)對(duì)當(dāng)前時(shí)間進(jìn)行記錄不就可以了嗎?
方案五:
優(yōu)化應(yīng)用異常退出造成的統(tǒng)計(jì)時(shí)長(zhǎng)誤差的問(wèn)題。
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
來(lái)記錄最后活躍時(shí)間,這樣一個(gè)相對(duì)完美的使用時(shí)長(zhǎng)方案就誕生了。同時(shí)對(duì)于某些有特殊需求,需要知道應(yīng)用何時(shí)切后臺(tái)也是實(shí)現(xiàn)了。
來(lái)源:簡(jiǎn)書(shū)