乐于分享
好东西不私藏

别再用错方法了!Android App前后台判断的那些坑

别再用错方法了!Android App前后台判断的那些坑

做Android开发好几年了,光是“判断App当前是在前台还是后台”这个需求,我就见过不下十种写法,而且有一半都是错的。最近帮同事排查一个线上问题——用户说“明明App就在眼前,为啥弹窗说我退出了?”——查到最后发现是他用onResumeonPause做计数器,结果每次打开相机拍照(系统相机把Activity盖住了但没完全盖住)就触发后台逻辑。这场景你经历过没?

今天不整那些虚的,就把我自己踩过的坑、看过源码后理解的原理,以及现在线上项目还在用的两种靠谱方案,掰开揉碎了说清楚。顺带提一嘴同事还说了一种ActivityManager方法,那玩意儿确实能用,但有坑。

一、从“外卖App”说起

去年我们做外卖骑手端App,有个需求:当App退到后台超过30秒,自动把实时订单轨迹的刷新频率从1秒降到10秒,省电;一旦切回前台,立马恢复高频刷新。这个需求看似简单,但如果前后台判断不准,要么骑手切出去回个消息回来,界面半天不刷新(误判为还在后台),要么人明明在聊天界面上,轨迹还在高频跑电(误判为前台)。

后面我试了好几种方案,最后留下的就两套:自己写计数器 和 Google的ProcessLifecycleOwner

二、自己手撸一个计数器(最灵活,也最容易写错)

说白了就是通过Application.registerActivityLifecycleCallbacks监听每个Activity的onStartedonStopped,用一个整数记录当前有多少个Activity处在“可见”状态。

先上代码,我用的是Kotlin,但Java也一个道理:

class RiderApplication : Application() {    companion object {        private var visibleActivityCount = 0        var isAppInForeground = false            private set    }    override funonCreate() {        super.onCreate()        registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {            override funonActivityStarted(activity: Activity) {                if (visibleActivityCount == 0) {                    // 从0变成1的那一刻,说明App刚从后台切到前台                    isAppInForeground = true                    // 给骑手发个震动提醒?或者恢复高频刷新                    restoreHighFrequencyLocation()                }                visibleActivityCount++            }            override funonActivityStopped(activity: Activity) {                visibleActivityCount--                if (visibleActivityCount == 0) {                    // 从1变成0,所有Activity都看不见了,退到后台                    isAppInForeground = false                    // 启动一个30秒的延迟任务,降低刷新频率                    scheduleReduceFrequency()                }            }            // 其他几个方法空实现就行            override funonActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}            override funonActivityResumed(activity: Activity) {}            override funonActivityPaused(activity: Activity) {}            override funonActivitySaveInstanceState(activity: Activity, outState: Bundle) {}            override funonActivityDestroyed(activity: Activity) {}        })    }}

为什么这里用onActivityStarted而不是onResume 这里真的有一半人会搞错。onResume代表“获得了焦点”,而onStarted代表“用户看得见”。两者区别太大了——举个例子:你骑手端正在导航,突然系统弹出一个“低电量请开启省电模式”的对话框(系统级弹窗)。这时候你当前的Activity会执行onPause(失去焦点),但onStop不会触发,因为Activity还在屏幕上,只是对话框盖在上面而已。如果你用onResume计数,这个瞬间计数器会减1,可能就误判为后台了。

再举个更生活化的:你老婆微信给你发了个视频通话邀请,那个悬浮通知也会让你的App失去焦点但依然可见。这时候如果前后台判断不准,就会出bug。

计数器的优点:实时,没有任何延迟。你从后台点图标进来,onActivityStarted瞬间就执行了,立马能拿到activity对象,想弹Dialog就弹Dialog,想刷新UI就刷新UI。

缺点:你得自己写这一坨代码,而且容易忘记处理边界情况(比如多进程、横竖屏切换等)。另外注意,如果App进程被系统回收了,visibleActivityCount会重置为0,下次启动时从0变1会触发一次前台事件,这通常符合预期,但你要确保恢复逻辑不会重复执行。

三、官方推荐的ProcessLifecycleOwner(省心但有延迟)

Google后来在androidx.lifecycle里搞了个ProcessLifecycleOwner,说白了就是把上面计数器的逻辑封装好了,还加了个“防抖”功能——解决了Activity之间跳转时的误判问题。

怎么用?三步:

