
閑談
好久沒更新了,前兩個月就定了標題,現(xiàn)在才開始動手寫。其實之前覺得這種獲取App累積時長的方法(以下內(nèi)容會介紹到)不唯一,可能有很多種,所以想收集多點資料,后來看到騰訊和360都是這么實現(xiàn)的,那么現(xiàn)在就開始介紹一下這個需求。
相信做游戲sdk開發(fā)的最近經(jīng)常有這個需求,為什么是游戲sdk開發(fā)需要?而且為什么是最近?
首先回答第一個問題,這個需求主要是要限制用戶對于app的使用時長,一般較為常見的是限制游戲游玩時長,其次,很少有Android應用有統(tǒng)計使用時長這一個需求。
第二個問題,為什么是最近?

相信玩過王者榮耀的對這個很熟悉吧
沒錯,最近可以說國內(nèi)的防沉迷越來越嚴重,導致出現(xiàn)幾乎所有游戲,都需要提供防沉迷系統(tǒng)來限制未成年人的游戲時長。關(guān)于這個舉措,我不作討論,也不是這個blog的內(nèi)容。。
案例
話不多說,Demo源碼+示例圖:

gif例
(點我,我是源碼
實現(xiàn)
- 現(xiàn)在有一些是通過在各個Activity中埋點來實現(xiàn)計時長的操作的,也就是通過跳轉(zhuǎn)Activity觸發(fā)其生命周期回調(diào)來作為監(jiān)控點,從而去獲取累積時長的,這種方法也可以。但是Google官方有提供一個api:
Application.ActivityLifecycleCallbacks(API14以上,現(xiàn)在還有低于14開發(fā)的嗎?不是吧不是吧)
這個api最好在Application中調(diào)用,作為貫穿整個應用的對象,applicationContext擁有比Activity更長的生命周期,更易維護,回調(diào)函數(shù)少,也不用申請權(quán)限(Android6.0),因為作為sdk開發(fā)而言,權(quán)限盡量越少越好。
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
ActivityLifeCycle lifecycleCallbacks = new ActivityLifeCycle();
registerActivityLifecycleCallbacks(lifecycleCallbacks);
}
}
我們可以實現(xiàn)一個ActivityLifeCycle繼承Application.ActivityLifecycleCallbacks
implements Application.ActivityLifecycleCallbacks
下面是實現(xiàn)的代碼:
public class ActivityLifeCycle implements Application.ActivityLifecycleCallbacks {
/**
* 上次檢查時間,用于在運行時作為基準獲取用戶時間
*/
public static long lastCheckTime = 0;
/**
* 前臺Activity數(shù)量
**/
private int foregroundActivityCount = 0;
/**
* Activity是否在修改配置,
*/
private boolean isChangingConfigActivity = false;
/**
* 應用將要切換到前臺
*/
private boolean willSwitchToForeground = false;
/**
* 當前是否在前臺
*/
private boolean isForegroundNow = false;
/**
* 上次暫停的Activity信息
*/
private String lastPausedActivityName;
private int lastPausedActivityHashCode;
private long lastPausedTime;
private long appUseReduceTime = 0;
/**
* 每次有Activity啟動時的開始時間點
*/
private long appStartTime = 0L;
/**
* 本次統(tǒng)計時,運行的時間
*/
private long runTimeThisDay = 0L;
@Override
public void onActivityCreated(Activity activity, Bundle bundle) {
Logger.msg("onActivityCreated" + getActivityName(activity));
}
@Override
public void onActivityStarted(Activity activity) {
Logger.msg("onActivityStarted " + activity.getClass().getSimpleName() + " " + foregroundActivityCount);
//前臺沒有Activity,說明新啟動或者將從從后臺恢復
if (foregroundActivityCount == 0 || !isForegroundNow) {
willSwitchToForeground = true;
} else {
//應用已經(jīng)在前臺,此時保存今日運行的時間。
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) {
Logger.msg("onActivityResumed" + getActivityName(activity));
//在這里更新檢查時間點,是為了保證從后臺恢復到前臺,持續(xù)計時的準確性。
lastCheckTime = System.currentTimeMillis();
addAppUseReduceTimeIfNeeded(activity);
if (willSwitchToForeground && isInteractive(activity)) {
isForegroundNow = true;
Logger.msg("switch to foreground");
}
if (isForegroundNow) {
willSwitchToForeground = false;
}
}
@Override
public void onActivityPaused(Activity activity) {
Logger.msg("onActivityPaused" + getActivityName(activity));
lastPausedActivityName = getActivityName(activity);
lastPausedActivityHashCode = activity.hashCode();
lastPausedTime = System.currentTimeMillis();
}
@Override
public void onActivityStopped(Activity activity) {
Logger.msg("onActivityStopped" + getActivityName(activity));
addAppUseReduceTimeIfNeeded(activity);
//如果這個Activity實在修改配置,如旋轉(zhuǎn)等,則不保存時間直接返回
if (activity.isChangingConfigurations()) {
isChangingConfigActivity = true;
return;
}
//該Activity要進入后臺,前臺Activity數(shù)量-1。
foregroundActivityCount -= 1;
//當前已經(jīng)是最后的一個Activity,代表此時應用退出了,保存時間。
// 如果跨天了,則從新一天的0點開始計時
if (foregroundActivityCount == 0) {
isForegroundNow = false;
Logger.msg("switch to background (reduce time[" + appUseReduceTime + "])");
// if (getTodayStartTime() > appStartTime){
// runTimeThisDay = System.currentTimeMillis() - getTodayStartTime();
// }else {
runTimeThisDay = System.currentTimeMillis() - appStartTime;
saveTodayPlayTime(activity, runTimeThisDay);
lastCheckTime = System.currentTimeMillis();
Logger.msg("run time :" + runTimeThisDay);
}
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
Logger.msg("onActivitySaveInstanceState" + getActivityName(activity));
}
@Override
public void onActivityDestroyed(Activity activity) {
runTimeThisDay = System.currentTimeMillis() - appStartTime;
saveTodayPlayTime(activity, runTimeThisDay);
lastCheckTime = System.currentTimeMillis();
Logger.msg("onActivityDestroyed" + getActivityName(activity) + "--runTimeThisDay:" + runTimeThisDay + "--lastCheckTime" + lastCheckTime);
}
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();
}
/**
* 保存運行時間
*
* @param context
* @param time
*/
private void saveTodayPlayTime(Context context, long time) {
long todayTime = ShareprefUtils.getLong(context, "APP_USE_TIME", 0);
Logger.msg("使用時長Log:" + (todayTime + time));
ShareprefUtils.saveLong(context, "APP_USE_TIME", todayTime + time);
}
}
累積時長會保存在本地sharepreference文件,當然也可以上傳服務器。
另外,ActivityLifecycleCallbacks這個api還可以用來管理 Activity 頁面棧,判斷應用前后臺,應用新開進程假重啟處理等
