资讯详情

资讯详情

建站行业动态 · 设计趋势 · 数字化升级干货

从一次诡异崩溃说起:VirtualApp 悬浮窗权限的十年适配之路

从一次诡异崩溃说起:VirtualApp 悬浮窗权限的十年适配之路 从一次诡异崩溃说起VirtualApp 悬浮窗权限的十年适配之路【免费下载链接】VirtualAppVirtual Engine for Android(Support 14.0 in business version)项目地址: https://gitcode.com/GitHub_Trending/vi/VirtualApp导语你在真机上测得好好的悬浮窗功能上线后却收到一屏无法添加窗口的崩溃日志同一个包名Android 9 上正常Android 12 上却毫无反应。作为一款主打应用多开与沙盒运行的引擎VirtualApp 几乎把每个 Android 版本的权限规则都踩了一遍。这篇文章不堆概念只讲坑——从崩溃现场出发带你逐层拆解悬浮窗权限在 Android 6 到 14 之间的真实行为差异并给出可落地的检查与加固方案。一次诡异的线上崩溃逼我重读权限源码崩溃日志里的关键词先还原事故现场。某个双开应用上线后华为与小米的反馈群里先后出现同一类崩溃BadTokenException: Unable to add window -- permission denied at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:...)报错的位置是WindowManager.addView而日志里permission denied这个词几乎与你还没拿到 SYSTEM_ALERT_WINDOW悬浮窗权限画等号。诡异的是部分用户明明在系统设置里看到了允许显示在其他应用上层的开关处于打开状态。权限开关打开了系统却不认账随后我们发现一个更反直觉的现象同一台设备上宿主应用能正常弹窗沙盒里被多开的应用却依然崩溃。这说明问题不在用户有没有授权而在系统认为谁在申请这个权限。 这里引出本篇文章的第一个关键结论悬浮窗权限的判定从来不是看 AndroidManifest 里有没有声明而是看系统在运行期向谁查询、查询到了什么结果。顺着这条线我们最终定位到两处需要关注的代码一是权限检查的入口Settings.canDrawOverlays()二是它背后的 AppOps应用操作服务。弄清楚这两者的关系后面所有版本适配才有了根基。本章收获崩溃日志中的 permission denied 只是表象真正的排查起点是系统向谁查询了悬浮窗权限。悬浮窗权限的两层皮声明、检查与授予第一层Manifest 声明只是入场券在VirtualApp的 lib 模块清单文件里你可以找到这样一行!-- lib/src/main/AndroidManifest.xml -- uses-permission android:nameandroid.permission.SYSTEM_ALERT_WINDOW /注意这只是申请系统并不会因为它就给你授权。Android 6.0 之后SYSTEM_ALERT_WINDOW被归入特殊权限必须由用户从系统设置页手动打开普通运行时权限弹窗对它无效。第二层运行时判定走的是 AppOps真正决定能不能弹的是系统内的 AppOps 服务。用大白话说AppOps 就是系统给每个应用记的一本操作台账SYSTEM_ALERT_WINDOW是台账上的一个条目条目值是MODE_ALLOWED允许还是MODE_ERRORED拒绝由用户在设置页里的开关控制。我们封装一个统一的检查工具让 6.0 以下老设备也能走通public final class OverlayGuard { /** 判断当前进程是否有权创建悬浮窗 */ public static boolean granted(Context context) { // 6.0 起官方提供了标准查询入口 if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { return Settings.canDrawOverlays(context); } // 更老的系统没有 canDrawOverlays退回去读 AppOps 台账 return legacyAppOpsAllowed(context); } private static boolean legacyAppOpsAllowed(Context context) { Object appOps context.getSystemService(appops); try { Method checkOp appOps.getClass().getMethod( checkOp, int.class, int.class, String.class); // OP_SYSTEM_ALERT_WINDOW 24 int result (int) checkOp.invoke(appOps, 24, android.os.Process.myUid(), context.getPackageName()); return result 0; // MODE_ALLOWED } catch (Exception ignored) { return true; // 极端情况下保守放行避免误伤老机型 } } }这段代码解决的是入口统一问题无论什么系统版本业务层只调OverlayGuard.granted()一个方法版本差异全部收敛在工具类内部。授权跳转的正确姿势当检查不通过时需要引导用户去设置页。这里有一个高频失误点很多人把ACTION_MANAGE_OVERLAY_PERMISSION当成普通 intent 直接startActivity却忘了带上包名参数结果跳到的是所有应用列表用户根本找不到自己的应用。public static void openSettings(Activity activity, int requestCode) { Intent intent new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); // 必须拼接包名否则系统不知道你想给谁授权 intent.setData(Uri.parse(package: activity.getPackageName())); activity.startActivityForResult(intent, requestCode); }本章收获Manifest 声明只是入场券运行期真正生效的是 AppOps 台账检查与跳转都要封装成统一工具避免散落各处。用一张表吃透 Android 6 到 14 的权限变迁很多文章按版本逐个讲信息是齐了但脑子是乱的。我们反过来先把结论浓缩成一张对照表再挑三个最容易被忽视的变化点展开。系统版本关键变化对开发者的直接冲击处理方式6.0 (M)引入运行时权限悬浮窗升级为特殊权限光声明不行了必须引导用户去设置页Settings.canDrawOverlays() 授权跳转8.0 (O)新增TYPE_APPLICATION_OVERLAY窗口类型用TYPE_PHONE弹窗会抛异常高版本切换新窗口类型10.0 (Q)后台应用弹窗受限退到后台瞬间弹窗可能被系统拦截结合生命周期控制弹窗时机11.0 (R)权限授予规则收紧一次授予长期生效部分设备上拒绝变得更难反悔检查结果要兜底不能只相信开关12.0 (S)前台服务类型化长驻弹窗需配前台服务后台保持悬浮窗的场景必须挂前台服务服务启动时声明foregroundServiceType14.0 (U)悬浮窗显示时机进一步收紧冷启动即弹窗的场景要重新设计等首个 Activity 进入前台后再 addView最容易被忽略的三个变化点其一窗口类型的分水岭在 8.0。TYPE_APPLICATION_OVERLAY是 Android 8.0 起专供悬浮窗的窗口类型替代了此前的TYPE_PHONE。声明参数时按版本分流int type Build.VERSION.SDK_INT Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_PHONE;其二10.0 之后后台弹窗被系统盯着。如果你的悬浮窗服务是常驻的务必监听应用进入后台的时机主动收起而不是等系统来拦截你——被动拦截的结果往往是直接崩溃。其三12.0 的前台服务类型声明。长驻悬浮窗通常要挂在服务上Android 12 要求服务在启动时显式声明类型并配套通知否则系统直接拒绝启动。这属于编译期看不出、运行期必炸的坑上线前一定要拿 Android 12 以上的真机验证。本章收获版本适配的本质是识别变化点 按版本分流一张对照表比十篇长文更管用8.0、10.0、12.0 是三道必须迈过的坎。沙盒场景的独有考题权限该落在谁的头上多进程让权限归属变得模糊普通应用只需要关心自己的权限。但 VirtualApp 是多进程沙盒宿主主进程、引擎服务进程、以及被多开的虚拟应用进程并存。每个进程向系统查询权限时拿到的 uid 和包名可能各不相同——这就回到了开篇那个开关明明打开了却依然崩溃的谜题。引擎如何欺骗系统服务VirtualApp 在 lib 模块中通过 Binder 代理层拦截系统服务调用。以 AppOps 服务为例AppOpsManagerStub会在转发前把参数里的包名与 uid 替换成宿主自己的确保系统始终把虚拟应用当作宿主本人来放行。核心思路是身份归一// 在 hook 到 AppOpsService 的调用后 // 1. 把 args 里的虚拟包名替换为宿主包名 // 2. 把 uid 替换为宿主真实 uid // 3. 再放行给真正的系统服务处理 beforeCall(who, method, args) { replacePackage(args); // 虚拟包名 - 宿主包名 replaceUid(args); // 虚拟 uid - 宿主 uid return true; // 继续走系统原逻辑 }简而言之系统只认宿主这一个人的身份沙盒里所有虚拟应用的权限查询都被折算成宿主的查询。这样宿主一旦获得悬浮窗权限所有虚拟应用天然共享。共享权限后还要解决时机问题身份归一解决了有没有权限但没解决什么时候能弹。虚拟应用在onCreate阶段就弹窗此时窗口管理器可能尚未就绪依旧会失败。稳妥的做法是延迟到首个可见窗口之后// 虚拟应用的 Activity 进入 onResume 后再 addView if (OverlayGuard.granted(context)) { windowManager.addView(floatView, buildParams()); } else { // 记录待办等待宿主授权回调后补弹 pendingViews.add(floatView); }⚠️ 不要小看这个时机在沙盒场景下权限与时机是两个独立的崩溃源前者报 permission denied后者报 BadTokenException日志几乎一样。本章收获沙盒引擎通过身份归一让虚拟应用共享宿主权限但弹窗时机仍需按窗口生命周期精心编排。上线前的兼容加固清单与回归验证适配方案再完整最终都要落在验收上。这里给出一份可直接抄的检查清单。五项必查清单声明完整性宿主与引擎进程所在模块的 Manifest 都声明了SYSTEM_ALERT_WINDOW缺一不可。检查兜底授权回调回来后不要直接信任RESULT_OK要再次调用granted()复核——部分 ROM 会在返回前才落盘授权状态。窗口类型分流搜索代码中所有addView调用点确认窗口类型按 8.0 做了分支。生命周期联动onPause时如果判定应用已退后台主动移除悬浮窗视图。多进程回归分别在宿主进程与虚拟应用进程里各弹一次窗确认都正常。用真机矩阵代替主观自信版本碎片化的残酷在于任何我以为没问题的结论都可能在某个 ROM 上翻车。建议至少覆盖以下组合再发版Android 6.0 / 7.x 模拟器验证 AppOps 兜底路径Android 8.0 / 9.0 真机验证TYPE_APPLICATION_OVERLAY切换Android 12 / 13 / 14 真机验证前台服务类型与弹窗时机回归时重点关注两条路径全新安装后首次授权、以及授权后重启设备的二次验证。本章收获兼容加固的本质是把版本判断、身份归一、弹窗时机三个环节分别验收并用真机矩阵覆盖关键版本。摘要悬浮窗权限从 Android 6.0 的特殊权限一路收紧到 Android 14 的时机管控VirtualApp 的适配实践告诉我们声明、检查、授权、时机四件事缺一不可而沙盒多进程场景还需额外处理权限归属问题。建议以 OverlayGuard 工具类统一入口用版本对照表指导分流并坚持用真机矩阵验收才能在系统碎片化面前稳住悬浮窗功能的稳定性。【免费下载链接】VirtualAppVirtual Engine for Android(Support 14.0 in business version)项目地址: https://gitcode.com/GitHub_Trending/vi/VirtualApp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关资讯