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

【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.dtcms.com/a/341979.html

相关文章:

  • 政务分建用户体系下基于OAuth2.0概念单点登录实现方案
  • CT02-20.有效的括号(Java)
  • 【Linux | 网络】多路转接IO之select
  • Linux软件编程--网络操作
  • FreeRTOS,事件标注组创建,xEventGroupCreate、xEventGroupCreateStatic
  • 【自记】Power BI 中 CALCULATETABLE 第二个及后续参数支持的两种类型说明
  • 基于混合注意力网络和深度信念网络的鲁棒视频水印技术基础理论深度解析
  • 【世纪龙科技】汽车专业虚拟仿真实训基地建设-理实虚一体化实践
  • 【lucene】lucene常用查询一览
  • 一键去除 Windows 图标角标(小箭头、盾牌与双箭头)
  • JeeSite V5.13.0 发布,升级 Spring Boot 3.5,Cloud 2025,AI 1.0,Vite 7
  • ORACLE中如何批量重置序列
  • 如何保证数据库和缓存的一致性?
  • 强化学习中的重要性采样:跨分布复用样本的核心技术
  • 大模型0基础开发入门与实践:第8章 “大力出奇迹”的哲学:大语言模型的核心技术揭秘
  • 【世纪龙科技】汽车专业虚拟仿真实训基地建设方案
  • 嵌入式软件典型架构:层次化模式 vs 递归模式
  • Java Main无法初始化主类的原因与解决方法(VsCode工具)
  • 【Java后端】Spring Boot 实现请求设备来源统计与UA解析全攻略
  • 智慧工厂的 “隐形大脑”:边缘计算网关凭什么重构设备连接新逻辑?
  • 编程刷题-资料分发1 图论/DFS
  • Kotlin-基础语法练习二
  • Android面试指南(四)
  • [新启航]机械深孔加工质控:新启航方案用激光频率梳破解 130mm 深度遮挡瓶颈
  • 闲聊汽车芯片的信息安全需求和功能
  • C# NX二次开发:反向控件和组控件详解
  • 智慧巡检新标杆:智能移动机器人——电力行业的守护者
  • 【数据结构】树与二叉树:结构、性质与存储
  • 解码欧洲宠物经济蓝海:跨境电商突围的战略逻辑与运营范式
  • Vue2+Vue3前端开发_Day5