当前位置: 首页 > ops >正文

【Android】悬浮窗清理

前文

在 Android 开发中,悬浮窗(Float Window)是一种常见的交互形式,广泛用于快捷操作、桌面工具等场景。但在实际开发中,我们经常会遇到悬浮窗重复显示、旧窗口残留的问题 —— 尤其是当应用进程意外崩溃或服务重启时,旧的悬浮窗可能无法被正常移除,导致多个窗口重叠显示,严重影响用户体验。

悬浮窗残留的常见场景

为什么会出现悬浮窗残留?主要有以下几种情况:

进程意外崩溃:

当悬浮窗服务所在进程崩溃时,系统可能无法触发onDestroy()方法,导致悬浮窗视图未被正常移除。

服务重启机制:

如果应用通过START_STICKY模式让服务自动重启,旧服务的悬浮窗可能未清理就启动了新服务,导致重复显示。

多入口启动:

应用可能通过多个入口(如 Activity、广播)启动悬浮窗服务,若未做互斥处理,会创建多个悬浮窗实例。

这些场景的共同问题是:旧悬浮窗脱离了应用的正常生命周期管理,常规的removeView()方法无法再对其生效。

解决方案:反射清理系统级悬浮窗

针对上述问题,我们可以通过反射技术直接访问 Android 系统的窗口管理机制,找到并移除残留的悬浮窗。核心思路是:

利用反射获取系统中所有窗口的视图列表;

根据预先设置的唯一标识筛选目标悬浮窗;

调用系统 API 强制移除这些残留窗口。

实现
1. 为悬浮窗设置唯一标识

