Android中判断app何时启动和关闭的技术研究

    Android开发中不可避免的会遇到需要检查app何时进入前台,何时被用户关闭。奇怪的是,要达到这个目的并不容易。检查app第一次启动并不难,但要判断它何时重新打开和关闭就没有那么简单了。

    这篇文章将介绍一种判断app打开,重新打开和关闭的技术。

    判断一个app打开和关闭的关键在于判断它的activities是否正在前台显示。让我们先从简单的例子开始,一个只有一个activity的app,而且不支持水平模式。这样想要判断app是打开还是关闭只需要检查activity的onStart和onStop方法即可:

    上面例子的问题在于当需要支持水平模式时该方法就失效了。当我们旋转设备时activity将会重建,onStart方法将被再次调用,这时将会错误的判断为app第二次被打开。

    为了处理设备旋转的情况,我们需要增加一个校验步骤。当activity退出时启动一个定时器,用于判断短时间内app的这个activity是否又被启动,如果没有,说明用户真的退出了这个app,如果重新启动了这个activity,说明用户还逗留在这个app中。

    这种校验方式也适用于拥有多个activities的app,因为从app的一个activity跳转到另一个activity也可以用这种校验方式来处理。

    使用这个技术我创建了一个管理类,所有的activities在可见和不可见时都会通知这个管理类。这个管理类为每个activity处理上述的校验步骤,从而避免错误的检测。它也提供了发布订阅(观察者)模式,任何对app启动和关闭感兴趣的模块都可以通过它来得到对应的通知。

    这个管理类的使用分为三个步骤:

    1. import android.os.Handler;
    2. import android.os.Looper;
    3. import android.os.Message;
    4. import android.support.annotation.NonNull;
    5. import android.text.format.DateUtils;
    6. import java.lang.ref.Reference;
    7. import java.lang.ref.WeakReference;
    8. import java.util.HashSet;
    9. import java.util.Set;
    10. /**
    11. * This class is responsible for tracking all currently open activities.
    12. * By doing so this class can detect when the application is in the foreground
    13. * and when it is running in the background.
    14. */
    15. public class AppForegroundStateManager {
    16. private static final String TAG = AppForegroundStateManager.class.getSimpleName();
    17. private static final int MESSAGE_NOTIFY_LISTENERS = 1;
    18. public static final long APP_CLOSED_VALIDATION_TIME_IN_MS = 30 * DateUtils.SECOND_IN_MILLIS; // 30 Seconds
    19. private Reference<Activity> mForegroundActivity;
    20. private Set<OnAppForegroundStateChangeListener> mListeners = new HashSet<>();
    21. private AppForegroundState mAppForegroundState = AppForegroundState.NOT_IN_FOREGROUND;
    22. private NotifyListenersHandler mHandler;
    23. // Make this class a thread safe singleton
    24. private static class SingletonHolder {
    25. public static final AppForegroundStateManager INSTANCE = new AppForegroundStateManager();
    26. }
    27. public static AppForegroundStateManager getInstance() {
    28. return SingletonHolder.INSTANCE;
    29. }
    30. private AppForegroundStateManager() {
    31. // Create the handler on the main thread
    32. mHandler = new NotifyListenersHandler(Looper.getMainLooper());
    33. }
    34. public enum AppForegroundState {
    35. IN_FOREGROUND,
    36. NOT_IN_FOREGROUND
    37. }
    38. public interface OnAppForegroundStateChangeListener {
    39. /** Called when the foreground state of the app changes */
    40. public void onAppForegroundStateChange(AppForegroundState newState);
    41. }
    42. /** An activity should call this when it becomes visible */
    43. public void onActivityVisible(Activity activity) {
    44. if (mForegroundActivity != null) mForegroundActivity.clear();
    45. mForegroundActivity = new WeakReference<>(activity);
    46. determineAppForegroundState();
    47. }
    48. /** An activity should call this when it is no longer visible */
    49. public void onActivityNotVisible(Activity activity) {
    50. * The foreground activity may have been replaced with a new foreground activity in our app.
    51. * So only clear the foregroundActivity if the new activity matches the foreground activity.
    52. */
    53. if (mForegroundActivity != null) {
    54. Activity ref = mForegroundActivity.get();
    55. if (activity == ref) {
    56. mForegroundActivity.clear();
    57. mForegroundActivity = null;
    58. }
    59. }
    60. determineAppForegroundState();
    61. }
    62. /** Use to determine if this app is in the foreground */
    63. public Boolean isAppInForeground() {
    64. return mAppForegroundState == AppForegroundState.IN_FOREGROUND;
    65. }
    66. /**
    67. * Call to determine the current state, update the tracking global, and notify subscribers if the state has changed.
    68. */
    69. private void determineAppForegroundState() {
    70. /* Get the current state */
    71. AppForegroundState oldState = mAppForegroundState;
    72. /* Determine what the new state should be */
    73. final boolean isInForeground = mForegroundActivity != null && mForegroundActivity.get() != null;
    74. mAppForegroundState = isInForeground ? AppForegroundState.IN_FOREGROUND : AppForegroundState.NOT_IN_FOREGROUND;
    75. /* If the new state is different then the old state the notify subscribers of the state change */
    76. if (mAppForegroundState != oldState) {
    77. validateThenNotifyListeners();
    78. }
    79. }
    80. /**
    81. * Add a listener to be notified of app foreground state change events.
    82. *
    83. * @param listener
    84. */
    85. public void addListener(@NonNull OnAppForegroundStateChangeListener listener) {
    86. mListeners.add(listener);
    87. }
    88. /**
    89. * Remove a listener from being notified of app foreground state change events.
    90. *
    91. * @param listener
    92. */
    93. public void removeListener(OnAppForegroundStateChangeListener listener) {
    94. mListeners.remove(listener);
    95. }
    96. /** Notify all listeners the app foreground state has changed */
    97. private void notifyListeners(AppForegroundState newState) {
    98. android.util.Log.i(TAG, "Notifying subscribers that app just entered state: " + newState);
    99. for (OnAppForegroundStateChangeListener listener : mListeners) {
    100. listener.onAppForegroundStateChange(newState);
    101. }
    102. }
    103. /**
    104. * This method will notify subscribes that the foreground state has changed when and if appropriate.
    105. * <br><br>
    106. * We do not want to just notify listeners right away when the app enters of leaves the foreground. When changing orientations or opening and
    107. * 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
    108. * foreground change is detected during the delay period then the notification will be canceled.
    109. */
    110. private void validateThenNotifyListeners() {
    111. if (mHandler.hasMessages(MESSAGE_NOTIFY_LISTENERS)) {
    112. android.util.Log.v(TAG, "Validation Failed: Throwing out app foreground state change notification");
    113. mHandler.removeMessages(MESSAGE_NOTIFY_LISTENERS);
    114. } else {
    115. if (mAppForegroundState == AppForegroundState.IN_FOREGROUND) {
    116. // If the app entered the foreground then notify listeners right away; there is no validation time for this
    117. mHandler.sendEmptyMessage(MESSAGE_NOTIFY_LISTENERS);
    118. } else {
    119. // We need to validate that the app entered the background. A delay is used to allow for time when the application went into the
    120. // background but we do not want to consider the app being backgrounded such as for in app purchasing flow and full screen ads.
    121. mHandler.sendEmptyMessageDelayed(MESSAGE_NOTIFY_LISTENERS, APP_CLOSED_VALIDATION_TIME_IN_MS);
    122. }
    123. }
    124. }
    125. private class NotifyListenersHandler extends Handler {
    126. private NotifyListenersHandler(Looper looper) {
    127. super(looper);
    128. }
    129. @Override
    130. public void handleMessage(Message inputMessage) {
    131. switch (inputMessage.what) {
    132. // The decoding is done
    133. case MESSAGE_NOTIFY_LISTENERS:
    134. /* Notify subscribers of the state change */
    135. android.util.Log.v(TAG, "App just changed foreground state to: " + mAppForegroundState);
    136. notifyListeners(mAppForegroundState);
    137. break;
    138. default:
    139. super.handleMessage(inputMessage);
    140. }
    141. }
    142. }
    143. }

    2)Activities在可见性改变的需要发送通知

    app中所有activities都要增加下面的代码,用于可见性改变时通知管理类。最好的实现方式是把这段代码加到工程的BaseActivity中。

    3)订阅app的前台可见性改变事件

    在感兴趣的模块中订阅app前台可见性改变事件,application类的onCreate函数是一个不错的地方,它可以保证每次app启动和关闭,你都能得到通知。

    1. public class MyApplication extends Application {
    2. @Override
    3. public void onCreate() {
    4. super.onCreate();
    5. AppForegroundStateManager.getInstance().addListener(this);
    6. }
    7. @Override
    8. public void onAppForegroundStateChange(AppForegroundStateManager.AppForegroundState newState) {
    9. if (AppForegroundStateManager.AppForegroundState.IN_FOREGROUND == newState) {
    10. // App just entered the foreground. Do something here!
    11. } else {
    12. // App just entered the background. Do something here!
    13. }
    14. }
    15. }

    有一些细节需要进一步讨论,下面讨论的几点针对具体的应用可以做微调。

    校验定时器检查app是否真的进入后台的时间间隔是多少合适呢?上面的代码设置为30秒,原因如下。

    当你的app在运行时,可能存在第三方的activities会覆盖全屏幕,一些常见的例子是Google应用内购买和Facebook登录注册页面。这些情况下你的app都会被迫进入后台,前台用于显示这些第三方页面。如果把这种情况当做用户离开了你的app,显然是不对的。30秒超时设置就是用来避免这种情况的。例如当用户在30秒内完成应用内购买,大部分用户都可以做得到,那么就不会当做用户突然离开app了。

    如果你的app不存在上述这种情况,我建议可以把你的校验时间设置为4秒,这样对于低配设备当屏幕旋转重新创建activity的时间间隔是合适的。

    CPU休眠

    现在我们已经知道如何检测app何时启动和关闭,但我们不知道app是如何启动的。是用户点击通知栏消息?还是点击一个链接?亦或是他们直接通过桌面图标或最近使用启动?

    首先我们需要知道在哪里检测app是如何启动的。基于前面一个例子我们可以打印出app何时启动,以及如何启动。

    设置启动机制

    现在我们可以打印app何时启动的机制,但我们没有设置它。因此下一步就是在用户通过链接或者通知启动app时我们记下它。如果没有通过这两种方式设置过,说明用户是通过点击app图标启动的。

    为了跟踪用户点击链接打开app,你需要找到代码中处理链接的地方,并加入下面的代码来跟踪启动机制。要确保这些代码在activity的onStart()函数之前调用。在哪些地方加入下面的代码取决于你的app架构了。

    1. getApplication().setLaunchMechanism(LaunchMechanism.URL);

    跟踪通知事件

    不幸的是跟踪通知点击需要更多技巧,通知显示后,点击它将会打开之前绑定好的一个PendingIntent,这里的技巧是为通知的所有PendingIntents添加一个标识表明是由通知发出的。

    例如当为通知创建PendingIntent时为每个intent添加如下代码:

    到这一步我们需要做的就是在每个activity(统一在BaseActivity中添加)中检查这个标识。当识别到这个标识时说明是从通知启动的,这时可以把启动机制设置为通过通知。这一步应该在onCreate中处理,这样在app启动到前台之前就设置好了(会触发启动机制的打印)。

    1. @Override
    2. public void onCreate(Bundle savedInstanceState) {
    3. super.onCreate(savedInstanceState);
    4. Intent intent = getIntent();
    5. if (intent != null && intent.getExtras() != null) {
    6. // Detect if the activity was launched by the user clicking on a notification
    7. if (intent.getExtras().getBoolean(EXTRA_HANDLING_NOTIFICATION, false)) {
    8. // Notify that the activity was opened by the user clicking on a notification.
    9. getApplication().setLaunchMechanism(LaunchMechanism.NOTIFICATION);
    10. }
    11. }

    本文接近尾声了,到这里你应该已经掌握了如何检测app何时启动和关闭,以及它是如何启动的。