  1. 加入依赖(别搞错版本):

implementation "androidx.lifecycle:lifecycle-process:2.6.2"
  1. Application里随便注册个观察者:

class RiderApplication : Application() {    override funonCreate() {        super.onCreate()        ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {            override funonStart(owner: LifecycleOwner) {                // 这里代表App进入了前台                restoreHighFrequencyLocation()            }            override funonStop(owner: LifecycleOwner) {                // 这里代表App退到了后台                scheduleReduceFrequency()            }        })    }}

源码里藏着什么秘密? 当时我为了搞清楚为什么它要比计数器“慢半拍”,专门去翻ProcessLifecycleOwner的源码,发现它在onActivityStopped后并不会马上分发ON_STOP事件,而是通过Handler发了个延迟消息,默认延迟700毫秒。

为什么?想象一下:你从骑手端订单列表页点进订单详情页——旧Activity(列表页)执行onStop,新Activity(详情页)执行onStart。如果没有这700毫秒延迟,在旧Activity停止的瞬间,计数器就会变成0,触发“后台”事件,紧接着新Activity启动又触发“前台”事件。这样一次正常的页面跳转,App会以为自己在后台打了个转又回来,简直是神经病。Google加这700ms,就是为了等一等:如果700ms内又有新的Activity启动了,那就取消后台事件;如果确实没再启动,才真正切到后台。

所以这个延迟不是bug,而是feature。 但对某些业务来说700ms太长了。比如你希望用户从后台回来时立刻弹出一个“欢迎回来,今日已接单8笔”的浮层,用ProcessLifecycleOwner就得等700ms,用户体验上会觉得卡了一下。这种场景还是用计数器方案合适。

四、同事推荐的“ActivityManager方法”

我来贴一下这段代码:

funisAppInForeground(context: Context)Boolean {    val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager    val appProcesses = activityManager.runningAppProcesses ?: return false    val packageName = context.packageName    for (appProcess in appProcesses) {        if (appProcess.processName == packageName &&            appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND        ) {            return true        }    }    return false}

这个方法确实能实时告诉你当前进程的“重要程度”,IMPORTANCE_FOREGROUND代表进程正在前台运行。但坑点在于:这个方法获取的是进程级别的前台状态,而Android 5.0以后,runningAppProcesses可能只返回当前进程的信息(受权限限制),而且在高版本上频繁调用会有性能问题。另外它无法区分你的App是不是只是有个服务在跑但界面全被盖住了——对于我那个外卖需求来说,这不够精细。但如果你只是想知道“App是不是大概活着并且可见”,这个方法简单粗暴够用了。

五、到底该用哪个?我给的建议

写了这么多年Android,我现在的习惯是:

  • 全局后台任务(比如清理缓存、上报在线时长、降低网络请求频率)→ 无脑上ProcessLifecycleOwner。省代码,Google维护,边界情况处理得比我周到。

  • 跟UI强相关的操作(比如退后台时保存草稿、回前台时弹出一个带当前Activity上下文的Dialog)→ 用手写计数器。因为在onActivityStarted回调里你能直接拿到Activity对象,不用费劲去搞全局的CurrentActivity管理。

  • 千万别用onResume/onPause做前后台判断,除非你完全确定你的App永远不会被系统弹窗、透明Activity、分屏等场景干扰。

最后说一句,如果你项目里已经用了ProcessLifecycleOwner,但又想在用户回前台的瞬间立刻弹个东西,可以组合使用:ProcessLifecycleOwner负责静默的后台任务,计数器只负责快速弹窗。两者不冲突。

   那个外卖App后来上线跑了大半年,前后台判断没出过因为方案选错导致的bug。唯一一次问题是用户反馈“退后台后还在高频刷新”,排查半天发现是另一个同事在服务里强制解锁了系统省电模式……那是另一个故事了。