一、需求背景与核心挑战

功能需求

需实现一个全局悬浮窗(Floating View),具体行为规范如下:

  • 应用位于前台时,隐藏悬浮窗,避免遮挡用户正常操作。
  • 应用退至后台时,显示悬浮窗,使其保持悬浮于系统或其他应用界面之上。

核心挑战

悬浮窗通常由独立的 Service 组件维护,以确保其生命周期独立于 Activity。因此,该需求的技术难点在于:在后台运行的 Service 如何实时、准确地感知当前应用的前后台状态切换?


二、悬浮窗实现的核心原理剖析

在阐述具体代码方案前,需明确通过"前台 Service + WindowManager"实现悬浮窗效果的底层原理。此机制主要涉及 Android 的显示架构与进程保活策略:

2.1 Android 的窗口层级体系 (Z-order)

Android 的显示系统并非局限于各个应用单独绘制自身区域。屏幕上的所有渲染内容均由 WindowManagerService (WMS) 统一管理。WMS 维护着一个全局的窗口 Z-order 栈,不同类型的窗口对应不同的显示层级:

  • 系统窗口层(System Window):最高层级,包含状态栏、导航栏、Toast、悬浮窗等。
  • 子窗口层(Sub Window):中间层级,包含 Dialog、PopupWindow 等(需依附于宿主窗口)。
  • 应用窗口层(Application Window):最底层级,对应普通 Activity 的窗口。

由于系统窗口层的 Z-order 高于应用窗口层,将窗口类型配置为系统级,即可实现覆盖于所有应用之上的"悬浮"效果。

2.2 WindowManager.addView() 的底层机制

调用 WindowManager.addView(view, params) 时,系统底层的实际调用链路如下:

应用层代码 → WindowManager (客户端代理) → Binder IPC → WindowManagerService (系统服务端) → SurfaceFlinger (合成渲染)

传入的 LayoutParams.type 参数决定了目标 View 所属的显示层级。当参数配置为:

params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY

WMS 会将该窗口分配至系统窗口层。该窗口独立于任何 Activity 的生命周期,直接由系统 WMS 管理,其 Z-order 默认高于所有普通 Activity,从而呈现悬浮状态。

2.3 Service 与 Activity 的生命周期差异

悬浮窗通常不依附于 Activity 实现,其核心原因如下:

  • Activity 的局限性:Activity 具有严格的系统生命周期。用户触发 Home 键或切换至其他应用时,Activity 将执行 onStop()/onDestroy(),其持有的 Context 和资源存在被系统回收的风险。悬浮窗需要在应用退至后台后持续存在,Activity 无法满足此场景需求。
  • Service 的适用性:Service 作为无界面的后台组件,生命周期独立于 Activity。维持 Service 的存活状态,即可确保其持有的 WindowManager 引用及附属 View 持续显示在屏幕上。

2.4 前台 Service 与进程保活

Android 系统的进程管理策略对后台组件有严格限制:

  • 普通 Service 运行于后台时,系统的内存管理机制(LMK)在资源紧张时会随时清理该进程。进程一旦消亡,悬浮窗将随之销毁。
  • 前台 Service(通过 startForeground() 启动)会在通知栏展示常驻通知,向系统声明该进程正在执行用户可感知的重要任务。系统因此会提升该进程的 oom_adj 优先级,极大降低其被后台清理机制终止的概率。因此,前台 Service 是保障悬浮窗持续稳定存在的前提。

三、方案设计与对比

针对"前台隐藏,后台显示"的功能需求,目前存在三种主流实现方案。其核心逻辑均依赖于应用生命周期监听机制,以判断应用的前后台状态。

方案一:ActivityLifecycleCallbacks + Binder 通信

利用 Application 的 ActivityLifecycleCallbacks 监听应用内所有 Activity 的生命周期,通过计数器计算前后台状态,并借助 Binder 跨组件控制 Service。

1. 悬浮窗 Service 实现

class FloatingViewService : Service() {
    private lateinit var windowManager: WindowManager
    private lateinit var floatingView: View
    private lateinit var params: WindowManager.LayoutParams
    private var isViewAdded = false

    override fun onCreate() {
        super.onCreate()
        // 获取系统级 WindowManager
        windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
        
        // 创建悬浮窗 View
        floatingView = LayoutInflater.from(this).inflate(R.layout.layout_floating, null)
        
        // 配置参数
        params = WindowManager.LayoutParams(
            WindowManager.LayoutParams.WRAP_CONTENT,
            WindowManager.LayoutParams.WRAP_CONTENT,
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
            PixelFormat.TRANSLUCENT
        ).apply {
            gravity = Gravity.TOP or Gravity.START
            x = 0
            y = 100
        }
        
        // 注册 View,初始状态设为隐藏
        windowManager.addView(floatingView, params)
        isViewAdded = true
        floatingView.visibility = View.GONE
    }

    // ===== 状态控制接口 =====
    fun showFloatingView() {
        if (isViewAdded) floatingView.visibility = View.VISIBLE
    }

    fun hideFloatingView() {
        if (isViewAdded) floatingView.visibility = View.GONE
    }

