【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),若系统版本变更导致类结构变化,可能需要适配(实际测试中主流版本兼容性良好)。
性能影响:
反射操作和遍历窗口列表会有轻微性能消耗,建议仅在必要时调用(如应用启动、服务重启)。