别再用错方法了!Android App前后台判断的那些坑
做Android开发好几年了,光是“判断App当前是在前台还是后台”这个需求,我就见过不下十种写法,而且有一半都是错的。最近帮同事排查一个线上问题——用户说“明明App就在眼前,为啥弹窗说我退出了?”——查到最后发现是他用onResume和onPause做计数器,结果每次打开相机拍照(系统相机把Activity盖住了但没完全盖住)就触发后台逻辑。这场景你经历过没?
今天不整那些虚的,就把我自己踩过的坑、看过源码后理解的原理,以及现在线上项目还在用的两种靠谱方案,掰开揉碎了说清楚。顺带提一嘴同事还说了一种ActivityManager方法,那玩意儿确实能用,但有坑。
一、从“外卖App”说起
去年我们做外卖骑手端App,有个需求:当App退到后台超过30秒,自动把实时订单轨迹的刷新频率从1秒降到10秒,省电;一旦切回前台,立马恢复高频刷新。这个需求看似简单,但如果前后台判断不准,要么骑手切出去回个消息回来,界面半天不刷新(误判为还在后台),要么人明明在聊天界面上,轨迹还在高频跑电(误判为前台)。
后面我试了好几种方案,最后留下的就两套:自己写计数器 和 Google的ProcessLifecycleOwner。
二、自己手撸一个计数器(最灵活,也最容易写错)
说白了就是通过Application.registerActivityLifecycleCallbacks监听每个Activity的onStarted和onStopped,用一个整数记录当前有多少个Activity处在“可见”状态。
先上代码,我用的是Kotlin,但Java也一个道理:
class RiderApplication : Application() {companion object {private var visibleActivityCount = 0var isAppInForeground = falseprivate 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之间跳转时的误判问题。
怎么用?三步:
-
加入依赖(别搞错版本):
implementation "androidx.lifecycle:lifecycle-process:2.6.2"
-
在
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 ActivityManagerval appProcesses = activityManager.runningAppProcesses ?: return falseval packageName = context.packageNamefor (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。唯一一次问题是用户反馈“退后台后还在高频刷新”,排查半天发现是另一个同事在服务里强制解锁了系统省电模式……那是另一个故事了。
夜雨聆风