文档章节

Android 判断app何时是打开或者关闭的技术研究

yaly
 yaly
发布于 2015/05/07 20:06
字数 2552
阅读 1158
收藏 32

只有两种东西能让一个团队团结,恐惧或忠诚。---《速度与激情7》

原文链接:http://engineering.meetme.com/2015/04/android-determine-when-app-is-opened-or-closed/

存在的问题

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

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

让我们开始吧

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

@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中。

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

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

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

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的时间间隔是合适的。

CPU休眠

可能存在的问题是当用户关闭app或者app仍处于前台时用户锁屏了,这时CPU可能不会等到定时器检测就休眠了。为了保证这种情况下定时器能够正常检测用户退出app,我们需要持有wakelock防止CPU休眠直到app关闭事件被确认。实践中相比使用wakelock,这种情况并不算问题。

判断app是如何启动的

现在我们已经知道如何检测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);



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

@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);
        }
    }
}



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

本文转载自:http://asce1885.gitbooks.io/android-rd-senior-advanced/content/androidzhong_pan_duan_app_he_shi_q...

共有 人打赏支持
yaly
粉丝 3
博文 99
码字总数 48367
作品 0
海淀
Android工程师
私信 提问
各位大侠们,根据你们的建议 我稍作修改了一下我的简历 你们看行吗?

我目前所在地是 河南商丘, 工作的目标地是 深圳附近。 我在 性格方面 是一个乐观积极的人,动手能力强,喜欢钻研。我在 能力方面 是一个负责并充满能量的年轻人,不懂就学,不会就问问,问百...

farley-fu
2014/09/05
1K
20
基于原生的移动跨平台研究和实践

基于原生主要是针对基于webview+h5比较来说的,基于H5的我不想再讨论了,我想尝试的是从UI到功能都是原生,而不是用H5模拟的所谓原生体验。 背景 我们从开发角度来考虑,但凡想从事长远的开发...

voxer
2017/03/26
109
0
那些年Android黑科技①:只要活着,就有希望

“黑科技什么的最喜欢了! 对,我们就是要搞事。 来呀。谁怕谁。三年血赚,死刑不亏。(๑´ڡ`๑) ” -- 来自暗世界android工程师 前言: 这个世界上手机有三大系统,苹果、 安卓、 中国安卓 ...

猴亮屏
2017/10/24
0
0
在Android手机上实现应用屏蔽

在Android手机上,很多App都实现了应用屏蔽功能。比如,360手机卫士可以在用户启动其他应用程序的时候弹出一个验证窗口来实现应用程序的加密,「我要当学霸」、「我要早睡」、「番茄土豆」可...

Cundong
2014/06/29
0
4
Android EditText不弹出输入法总结,焦点问题的总结

看一个manifest中Activity的配置,如果这个页面有EditText,并且我们想要进入这个页面的时候默认弹出输入法,可以这样设置这个属相:android:windowSoftInputMode=stateVisible,这样就会默认...

带梦想一7飞
2013/07/12
0
0

没有更多内容

加载失败,请刷新页面

加载更多

使用正则表达式实现网页爬虫的思路详解

网页爬虫:就是一个程序用于在互联网中获取指定规则的数据。这篇文章主要介绍了使用正则表达式实现网页爬虫的思路详解,需要的朋友可以参考下 网页爬虫:就是一个程序用于在互联网中获取指定规...

前端小攻略
23分钟前
0
0
vue中锚点的三种方法

第一种: router.js中添加 mode: 'history', srcollBehavior(to,from,savedPosition){ if(to.hash){ return {selector:to.hash } } } 组件: <template><div><ul class="li......

peakedness丶
24分钟前
0
0
记一次面试最常见的10个Redis"刁难"问题

导读:在程序员面试过程中Redis相关的知识是常被问到的话题。作为一名在互联网技术行业打击过成百上千名的资深技术面试官,本文作者总结了面试过程中经常问到的问题。十分值得一读。 Redis在...

小刀爱编程
今天
20
0
TiDB Lab 诞生记 | TiDB Hackathon 优秀项目分享

本文由红凤凰粉凤凰粉红凤凰队的成员主笔,他们的项目 TiDB Lab 在本届 TiDB Hackathon 2018 中获得了二等奖。TiDB Lab 为 TiDB 培训体系增加了一个可以动态观测 TiDB / TiKV / PD 细节的动画...

TiDB
今天
5
0
当区块链遇到零知识证明

本文由云+社区发表 当区块链遇到零知识证明 什么是零知识证明 零知识证明的官方定义是能够在不向验证者任何有用的信息的情况下,使验证者相信某个论断是正确的。这个定义有点抽象,下面笔者举...

腾讯云加社区
今天
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部