首先,需要在创建悬浮窗时,为其WindowManager.LayoutParams设置唯一标识(确保全局唯一性):

    private void initFloatingView() {floatingView = LayoutInflater.from(this).inflate(R.layout.floating_window, null);ivFloating = floatingView.findViewById(R.id.iv_floating);// 初始化悬浮窗参数int windowType = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY: WindowManager.LayoutParams.TYPE_PHONE;floatingParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,WindowManager.LayoutParams.WRAP_CONTENT,windowType,WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,PixelFormat.TRANSLUCENT);floatingParams.gravity = Gravity.START | Gravity.TOP;// 设置唯一标识(主悬浮窗)String mainFloatTag = "float_main_float_window";floatingParams.setTitle(mainFloatTag);}

如果应用有多个悬浮窗(如主窗口 + 操作按钮窗口),需为每个窗口设置不同的标识(如main_float和buttons_float)。

2. 反射清理工具类实现

下面是完整的悬浮窗清理工具类,通过反射获取系统窗口列表并清理目标窗口:

import android.content.Context;
import android.view.View;
import android.view.WindowManager;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;public class FloatWindowUtils {private static final String TAG = "FloatWindowUtils";// 系统窗口管理相关的反射常量private static final String WINDOW_MANAGER_GLOBAL_CLASS = "android.view.WindowManagerGlobal";private static final String GET_INSTANCE_METHOD = "getInstance";private static final String M_VIEWS_FIELD = "mViews";/*** 清理指定标识的悬浮窗* @param context 上下文(建议使用Application Context)* @param targetTags 需要清理的悬浮窗标识数组*/public static void forceCloseOldFloatWindows(Context context, String[] targetTags) {// 1. 参数校验(快速失败)if (!validateParams(context, targetTags)) {return;}// 2. 获取WindowManager实例WindowManager windowManager = getWindowManager(context);if (windowManager == null) {logE("获取WindowManager失败,无法清理悬浮窗");return;}// 3. 反射获取系统所有窗口List<View> allWindows = getSystemWindowList();if (allWindows == null || allWindows.isEmpty()) {logE("未获取到系统窗口列表,无需清理");return;}// 4. 筛选需要移除的目标窗口List<View> windowsToRemove = filterTargetWindows(allWindows, targetTags);// 5. 移除目标窗口removeWindows(windowManager, windowsToRemove);}// 参数合法性校验private static boolean validateParams(Context context, String[] targetTags) {if (context == null) {logE("参数错误:context为null");return false;}if (targetTags == null || targetTags.length == 0) {logE("参数错误:targetTags为null或空数组");return false;}for (String tag : targetTags) {if (tag == null || tag.trim().isEmpty()) {logE("参数错误:targetTags包含null或空字符串");return false;}}return true;}// 获取WindowManager实例private static WindowManager getWindowManager(Context context) {try {return (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);} catch (Exception e) {logE("获取WindowManager异常:" + e.getMessage());return null;}}// 反射获取系统窗口列表@SuppressWarnings("unchecked")private static List<View> getSystemWindowList() {try {// 反射获取WindowManagerGlobal实例Class<?> wmGlobalClass = Class.forName(WINDOW_MANAGER_GLOBAL_CLASS);Method getInstanceMethod = wmGlobalClass.getDeclaredMethod(GET_INSTANCE_METHOD);getInstanceMethod.setAccessible(true);Object wmGlobal = getInstanceMethod.invoke(null);// 获取窗口列表字段Field mViewsField = wmGlobalClass.getDeclaredField(M_VIEWS_FIELD);mViewsField.setAccessible(true);return (List<View>) mViewsField.get(wmGlobal);} catch (Exception e) {logE("反射获取窗口列表失败:" + e.getMessage());return null;}}// 筛选目标窗口private static List<View> filterTargetWindows(List<View> allWindows, String[] targetTags) {List<View> result = new ArrayList<>();List<String> targetTagList = Arrays.asList(targetTags);for (View window : allWindows) {try {WindowManager.LayoutParams params = (WindowManager.LayoutParams) window.getLayoutParams();if (params == null) continue;String windowTag = (String) params.title;if (windowTag != null && targetTagList.contains(windowTag)) {result.add(window);logI("匹配到目标窗口:" + windowTag);}} catch (Exception e) {logE("处理窗口时异常:" + e.getMessage());}}return result;}// 移除窗口private static void removeWindows(WindowManager windowManager, List<View> windowsToRemove) {if (windowsToRemove.isEmpty()) {logI("没有需要移除的悬浮窗");return;}logI("开始移除悬浮窗,共" + windowsToRemove.size() + "个");for (View window : windowsToRemove) {try {if (window.getParent() instanceof WindowManager) {windowManager.removeViewImmediate(window);String tag = (String) ((WindowManager.LayoutParams) window.getLayoutParams()).title;logI("成功移除悬浮窗:" + tag);} else {logW("窗口已不在管理器中,无需移除");}} catch (Exception e) {logE("移除窗口失败:" + e.getMessage());}}}// 日志工具方法private static void logI(String msg) { android.util.Log.i(TAG, msg); }private static void logE(String msg) { android.util.Log.e(TAG, msg); }private static void logW(String msg) { android.util.Log.w(TAG, msg); }
}
3. 使用方式

在启动悬浮窗服务前调用清理方法,确保旧窗口被移除:(这里清除两个悬浮窗)

 FloatWindowUtils.forceCloseOldFloatWindows(this,new String[]{"float_main_float_window", "float_buttons_float_window"});
注意事项
悬浮窗权限:

确保应用已获取SYSTEM_ALERT_WINDOW权限(Android 6.0 + 需动态申请)。

反射稳定性:

反射依赖系统内部类(WindowManagerGlobal),若系统版本变更导致类结构变化,可能需要适配(实际测试中主流版本兼容性良好)。

性能影响:

反射操作和遍历窗口列表会有轻微性能消耗,建议仅在必要时调用(如应用启动、服务重启)。

http://www.xdnf.cn/news/18363.html

相关文章:

  • Pytorch基础学习--张量(生成,索引,变形)
  • 从系统漏洞归零到候诊缩短20%:一个信创样本的效能革命
  • 机器学习聚类与集成算法全解析:从 K-Means 到随机森林的实战指南
  • CRMEB私域电商系统后台开发实战:小程序配置全流程解析
  • 贪吃蛇游戏(纯HTML)
  • 什么是区块链?从比特币到Web3的演进
  • 图像中物体计数:基于YOLOv5的目标检测与分割技术
  • 十分钟速通堆叠
  • 智慧城市SaaS平台/市政设施运行监测系统之空气质量监测系统、VOC气体监测系统、污水水质监测系统及环卫车辆定位调度系统架构内容
  • 终结开发混乱,用 Amazon Q 打造AI助手
  • 华为云ModelArts+Dify AI:双剑合璧使能AI应用敏捷开发
  • CSS【详解】性能优化
  • 【知识储备】PyTorch / TensorFlow 和张量的联系
  • 数字货币发展存在的问题:交易平台的问题不断,但监管日益加强
  • React + Antd+TS 动态表单容器组件技术解析与实现
  • Linux -- 封装一个线程池
  • 射频电路的完整性简略
  • ubuntu编译ijkplayer版本k0.8.8(ffmpeg4.0)
  • JVM-(7)堆内存逻辑分区
  • 智能编程中的智能体与 AI 应用:概念、架构与实践场景
  • 【Flutter】Container设置对齐方式会填满父组件剩余空间
  • BaaS(Backend as a Service)技术深度解析:云时代的后端开发革命
  • 数据结构青铜到王者第一话---数据结构基本常识(1)
  • Spring面试宝典:Spring IOC的执行流程解析
  • JavaScript 十六进制与字符串互相转(HEX)
  • 通义千问VL-Plus:当AI“看懂”屏幕,软件测试的OCR时代正式终结!
  • 微信小程序基础Day1
  • iOS 文件管理全景实战 多工具协同提升开发与调试效率
  • ACM模式输入输出
  • mlir CollapseShapeOp ExpandShapeOp的构造