前言
目前大部分應(yīng)用都需要統(tǒng)計(jì)用戶每日活躍時(shí)間,尤其是類似于趣頭條的這種激勵(lì)式應(yīng)用。本篇文章是本人之前實(shí)現(xiàn)的時(shí)長(zhǎng)統(tǒng)計(jì),主要利用了ActivityLifecycleCallbacks ,這個(gè)類管理當(dāng)前應(yīng)用所有Activity生命周期的回調(diào)來(lái)實(shí)現(xiàn),目前功能已經(jīng)穩(wěn)定,時(shí)間誤差較小。本文的思路是之前看了別人的思路搞出來(lái)的,今天分享出來(lái),希望大家共同進(jìn)步。
主要思路:
Application.ActivityLifecycleCallbacks,該類是一個(gè)Application內(nèi)部有Activity活動(dòng)時(shí)的一個(gè)回調(diào),當(dāng)前其中有Activity的生命周期發(fā)生改變時(shí),會(huì)回調(diào)調(diào)用對(duì)應(yīng)的方法,如onActivityCreated(Activity activity, Bundle bundle)。所以在這個(gè)接口中,我們可以知道應(yīng)用內(nèi)所有Activity的生命周期情況。
- 1.在應(yīng)用每次有Activity創(chuàng)建時(shí),統(tǒng)計(jì)前一個(gè)Activity的開始在前臺(tái)(onStart)到當(dāng)前新的Activity在前臺(tái)的時(shí)間差;
- 2.當(dāng)一個(gè)Activity銷毀時(shí),若銷毀后前臺(tái)沒(méi)有Activity,則記錄此時(shí)的事件和該Activity可見(jiàn)時(shí)的時(shí)間差,若前臺(tái)依然有,則記錄銷毀后展示的Activity onStart的時(shí)間和被銷毀的Activity onStart的時(shí)間差。
- 3.以上的時(shí)間差進(jìn)行計(jì)算時(shí),要考慮跨天的情況,即應(yīng)用運(yùn)行時(shí)跨越了0點(diǎn)的情況,我們產(chǎn)品的要求是按照新一天的0點(diǎn)開始記錄。
我的數(shù)據(jù)時(shí)保存SP中,以當(dāng)天0點(diǎn)的毫秒值作為Key值,該值每次獲取會(huì)有微小的差值,我的做法是 /1000*1000保證每次都相同。
大家可以對(duì)照生命周期圖來(lái)理解。
代碼入下:
使用時(shí),在Application中注冊(cè)。
lifecycleCallbacks = new ActivityLifeCycle();
registerActivityLifecycleCallbacks(lifecycleCallbacks);
public class ActivityLifeCycle implements Application.ActivityLifecycleCallbacks {
/**
* 上次檢查時(shí)間,用于在運(yùn)行時(shí)作為基準(zhǔn)獲取用戶時(shí)間
*/
public static long lastCheckTime = 0;
/** 前臺(tái)Activity數(shù)量 **/
private int foregroundActivityCount = 0;
/**
* Activity是否在修改配置,
*/
private boolean isChangingConfigActivity = false;
/**
* 應(yīng)用將要切換到前臺(tái)
*/
private boolean willSwitchToForeground = false;
/**
* 當(dāng)前是否在前臺(tái)
*/
private boolean isForegroundNow = false;
/**
* 上次暫停的Activity信息
*/
private String lastPausedActivityName;
private int lastPausedActivityHashCode;
private long lastPausedTime;
private long appUseReduceTime = 0;
/**
* 每次有Activity啟動(dòng)時(shí)的開始時(shí)間點(diǎn)
*/
private long appStartTime = 0L;
/**
* 本次統(tǒng)計(jì)時(shí),運(yùn)行的時(shí)間
*/
private long runTimeThisDay = 0L;
@Override
public void onActivityCreated(Activity activity, Bundle bundle) {
KLog.d("onActivityCreated" + getActivityName(activity));
}
@Override
public void onActivityStarted(Activity activity) {
KLog.d("onActivityStarted " + getActivityName(activity) + " " + foregroundActivityCount);
//前臺(tái)沒(méi)有Activity,說(shuō)明新啟動(dòng)或者將從從后臺(tái)恢復(fù)
if (foregroundActivityCount == 0 || !isForegroundNow) {
willSwitchToForeground = true;
}else {
//應(yīng)用已經(jīng)在前臺(tái),此時(shí)保存今日運(yùn)行的時(shí)間。
runTimeThisDay = System.currentTimeMillis() - appStartTime;
lastCheckTime = System.currentTimeMillis();
saveTodayPlayTime(activity, runTimeThisDay);
}
appStartTime = System.currentTimeMillis();
if (isChangingConfigActivity) {
isChangingConfigActivity = false;
return;
}
foregroundActivityCount += 1;
}
@Override
public void onActivityResumed(Activity activity) {
KLog.d("onActivityResumed" + getActivityName(activity));
//在這里更新檢查時(shí)間點(diǎn),是為了保證從后臺(tái)恢復(fù)到前臺(tái),持續(xù)計(jì)時(shí)的準(zhǔn)確性。
lastCheckTime = System.currentTimeMillis();
addAppUseReduceTimeIfNeeded(activity);
if (willSwitchToForeground && isInteractive(activity)) {
isForegroundNow = true;
KLog.d( "switch to foreground");
}
if (isForegroundNow) {
willSwitchToForeground = false;
}
}
@Override
public void onActivityPaused(Activity activity) {
KLog.d("onActivityPaused" + getActivityName(activity));
lastPausedActivityName = getActivityName(activity);
lastPausedActivityHashCode = activity.hashCode();
lastPausedTime = System.currentTimeMillis();
}
@Override
public void onActivityStopped(Activity activity) {
KLog.d("onActivityStopped" + getActivityName(activity));
addAppUseReduceTimeIfNeeded(activity);
//如果這個(gè)Activity實(shí)在修改配置,如旋轉(zhuǎn)等,則不保存時(shí)間直接返回
if (activity.isChangingConfigurations()) {
isChangingConfigActivity = true;
return;
}
//該Activity要進(jìn)入后臺(tái),前臺(tái)Activity數(shù)量-1。
foregroundActivityCount -= 1;
//當(dāng)前已經(jīng)是最后的一個(gè)Activity,代表此時(shí)應(yīng)用退出了,保存時(shí)間。
// 如果跨天了,則從新一天的0點(diǎn)開始計(jì)時(shí)
if (foregroundActivityCount == 0) {
isForegroundNow = false;
KLog.d("switch to background (reduce time[" + appUseReduceTime + "])");
if (getTodayStartTime() > appStartTime){
runTimeThisDay = System.currentTimeMillis() - getTodayStartTime();
}else {
runTimeThisDay = System.currentTimeMillis() - appStartTime;
}
saveTodayPlayTime(activity, runTimeThisDay);
lastCheckTime = System.currentTimeMillis();
KLog.d("run time :" + runTimeThisDay);
}
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
KLog.d("onActivitySaveInstanceState" + getActivityName(activity));
}
@Override
public void onActivityDestroyed(Activity activity) {
KLog.d("onActivityDestroyed" + getActivityName(activity));
}
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();
}
/**
* 獲取今日0點(diǎn)的時(shí)間點(diǎn),/1000*1000保證每次取值相同。
* @return
*/
private long getTodayStartTime(){
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
long time = calendar.getTimeInMillis()/1000*1000;
return time;
}
/**
* 保存今日運(yùn)行時(shí)間,以今日0點(diǎn)的時(shí)間作為Key值。
* @param context
* @param time
*/
private void saveTodayPlayTime(Context context, long time){
long todayTime = PreferenceUtil.getLongValue(context,PreferenceUtil.SP_NAME,String.valueOf(getTodayStartTime()) , 0);
KLog.d("today :" + String.valueOf(getTodayStartTime()) + " : time : " + todayTime);
KLog.d("today totalTime:" +time +" :" + (todayTime + time));
PreferenceUtil.putLongValue(context,PreferenceUtil.SP_NAME,String.valueOf(getTodayStartTime()), todayTime+time);
long yesterdayTime = PreferenceUtil.getLongValue(context,PreferenceUtil.SP_NAME_INJOY, String.valueOf(getTodayStartTime() - Constants.ONE_DAY) , 0);
//清除前一天的值
if ( yesterdayTime > 0){
PreferenceUtil.removeData(context,PreferenceUtil.SP_NAME_INJOY,
String.valueOf(getTodayStartTime() - Constants.ONE_DAY));
}
}
}