歡迎加入QQ討論群258996829
麥子學(xué)院 頭像
蘋(píng)果6袋
6
麥子學(xué)院

Android應(yīng)用使用時(shí)長(zhǎng)精確計(jì)算方法詳解

發(fā)布時(shí)間:2017-09-13 22:36  回復(fù):0  查看:3249   最后回復(fù):2017-09-13 22:36  
本文和大家分享的主要是android 應(yīng)用使用時(shí)長(zhǎng)精確計(jì)算方法 相關(guān)內(nèi)容,一起來(lái)看看吧,希望對(duì)大家 學(xué)習(xí)android開(kāi)發(fā)有所幫助。
注: 以下方案都針對(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ū)
您還未登錄,請(qǐng)先登錄

熱門(mén)帖子

最新帖子

?