只有兩種東西能讓一個團隊團結,恐懼或忠誠。---《速度與激情7》 java
原文連接:http://engineering.meetme.com/2015/04/android-determine-when-app-is-opened-or-closed/ android
Android開發中不可避免的會遇到須要檢查app什麼時候進入前臺,什麼時候被用戶關閉。奇怪的是,要達到這個目的並不容易。檢查app第一次啓動並不難,但要判斷它什麼時候從新打開和關閉就沒有那麼簡單了。 架構
這篇文章將介紹一種判斷app打開,從新打開和關閉的技術。 app
判斷一個app打開和關閉的關鍵在於判斷它的activities是否正在前臺顯示。讓咱們先從簡單的例子開始,一個只有一個activity的app,並且不支持水平模式。這樣想要判斷app是打開仍是關閉只須要檢查activity的onStart和onStop方法便可: ide
@Override protected void onStart() { super.onStart(); // The Application has been opened! } @Override protected void onStop() { super.onStop(); // The Application has been closed! }
上面例子的問題在於當須要支持水平模式時該方法就失效了。當咱們旋轉設備時activity將會重建,onStart方法將被再次調用,這時將會錯誤的判斷爲app第二次被打開。 函數
爲了處理設備旋轉的狀況,咱們須要增長一個校驗步驟。當activity退出時啓動一個定時器,用於判斷短期內app的這個activity是否又被啓動,若是沒有,說明用戶真的退出了這個app,若是從新啓動了這個activity,說明用戶還逗留在這個app中。 oop
這種校驗方式也適用於擁有多個activities的app,由於從app的一個activity跳轉到另外一個activity也能夠用這種校驗方式來處理。 ui
使用這個技術我建立了一個管理類,全部的activities在可見和不可見時都會通知這個管理類。這個管理類爲每一個activity處理上述的校驗步驟,從而避免錯誤的檢測。它也提供了發佈訂閱(觀察者)模式,任何對app啓動和關閉感興趣的模塊均可以經過它來獲得對應的通知。 this
這個管理類的使用分爲三個步驟: spa
1)把它添加到你的工程中
import android.app.Activity; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.support.annotation.NonNull; import android.text.format.DateUtils; import java.lang.ref.Reference; import java.lang.ref.WeakReference; import java.util.HashSet; import java.util.Set; /** * This class is responsible for tracking all currently open activities. * By doing so this class can detect when the application is in the foreground * and when it is running in the background. */ public class AppForegroundStateManager { private static final String TAG = AppForegroundStateManager.class.getSimpleName(); private static final int MESSAGE_NOTIFY_LISTENERS = 1; public static final long APP_CLOSED_VALIDATION_TIME_IN_MS = 30 * DateUtils.SECOND_IN_MILLIS; // 30 Seconds private Reference<Activity> mForegroundActivity; private Set<OnAppForegroundStateChangeListener> mListeners = new HashSet<>(); private AppForegroundState mAppForegroundState = AppForegroundState.NOT_IN_FOREGROUND; private NotifyListenersHandler mHandler; // Make this class a thread safe singleton private static class SingletonHolder { public static final AppForegroundStateManager INSTANCE = new AppForegroundStateManager(); } public static AppForegroundStateManager getInstance() { return SingletonHolder.INSTANCE; } private AppForegroundStateManager() { // Create the handler on the main thread mHandler = new NotifyListenersHandler(Looper.getMainLooper()); } public enum AppForegroundState { IN_FOREGROUND, NOT_IN_FOREGROUND } public interface OnAppForegroundStateChangeListener { /** Called when the foreground state of the app changes */ public void onAppForegroundStateChange(AppForegroundState newState); } /** An activity should call this when it becomes visible */ public void onActivityVisible(Activity activity) { if (mForegroundActivity != null) mForegroundActivity.clear(); mForegroundActivity = new WeakReference<>(activity); determineAppForegroundState(); } /** An activity should call this when it is no longer visible */ public void onActivityNotVisible(Activity activity) { /* * The foreground activity may have been replaced with a new foreground activity in our app. * So only clear the foregroundActivity if the new activity matches the foreground activity. */ if (mForegroundActivity != null) { Activity ref = mForegroundActivity.get(); if (activity == ref) { // This is the activity that is going away, clear the reference mForegroundActivity.clear(); mForegroundActivity = null; } } determineAppForegroundState(); } /** Use to determine if this app is in the foreground */ public Boolean isAppInForeground() { return mAppForegroundState == AppForegroundState.IN_FOREGROUND; } /** * Call to determine the current state, update the tracking global, and notify subscribers if the state has changed. */ private void determineAppForegroundState() { /* Get the current state */ AppForegroundState oldState = mAppForegroundState; /* Determine what the new state should be */ final boolean isInForeground = mForegroundActivity != null && mForegroundActivity.get() != null; mAppForegroundState = isInForeground ? AppForegroundState.IN_FOREGROUND : AppForegroundState.NOT_IN_FOREGROUND; /* If the new state is different then the old state the notify subscribers of the state change */ if (mAppForegroundState != oldState) { validateThenNotifyListeners(); } } /** * Add a listener to be notified of app foreground state change events. * * @param listener */ public void addListener(@NonNull OnAppForegroundStateChangeListener listener) { mListeners.add(listener); } /** * Remove a listener from being notified of app foreground state change events. * * @param listener */ public void removeListener(OnAppForegroundStateChangeListener listener) { mListeners.remove(listener); } /** Notify all listeners the app foreground state has changed */ private void notifyListeners(AppForegroundState newState) { android.util.Log.i(TAG, "Notifying subscribers that app just entered state: " + newState); for (OnAppForegroundStateChangeListener listener : mListeners) { listener.onAppForegroundStateChange(newState); } } /** * This method will notify subscribes that the foreground state has changed when and if appropriate. * <br><br> * We do not want to just notify listeners right away when the app enters of leaves the foreground. When changing orientations or opening and * closing the app quickly we briefly pass through a NOT_IN_FOREGROUND state that must be ignored. To accomplish this a delayed message will be * Sent when we detect a change. We will not notify that a foreground change happened until the delay time has been reached. If a second * foreground change is detected during the delay period then the notification will be canceled. */ private void validateThenNotifyListeners() { // If the app has any pending notifications then throw out the event as the state change has failed validation if (mHandler.hasMessages(MESSAGE_NOTIFY_LISTENERS)) { android.util.Log.v(TAG, "Validation Failed: Throwing out app foreground state change notification"); mHandler.removeMessages(MESSAGE_NOTIFY_LISTENERS); } else { if (mAppForegroundState == AppForegroundState.IN_FOREGROUND) { // If the app entered the foreground then notify listeners right away; there is no validation time for this mHandler.sendEmptyMessage(MESSAGE_NOTIFY_LISTENERS); } else { // We need to validate that the app entered the background. A delay is used to allow for time when the application went into the // background but we do not want to consider the app being backgrounded such as for in app purchasing flow and full screen ads. mHandler.sendEmptyMessageDelayed(MESSAGE_NOTIFY_LISTENERS, APP_CLOSED_VALIDATION_TIME_IN_MS); } } } private class NotifyListenersHandler extends Handler { private NotifyListenersHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message inputMessage) { switch (inputMessage.what) { // The decoding is done case MESSAGE_NOTIFY_LISTENERS: /* Notify subscribers of the state change */ android.util.Log.v(TAG, "App just changed foreground state to: " + mAppForegroundState); notifyListeners(mAppForegroundState); break; default: super.handleMessage(inputMessage); } } } }
2)Activities在可見性改變的須要發送通知
app中全部activities都要增長下面的代碼,用於可見性改變時通知管理類。最好的實現方式是把這段代碼加到工程的BaseActivity中。
@Override protected void onStart() { super.onStart(); AppForegroundStateManager.getInstance().onActivityVisible(this); } @Override protected void onStop() { AppForegroundStateManager.getInstance().onActivityNotVisible(this); super.onStop(); }
3)訂閱app的前臺可見性改變事件
在感興趣的模塊中訂閱app前臺可見性改變事件,application類的onCreate函數是一個不錯的地方,它能夠保證每次app啓動和關閉,你都能獲得通知。
public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); AppForegroundStateManager.getInstance().addListener(this); } @Override public void onAppForegroundStateChange(AppForegroundStateManager.AppForegroundState newState) { if (AppForegroundStateManager.AppForegroundState.IN_FOREGROUND == newState) { // App just entered the foreground. Do something here! } else { // App just entered the background. Do something here! } } }
有一些細節須要進一步討論,下面討論的幾點針對具體的應用能夠作微調。
校驗定時器檢查app是否真的進入後臺的時間間隔是多少合適呢?上面的代碼設置爲30秒,緣由以下。
當你的app在運行時,可能存在第三方的activities會覆蓋全屏幕,一些常見的例子是Google應用內購買和Facebook登陸註冊頁面。這些狀況下你的app都會被迫進入後臺,前臺用於顯示這些第三方頁面。若是把這種狀況當作用戶離開了你的app,顯然是不對的。30秒超時設置就是用來避免這種狀況的。例如當用戶在30秒內完成應用內購買,大部分用戶均可以作獲得,那麼就不會當作用戶忽然離開app了。
若是你的app不存在上述這種狀況,我建議能夠把你的校驗時間設置爲4秒,這樣對於低配設備當屏幕旋轉從新建立activity的時間間隔是合適的。
可能存在的問題是當用戶關閉app或者app仍處於前臺時用戶鎖屏了,這時CPU可能不會等到定時器檢測就休眠了。爲了保證這種狀況下定時器可以正常檢測用戶退出app,咱們須要持有wakelock防止CPU休眠直到app關閉事件被確認。實踐中相比使用wakelock,這種狀況並不算問題。
如今咱們已經知道如何檢測app什麼時候啓動和關閉,但咱們不知道app是如何啓動的。是用戶點擊通知欄消息?仍是點擊一個連接?亦或是他們直接經過桌面圖標或最近使用啓動?
首先咱們須要知道在哪裏檢測app是如何啓動的。基於前面一個例子咱們能夠打印出app什麼時候啓動,以及如何啓動。
public class MyApplication extends Application { public final String TAG = MyApplication.class.getSimpleName(); public enum LaunchMechanism { DIRECT, NOTIFICATION, URL; } private LaunchMechanism mLaunchMechanism = LaunchMechanism.DIRECT; public void setLaunchMechanism(LaunchMechanism launchMechanism) { mLaunchMechanism = launchMechanism; } @Override public void onCreate() { super.onCreate(); AppForegroundStateManager.getInstance().addListener(this); } @Override public void onAppForegroundStateChange(AppForegroundStateManager.AppForegroundState newState) { if (AppForegroundStateManager.AppForegroundState.IN_FOREGROUND.equals(newState)) { // App just entered the foreground. Log.i(TAG, "App Just Entered the Foreground with launch mechanism of: " + mLaunchMechanism); } else { // App just entered the background. Set our launch mode back to the default of direct. mLaunchMechanism = LaunchMechanism.DIRECT; } } }
如今咱們能夠打印app什麼時候啓動的機制,但咱們沒有設置它。所以下一步就是在用戶經過連接或者通知啓動app時咱們記下它。若是沒有經過這兩種方式設置過,說明用戶是經過點擊app圖標啓動的。
爲了跟蹤用戶點擊連接打開app,你須要找到代碼中處理連接的地方,並加入下面的代碼來跟蹤啓動機制。要確保這些代碼在activity的onStart()函數以前調用。在哪些地方加入下面的代碼取決於你的app架構了。
getApplication().setLaunchMechanism(LaunchMechanism.URL);
不幸的是跟蹤通知點擊須要更多技巧,通知顯示後,點擊它將會打開以前綁定好的一個PendingIntent,這裏的技巧是爲通知的全部PendingIntents添加一個標識代表是由通知發出的。
例如當爲通知建立PendingIntent時爲每一個intent添加以下代碼:
public static final String EXTRA_HANDLING_NOTIFICATION = "Notification.EXTRA_HANDLING_NOTIFICATION"; // Put an extra so we know when an activity launches if it is a from a notification intent.putExtra(EXTRA_HANDLING_NOTIFICATION, true);
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent = getIntent(); if (intent != null && intent.getExtras() != null) { // Detect if the activity was launched by the user clicking on a notification if (intent.getExtras().getBoolean(EXTRA_HANDLING_NOTIFICATION, false)) { // Notify that the activity was opened by the user clicking on a notification. getApplication().setLaunchMechanism(LaunchMechanism.NOTIFICATION); } } }