    // ===== Binder 通信实现 =====
    private val binder = FloatingBinder()
    inner class FloatingBinder : Binder() {
        fun getService(): FloatingViewService = this@FloatingViewService
    }
    
    override fun onBind(intent: Intent): IBinder = binder

    override fun onDestroy() {
        if (isViewAdded) windowManager.removeView(floatingView)
        super.onDestroy()
    }
}

2. Application 生命周期监听

class MyApplication : Application() {
    var floatingService: FloatingViewService? = null
    private var activityCount = 0

    private val serviceConnection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            val binder = service as FloatingViewService.FloatingBinder
            floatingService = binder.getService()
        }
        override fun onServiceDisconnected(name: ComponentName?) {
            floatingService = null
        }
    }

    override fun onCreate() {
        super.onCreate()
        
        // 启动并绑定悬浮窗 Service
        val intent = Intent(this, FloatingViewService::class.java)
        startForegroundService(intent) // 需配置前台服务通知
        bindService(intent, serviceConnection, BIND_AUTO_CREATE)

        // 注册全局 Activity 生命周期回调
        registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
            override fun onActivityStarted(activity: Activity) {
                activityCount++
                if (activityCount == 1) {
                    // 计数器由 0 变为 1:应用切换至前台
                    floatingService?.hideFloatingView()
                }
            }

            override fun onActivityStopped(activity: Activity) {
                activityCount--
                if (activityCount == 0) {
                    // 计数器归零:应用完全退至后台
                    floatingService?.showFloatingView()
                }
            }
            
            // 其他回调缺省处理
            override fun onActivityCreated(a: Activity, s: Bundle?) {}
            override fun onActivityResumed(a: Activity) {}
            override fun onActivityPaused(a: Activity) {}
            override fun onActivitySaveInstanceState(a: Activity, s: Bundle) {}
            override fun onActivityDestroyed(a: Activity) {}
        })
    }
}

逻辑依据:采用 onStart/onStop 进行状态计数的合理性

相较于 onResume/onPause,使用 onStart/onStop 进行计数可有效避免误判:

  • 页面跳转场景 (Activity A -> B):目标页面 B 先触发 onStart (activityCount=2),源页面 A 后触发 onStop (activityCount=1)。此过程计数器不会归零,可防止误触发后台逻辑。
  • 退至后台场景 (如触发 Home 键):当前页面直接触发 onStop (activityCount=0),系统正确识别为后台状态并显示悬浮窗。

方案二:ActivityLifecycleCallbacks + 全局状态总线 (LiveData/EventBus)

为降低 Application 与 Service 之间的耦合度,可采用响应式通信方式替代 Binder 机制。

1. 定义全局状态

object AppStateManager {
    val isAppForeground = MutableLiveData<Boolean>()
}

2. Application 状态分发

override fun onActivityStarted(activity: Activity) {
    activityCount++
    if (activityCount == 1) {
        AppStateManager.isAppForeground.postValue(true)
    }
}

override fun onActivityStopped(activity: Activity) {
    activityCount--
    if (activityCount == 0) {
        AppStateManager.isAppForeground.postValue(false)
    }
}

3. Service 状态观察

// 置于 Service 的 onCreate() 中
AppStateManager.isAppForeground.observeForever { isForeground ->
    if (isForeground) {
        hideFloatingView()
    } else {
        showFloatingView()
    }
}

方案三:ProcessLifecycleOwner(官方推荐方案)

利用 Google 官方提供的 Lifecycle 架构组件,直接监听应用进程级的前后台状态。

技术优势:无需手动维护计数逻辑或跨组件通信机制,代码精简,且对配置变更(Configuration Changes)、多窗口模式等边界场景具有最佳兼容性。

1. 引入依赖

implementation "androidx.lifecycle:lifecycle-process:2.6.2" // 建议使用最新稳定版

2. Service 状态监听实现

class FloatingViewService : Service() {
    // 隐藏 WindowManager 与 View 初始化逻辑 ...

    override fun onCreate() {
        super.onCreate()
        // 隐藏视图注册逻辑 ...

        // 注册进程级生命周期观察者
        ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onStart(owner: LifecycleOwner) {
                // 应用进入前台 -> 隐藏悬浮窗
                hideFloatingView()
            }

            override fun onStop(owner: LifecycleOwner) {
                // 应用退至后台 -> 显示悬浮窗
                showFloatingView()
            }
        })
    }
}

四、总结与建议

方案优势局限性适用场景
方案一 (Binder)无需依赖第三方组件,控制逻辑完全自主代码较为冗长,存在 Application 与 Service 强耦合限制引入外部依赖的遗留系统维护
方案二 (LiveData)组件间解耦良好,业务逻辑更清晰仍需人工维护生命周期计数,需关注内存泄漏风险项目架构已深度整合响应式编程范式
方案三 (Lifecycle)官方标准方案,代码极简,系统级边界处理最稳健需引入 androidx.lifecycle 外部依赖所有现代化 Android 工程首选

结论:在多数业务场景中,推荐优先采用 方案三(ProcessLifecycleOwner) 并配合前台 Service,以构建稳定且易于维护的悬浮窗交互架构。