ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

VirtualApp 悬浮窗权限适配实战:从 Android 6 到 14 不闪退的 7 个关键点

VirtualApp 悬浮窗权限适配实战:从 Android 6 到 14 不闪退的 7 个关键点 VirtualApp 悬浮窗权限适配实战从 Android 6 到 14 不闪退的 7 个关键点【免费下载链接】VirtualAppVirtual Engine for Android(Support 14.0 in business version)项目地址: https://gitcode.com/GitHub_Trending/vi/VirtualAppVirtualApp 是一款开源的 Android 沙盒多开框架通过 Hook 系统服务让应用分身并行运行当你在它之上叠加悬浮窗功能时最难缠的不是业务逻辑而是 Android 6 到 Android 14 之间不断收紧的悬浮窗权限。这篇文章用一次真实的上线事故做引子把 SYSTEM_ALERT_WINDOW 的申请、窗口类型选择、后台限制和多进程同步一次讲透。一、一次真实事故为什么换了 Android 10 手机悬浮球集体消失某多开应用上线三个月测试机是 Android 8悬浮球一直好好的。上线后用户反馈却陆续炸开Android 10 的机型上悬浮球偶尔出现、经常消失Android 11 的机型上更直接点击开启悬浮窗的开关应用当场闪退。最诡异的是同一份 APK 在 Android 6/7 的老设备上却一切正常。三台手机三种表现背后其实是三个不同层面的问题权限判断、窗口类型和后台限制。悬浮窗权限看似只有一个SYSTEM_ALERT_WINDOW但它从 Android 6 到 Android 14 被拆成了检测、申请、显示、保活、跨进程五条线任何一条线没适配用户看到的就是权限开了也没用。二、悬浮窗权限到底是什么先看懂它的版本演进地图悬浮窗权限在 Android 里的定位很特殊它不属于运行时权限不会弹系统授权框而是特殊权限Special Access由用户去系统设置里手动开启底层通过 AppOps 机制记录开关状态。这意味着requestPermissions对它是无效的检测和申请都必须走独立的 API这是新手最容易踩的第一个坑。从 Android 6 开始这套机制经历了六次明显调整Android 版本关键变化开发者必须做的事6.0API 23悬浮窗被划入特殊权限用Settings.canDrawOverlays()检测8.0API 26新增TYPE_APPLICATION_OVERLAY窗口类型不再使用TYPE_PHONE加窗10API 29收紧后台弹窗与悬浮窗退后台时主动隐藏悬浮窗12API 31前台服务必须声明类型声明foregroundServiceType13API 33通知权限运行时化申请POST_NOTIFICATIONS14API 34前台服务类型强制校验使用specialUse并登记用途在 VirtualApp 这样的沙盒架构里事情还要更复杂一层。如下图所示VirtualApp 分为 VA Space虚拟应用运行区、VA FrameworkHook 与代理层、VA Native底层 IO/虚拟机 Hook三层沙盒内应用的悬浮窗请求并不是直接打到系统而是被 VA Framework 拦截后代理转发因此权限状态既要被宿主识别也要能在虚拟容器里查到三、权限检查与申请的正确姿势封装成单例别再裸写 Settings把检测和申请逻辑散落在各个 Activity 里是第二个高频坑。原因很直白申请悬浮窗权限是跳去系统设置页再跳回来的异步过程回来后的结果要处理、用户可能反悔、多个页面都可能发起申请裸写几行startActivityForResult根本扛不住。所以正确的做法是抽一个全局唯一的OverlayAccessor统一负责三件事检测、发起申请、分发授权结果。代码里通过回调把授权完成这件事交给调用方页面只关心自己关心的结果public final class OverlayAccessor { public interface GrantListener { void onGranted(); void onDenied(); } private static volatile OverlayAccessor sInstance; private final SetGrantListener mListeners new CopyOnWriteArraySet(); private OverlayAccessor() {} public static OverlayAccessor get() { if (sInstance null) { synchronized (OverlayAccessor.class) { if (sInstance null) { sInstance new OverlayAccessor(); } } } return sInstance; } /** Android 6 以下默认为已授权直接返回 true */ public boolean hasOverlayAccess(Context context) { return Build.VERSION.SDK_INT Build.VERSION_CODES.M || Settings.canDrawOverlays(context); } /** 发起申请若已授权则直接走 onGranted不重复跳设置页 */ public void request(Activity activity, int requestCode) { if (hasOverlayAccess(activity)) { notifyGranted(); return; } Intent goSettings new Intent( Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse(package: activity.getPackageName())); activity.startActivityForResult(goSettings, requestCode); } /** 在 Activity#onActivityResult 里统一回调 */ public void dispatchResult(Activity activity, boolean fromRequest) { if (!fromRequest) { return; } if (hasOverlayAccess(activity)) { notifyGranted(); } else { notifyDenied(); } } public void register(GrantListener listener) { mListeners.add(listener); } public void unregister(GrantListener listener) { mListeners.remove(listener); } private void notifyGranted() { for (GrantListener l : mListeners) { l.onGranted(); } } private void notifyDenied() { for (GrantListener l : mListeners) { l.onDenied(); } } }页面侧只做两件事注册监听、在onActivityResult里调用dispatchResult。这样即使将来要改成协程或者ActivityResultLauncher方案改动也只局限在这一个类里。四、窗口类型选错直接闪退最稳的 TYPE 选择方案授权成功之后崩溃才真正开始。Android 8 之前大家习惯用TYPE_PHONE挂悬浮窗但 Android 8 起系统引入了TYPE_APPLICATION_OVERLAY来统一管理应用悬浮窗继续用TYPE_PHONE会直接抛BadTokenException。Android 11 上那个点开关就闪退的反馈日志大概率长这样崩溃日志片段原因解法BadTokenException: Unable to add window -- permission denied悬浮窗权限实际未授予先跑hasOverlayAccess再加窗BadTokenException: token null is not valid用TYPE_PHONE在 Android 8 加窗换TYPE_APPLICATION_OVERLAYWindowManager.BadTokenException: unable to add window窗口类型与权限、进程不匹配统一走pickWindowType()所以窗口类型不能写死要按版本来低于 8.0 用TYPE_PHONE8.0 及以上用TYPE_APPLICATION_OVERLAY。同时把FLAG_NOT_FOCUSABLE加上避免悬浮窗抢走输入焦点、挡住用户操作private WindowManager.LayoutParams buildFloatParams() { int windowType Build.VERSION.SDK_INT Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_PHONE; return new WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, windowType, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); }这里还有一个容易忽略的细节加窗前必须再次校验权限。因为用户可能从系统设置里手动关掉悬浮窗开关应用自己却不知道此时addView照样抛异常。稳妥的顺序是检测 → 加窗 → 捕获BadTokenException兜底回收。五、后台就不许显示用状态机管住悬浮窗生命周期Android 10 之后应用退到后台悬浮窗还在飘成为被系统重点整治的行为。系统虽然不直接杀悬浮窗但配合后台启动限制会把你从后台拉起的相关组件直接拦下表现就是悬浮球时有时无。那个 Android 10 用户反馈的经常消失正是这个原因。与其在onPause、onStop、onResume里写一堆散乱的 if-else不如给悬浮窗定义一个精简的状态机IDLE → SHOWING → HIDDEN → SHOWING用事件驱动切换。下面这个FloatWindowState只保留最关键的三态迁移后续加拖动中缩放中等状态时也好扩展public enum FloatWindowState { IDLE, // 尚未创建 SHOWING, // 可见 HIDDEN; // 因后台等原因隐藏 public FloatWindowState toForeground() { return SHOWING; } public FloatWindowState toBackground() { return (this SHOWING) ? HIDDEN : this; } }控制器的核心逻辑是前后台切换时对称收放。判断前后台不要用RunningAppProcessInfo去轮询性能差、还可能被系统返回空列表改用ProcessLifecycleOwner监听应用生命周期再通过onStart/onStop驱动状态机public final class FloatWindowController { private final WindowManager mWm; private View mFloatView; private WindowManager.LayoutParams mParams; private FloatWindowState mState FloatWindowState.IDLE; public FloatWindowController(Context context) { mWm (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); } /** 应用回到前台恢复显示 */ public void onAppForeground() { if (mFloatView null) { return; } if (mState FloatWindowState.HIDDEN) { mWm.addView(mFloatView, mParams); mState mState.toForeground(); } } /** 应用退到后台立即隐藏避免触发系统后台限制 */ public void onAppBackground() { if (mState FloatWindowState.SHOWING) { mWm.removeView(mFloatView); mState mState.toBackground(); } } public void showFloatView(View view, WindowManager.LayoutParams params) { if (mState ! FloatWindowState.IDLE) { return; } mFloatView view; mParams params; mWm.addView(view, params); mState FloatWindowState.SHOWING; } }这套退后台必隐藏、回前台再挂载的策略是应对 Android 10 之后系统限制最省心的方案——宁可少显示也不能让系统判定你违规。六、Android 11 到 14 的新规矩前台服务、通知权限一个都不能少Android 11 以后悬浮窗保活的玩法也被堵死了想在后台维持悬浮窗就必须挂在前台服务上而前台服务从 Android 12 起强制要求声明类型。到了 Android 14specialUse类型不仅要在 Manifest 里声明Play 审核时还要给出正当用途说明。这里给出一个完整的悬浮窗保活组合方案。第一步在 Manifest 里把服务和权限一次性声明齐uses-permission android:nameandroid.permission.SYSTEM_ALERT_WINDOW / uses-permission android:nameandroid.permission.POST_NOTIFICATIONS / service android:name.floatwin.FloatWindowService android:exportedfalse android:foregroundServiceTypespecialUse property android:nameandroid.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE android:valueshow persistent floating ball for multi-window control / /service第二步在服务里把前台通知和悬浮窗一起拉起。注意 Android 13 以上如果用户拒绝了通知权限startForeground会静默降级为不显示通知的普通前台服务功能不受影响但你要做好兼容不能因为没通知权限就崩溃public class FloatWindowService extends Service { private static final int NOTIFY_ID 0x11; Override public int onStartCommand(Intent intent, int flags, int startId) { startForeground(NOTIFY_ID, buildNotification()); return START_STICKY; } private Notification buildNotification() { NotificationChannel channel new NotificationChannel( float, 悬浮窗, NotificationManager.IMPORTANCE_MIN); NotificationManager nm getSystemService(NotificationManager.class); nm.createNotificationChannel(channel); return new Notification.Builder(this, float) .setContentTitle(悬浮球运行中) .setContentText(点击返回主界面) .setSmallIcon(R.drawable.ic_float_ball) .build(); } }第三件事别漏Android 13 上POST_NOTIFICATIONS是运行时权限第一次启动时要用常规的requestPermissions弹窗申请用户拒绝后服务仍能运行只是通知不展示所以悬浮窗功能本身不要和通知权限强绑定。七、VirtualApp 多进程下的权限同步宿主、核心进程与容器各管一段普通应用处理好上面五步就够用了但 VirtualApp 场景多了一道工序——多进程。VirtualApp 由宿主进程、核心管理进程VA Server和多个虚拟容器进程组成沙盒内应用的悬浮窗请求发生在容器进程权限状态却要由核心进程统一维护否则就会出现容器里认为有权限、系统实际没授予或反之的错乱正确的思路是让 VA Server 成为权限状态的唯一数据源容器进程通过 Binder 查询而不是各自去读系统设置。核心进程里维护一份内存缓存再叠加系统检测做兜底public class VAOverlayStore { private static final VAOverlayStore INSTANCE new VAOverlayStore(); private final MapString, Boolean mCache new ConcurrentHashMap(); public static VAOverlayStore get() { return INSTANCE; } /** 容器进程调用优先读缓存缓存未命中再回退到系统检测 */ public boolean queryOverlayAccess(String packageName, Context context) { Boolean cached mCache.get(packageName); if (cached ! null) { return cached; } boolean real Settings.canDrawOverlays(context); mCache.put(packageName, real); return real; } /** 宿主在用户授权/关闭后主动刷新保持各进程一致 */ public void refresh(String packageName, boolean granted) { mCache.put(packageName, granted); } }这套方案的额外收益是性能跨进程查 AppOps 是昂贵的 Binder 往返缓存命中后可以避免每次显示悬浮窗都走一次系统查询。上线前建议按下面这张清单过一遍检查项关注点Android 6/7 老设备检测 申请流程是否正常走设置页Android 8-9窗口类型是否全部替换为TYPE_APPLICATION_OVERLAYAndroid 10退后台后悬浮窗是否立即隐藏Android 12前台服务类型是否声明、是否与用途匹配Android 13通知权限拒绝后服务是否还能跑Android 14specialUse类型与用途说明是否齐备多进程环境容器进程查询的权限状态是否与宿主一致回到开头的三台手机Android 10 的悬浮球消失是后台限制没处理Android 11 的点开关闪退是窗口类型与权限校验缺失老设备正常则是版本分支兜底得当。把检测、申请、类型、后台、保活、多进程这六条线按本文的方案各就各位Android 6 到 14 的悬浮窗权限适配就能一次到位。如果你想对照源码研究 VirtualApp 的权限代理实现可以执行git clone https://gitcode.com/GitHub_Trending/vi/VirtualApp获取完整工程重点看 lib 模块里对系统服务与权限相关类的 Hook 逻辑。SEO 关键词核心关键词VirtualApp 悬浮窗权限长尾关键词Android 悬浮窗权限适配、SYSTEM_ALERT_WINDOW 申请流程、TYPE_APPLICATION_OVERLAY 崩溃解决、Android 10 悬浮窗后台显示限制、VirtualApp 多开悬浮窗保活、Android 14 前台服务 specialUse【免费下载链接】VirtualAppVirtual Engine for Android(Support 14.0 in business version)项目地址: https://gitcode.com/GitHub_Trending/vi/VirtualApp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表