欢迎加入QQ讨论群258996829
麦子学院 头像
苹果6袋
6
麦子学院

Android应用使用时长精确计算方法详解

发布时间:2017-09-13 22:36  回复:0  查看:3298   最后回复:2017-09-13 22:36  
本文和大家分享的主要是android 应用使用时长精确计算方法 相关内容,一起来看看吧,希望对大家 学习android开发有所帮助。
注: 以下方案都针对ApiLevel14+, ApiLevel14 以前的版本还是由服务端计算每个页面的使用时长相加毕竟这类设备已经很少了,很多应用都不支持 14 以前的版本了
方案一:
通过onStart, onStop 来统计前台 Activity 数量是否是 0->1, 1->0 来判断是否到前台或者后台。 ( 网上大多采用这个方案 )
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 之间切换以及一些常规状态的处理。不过当遇到在最上层 Activity 有重建逻辑 ( 比如:横竖屏旋转 ) 时会有问题, Activity 走的流程 onPause->onStop->onDestory->onStart->onResume 。这过程中 onStop 时前台 Activity 数量为 0 的情况,所以会有无缘无故多了一次前后天切换的逻辑,解决方法看方案二。
方案二:
在方案一的基础上,在onStop 时检查 Activity 是否在 changingConfiguration 来决定是否计入前台 Activity 数量。
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");
        }
}
此方案基本就能解决屏幕旋转造成的误判,不过在进行锁屏测试时又发现了新的问题,对于竖屏状态下锁屏方案二没有什么问题。但是对于支持横竖屏旋转的Activity 先转成横屏再进行锁屏这时候的 Activity 流程 onPause->onStop->onStart->onResume->onPause ,也就是 Activity 先进入后台,又重新创建进入前台,同时只到 onPause 没有再触发 onStop 。导致我们以为应用还在前台,这时候通过前台 Activity 的数量来判断是否真正在前台就不准确了。在分析这个流程的过程中发现 onResume 的时候屏幕已经关掉了。正常情况下 onResume 一定是在屏幕还亮着的情况下进行的根,据这点就有了方案三。
方案三:
通过onResume 是否处在屏幕可操作来决定是否处在前台,之前方案的做法是在 onStart 的时候已经能判断 App 是否进入前台,而我们需要延时这个判断时机,不废话直接上代码。
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 将判断是否进入前台的时机延后到 onResume 来做,同时添加一个当前状态 isForegroundNow 防止出现误判。这样 App 前后台切换基本就算 ok 了。在进行更多细节时发现部分手机比如 oppo 呼起语音助手、锤子的闪念胶囊都会只执行一个 onPause 而不会有后续的其他生命周期回调。而这种场景可能经常会出现,为了更精细的计算 App 的前台时间我们还是应该把这部分时长也去除,一开始想能否用方案三类似的手段将判断提前?这个逻辑上其实是不可行的,只能延后判断不能提前判断。如果无法做我们是否可以直接将这部分时间从总的 App 使用时长中减去呢?也就有了方案四。
方案四:
由于呼出系统应用后App 可能会有两种生命周期 onResume 或者 onStop 我们根据时间间隔大于 1 ( 以误差为 1 秒计,本身页面切换需要时间 ) ,认为不在当前 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();
}
方案四基本上解决了语音助手等系统App 的使用时长问题,对于正常 App 的时长统计基本上比较 OK 了。这时候我们还遇到了一个问题,那就是应用崩溃导致统计时长缺失,怎么计算这部分时长?首先崩溃是不可预知的,简单的方法就是使用心跳,每个固定时间检查应用是否在前台,并将时间戳记下,正常关闭时清除这个时间戳,下次打开时发现有这个时间戳,说明上一次是异常关闭。这样的方案本身没有问题,但是消耗手机资源。由于 android 的奔溃很多都是 jvm 层面的,于是我灵光一现想到只要在页面打开、关闭、崩溃 catch 时对当前时间进行记录不就可以了吗?
方案五:
优化应用异常退出造成的统计时长误差的问题。
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 来记录最后活跃时间,这样一个相对完美的使用时长方案就诞生了。同时对于某些有特殊需求,需要知道应用何时切后台也是实现了。
来源:简书
您还未登录,请先登录

热门帖子

最新帖子