如何精確計(jì)算Android應(yīng)用的使用時(shí)長

應(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)了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末制恍,一起剝皮案震驚了整個(gè)濱河市父能,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌净神,老刑警劉巖何吝,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異鹃唯,居然都是意外死亡爱榕,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門坡慌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來黔酥,“玉大人,你說我怎么就攤上這事」蛘撸” “怎么了棵帽?”我有些...
    開封第一講書人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長坑夯。 經(jīng)常有香客問我,道長抡四,這世上最難降的妖魔是什么柜蜈? 我笑而不...
    開封第一講書人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮指巡,結(jié)果婚禮上淑履,老公的妹妹穿的比我還像新娘。我一直安慰自己藻雪,他們只是感情好秘噪,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著勉耀,像睡著了一般指煎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上便斥,一...
    開封第一講書人閱讀 51,573評(píng)論 1 305
  • 那天至壤,我揣著相機(jī)與錄音,去河邊找鬼枢纠。 笑死像街,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的晋渺。 我是一名探鬼主播镰绎,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼木西!你這毒婦竟也來了畴栖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤八千,失蹤者是張志新(化名)和其女友劉穎驶臊,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體叼丑,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡关翎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鸠信。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片纵寝。...
    茶點(diǎn)故事閱讀 39,991評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出爽茴,到底是詐尸還是另有隱情葬凳,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評(píng)論 5 346
  • 正文 年R本政府宣布室奏,位于F島的核電站火焰,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏胧沫。R本人自食惡果不足惜昌简,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望绒怨。 院中可真熱鬧纯赎,春花似錦、人聲如沸南蹂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽六剥。三九已至晚顷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間疗疟,已是汗流浹背音同。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留秃嗜,地道東北人权均。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像锅锨,于是被迫代替她去往敵國和親叽赊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容