Android应用锁核心原理与实战:无障碍服务+悬浮窗实现
你有没有琢磨过手机里的应用锁App到底是怎么做到“你一点微信它就弹密码”的我最早接这个需求的时候第一反应是去监听Activity生命周期结果跑起来才发现ActivityLifecycleCallbacks只能监听自己App内部的生命周期别人家App开了什么页面你根本收不到任何通知。后来又试过用UsageStatsManager轮询前台应用延迟能大到按完图标之后等个两三秒才弹锁那种体验放自己手机上都不想用。折腾了小半天之后我把方案锁定在了“无障碍服务 全局悬浮窗”这套组合上这也是目前国内应用锁产品里最主流的实现路径。这篇文章我会把整个项目的设计思路、核心原理、关键代码和我在线上踩过的坑全部写出来。不管你是刚学安卓开发的学生还是想给公司App做安全模块的工程师这篇文章都能帮你少走很多弯路。完整代码由几段核心类拼起来就能跑我会把每段代码为什么要这么写、参数怎么调都讲清楚你拿过去改成自己的应用锁完全没有问题。1. 应用锁的整体设计与方案选型1.1 三种主流实现路径对比先说结论安卓应用锁本质上的问题是“我如何知道另一个App已经在前台了”以及“如何在这个App上面强行盖一层密码验证界面”。围绕这两个问题行业里大致有三种做法。第一种是无障碍服务 悬浮窗靠AccessibilityService监听窗口状态变化检测到目标应用切到前台后就往WindowManager上塞一个全屏的密码输入View。它的优点是实时性好、兼容性覆盖面非常广不需要root代码都在应用层就能完成。缺点是用户需要手动开启无障碍权限而且无障碍服务属于系统敏感权限应用市场上架时审核比较严格。第二种是UsageStatsManager轮询也就是定时去查“当前前台应用是什么包名”。它拿到权限的方式也很折磨人需要用户跳到系统设置里单独授予“使用情况访问权限”而且queryEvents的结果不是实时的延迟从几百毫秒到几秒钟都有可能。用这种方案做防盗锁用户体验基本是灾难级别的我只能说它比较适合做统计类需求不适合做锁屏。第三种是插件化或Hook系统底层比如通过反射替换ActivityManagerNative的单例来拦截startActivity调用或者直接上Xposed框架。这种方案确实能拿到真正意义上的“拦截Activity启动”能力但代价是要root权限而普通用户根本不可能为了一个应用锁去root手机系统每次升级都有适配风险而且反射系统类在Android 10之后被限制得很厉害容易直接崩溃。我不推荐普通应用采用这种路线。1.2 “拦截Activity启动”在应用层到底是什么这里必须把概念说清楚标题里写的“拦截Activity启动”在无障碍服务方案里并不是真正在系统层面把Activity给拦下来而是“感知到前台窗口变化 → 在目标Activity上面覆盖一个全屏锁屏浮层 → 密码验证通过后移除浮层”。用户看到的效果和“拦截”是一样的但从系统角度来看目标Activity已经正常启动了只是内容被浮层挡住看不到而已。为什么要用覆盖而不是真实拦截一个是成本问题真实拦截要碰系统API风险大、维护成本高另一个是高版本安卓对后台启动Activity限制得特别严格Android 10之后应用进入后台就基本不能直接拉起Activity了而浮层View不依赖Activity用WindowManager.addView可以无视这个限制。这就是为什么现在市面上绝大多数的应用锁都采用“检测 覆盖”的方案而不是真正去拦截启动流程。1.3 项目模块划分我把整个项目拆成了四个模块各自职责单一后面对接和排查问题都很清晰无障碍服务模块负责监听全局窗口变化事件判断当前前台应用是否需要锁定并在必要时触发锁屏。锁屏管理器模块负责创建、显示、销毁全屏的锁屏悬浮窗同时管理锁屏状态防止重复弹出。密码校验模块负责密码的存储、验证和修改通常配合SharedPreferences或者加密数据库使用。应用管理模块负责维护“哪些应用需要锁定”的包名集合提供增删改查和内存缓存。模块之间通过一个单例的AppLockManager来通信服务只负责检测锁屏管理器只负责展示密码规则单独封装这样写出来的工程不会变成一坨几百行的面条代码。后面所有代码片段都会围绕这四个模块展开。2. 核心原理拆解无障碍服务如何感知Activity启动2.1 TYPE_WINDOW_STATE_CHANGED 事件到底在什么时候触发无障碍服务监听窗口变化核心依赖是AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED这个事件类型。官方文档的说法是“窗口状态发生变化”很多新手会以为它只在Activity切换时触发实际上它触发的场景比你想象的多得多Activity的onCreate / onResume过程中窗口内容变化Dialog弹出和关闭PopupWindow显示输入法窗口弹出和收起甚至部分系统级浮层出现。也就是说这个事件相比“Activity启动”是一个更宽泛的概念。好处是包名加类名一起拿非常详细坏处是误报多你要在代码里做大量的过滤条件。事件回调里能拿到两个关键字段String packageName event.getPackageName().toString(); String className event.getClassName().toString();packageName对应的是当前正在创建窗口的应用包名className对应的是具体窗口对应的类名Activity场景下一般就是Activity的完整类名。对于应用锁来说我们初步判断用packageName就够了但如果有些应用内部有多个界面你希望只锁其中某些特定页面那就要连className一起判断。2.2 包名与类名的双重校验检测逻辑看起来很简单如果packageName在锁定列表里就弹锁屏。但直接这么写一定会翻车因为系统里有一堆应用是不能锁的。举个最典型的例子你锁了微信用户打开微信要输密码输完密码之后用户切到桌面这时如果桌面被锁了那就永远回不去了。桌面也就是Launcher在不同厂商手机上包名还不一样小米是com.miui.home华为是com.huawei.android.launcher三星是com.sec.android.app.launcher原生安卓是com.google.android.apps.nexuslauncher之类。除了桌面输入法也不能锁不然输入密码的时候每次弹输入法都会触发窗口事件容易造成振荡。所以代码里要维护两个集合// 不参与加锁的系统应用和桌面应用 private static final SetString SYSTEM_WHITE_PKG new HashSet(Arrays.asList( com.android.systemui, com.android.settings, com.android.inputmethod.latin // 根据机型补充桌面和输入法包名 )); // 不参与加锁的Activity类名后缀比如输入法的候选窗口 private static final SetString SYSTEM_WHITE_CLS new HashSet(Arrays.asList( android.inputmethodservice.SoftInputWindow ));然后在事件回调里做过滤if (SYSTEM_WHITE_PKG.contains(packageName) || SYSTEM_WHITE_CLS.contains(className) || packageName.equals(context.getPackageName())) { return; }注意一定要把context.getPackageName()也就是应用锁自己的包名排除掉。如果不排除当你打开应用锁的设置页面时无障碍事件会触发然后把自己给锁了又因为锁定集合里包含自己就会在自己的设置页面盖上密码框直接死循环。这个坑我帮你们踩过写出来是不希望大家再踩一次。2.3 加上状态机避免锁屏反复弹出无障碍事件是流式的同一窗口切换可能短时间内触发多次。如果每次事件都弹锁屏你会发现锁屏窗口刚addView上去又一个事件过来了又addView一次直接崩溃。这里需要一个非常轻量的状态机。我在AppLockManager里维护两个核心字段public static final long DUPLICATE_INTERVAL 500L; private String lastLockedPkg ; private long lastLockTime 0L; private boolean isLockShowing false;当要触发锁屏时先判断如果当前已经处于锁屏显示状态直接return如果同一个包名在500毫秒内已经弹过一次锁也直接return。这两个判断能挡掉90%以上的重复事件。等到用户密码验证通过后再把isLockShowing置回false。整个状态机其实就三态UNLOCKED未锁定正常使用LOCKED锁屏浮层正在显示忽略所有新事件VERIFY_PASS密码验证通过浮层移除回到UNLOCKED。用代码写出来就是public boolean shouldLock(String pkg) { if (isLockShowing) { return false; } if (pkg.equals(lastLockedPkg) System.currentTimeMillis() - lastLockTime DUPLICATE_INTERVAL) { return false; } return needLock(pkg); }这个状态机是整个项目最简单但最关键的部分它直接决定了锁屏弹得稳不稳、会不会崩溃。3. 完整实战代码从无障碍服务到锁屏界面3.1 项目准备与权限配置新建一个安卓工程包名可以自己定我这里以com.example.applock为例。首先在AndroidManifest.xml里声明权限和服务uses-permission android:nameandroid.permission.SYSTEM_ALERT_WINDOW / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / application service android:name.service.LockAccessibilityService android:exportedtrue android:permissionandroid.permission.BIND_ACCESSIBILITY_SERVICE intent-filter action android:nameandroid.accessibilityservice.AccessibilityService / /intent-filter meta-data android:nameandroid.accessibilityservice android:resourcexml/accessibility_service_config / /service /applicationSYSTEM_ALERT_WINDOW是悬浮窗权限如果不开这个权限WindowManager.addView会直接抛BadTokenException方式失败。FOREGROUND_SERVICE是前台服务权限建议把无障碍监测放到一个前台服务里保活不然进程很容易在后台被回收锁定功能就失效了。然后在res/xml目录新建accessibility_service_config.xml?xml version1.0 encodingutf-8? accessibility-service xmlns:androidhttp://schemas.android.com/apk/res/android android:accessibilityEventTypestypeWindowStateChanged android:accessibilityFeedbackTypefeedbackGeneric android:accessibilityFlagsflagDefault|flagRetrieveInteractiveWindows|flagReportViewIds|flagRequestFilterKeyEvents android:canRetrieveWindowContenttrue android:canRequestFilterKeyEventstrue android:notificationTimeout100 android:descriptionstring/accessibility_service_desc android:settingsActivitycom.example.applock.ui.SettingsActivity /解释几个参数typeWindowStateChanged只监听窗口状态变化事件类型越少越省电。notificationTimeout事件通知的最小间隔100毫秒已经够用太短会频繁回调太长会丢事件。flagRequestFilterKeyEvents和canRequestFilterKeyEvents允许服务拦截系统按键事件后面防返回键绕过会用到。settingsActivity用户在系统设置里点击这个服务的时候会跳到我们App的设置页体验好不少。3.2 核心无障碍服务代码服务本身不干重活只做事件接收和过滤具体判断交给AppLockManagerpublic class LockAccessibilityService extends AccessibilityService { Override protected void onServiceConnected() { super.onServiceConnected(); Log.i(LockService, 无障碍服务已连接); } Override public void onAccessibilityEvent(AccessibilityEvent event) { if (event.getEventType() ! AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { return; } String pkg event.getPackageName() null ? : event.getPackageName().toString(); String cls event.getClassName() null ? : event.getClassName().toString(); if (!shouldHandle(pkg, cls)) { return; } if (AppLockManager.getInstance(this).shouldLock(pkg)) { LockScreenManager.getInstance(this).showLockScreen(pkg); } } private boolean shouldHandle(String pkg, String cls) { if (TextUtils.isEmpty(pkg) || TextUtils.isEmpty(cls)) { return false; } // 过滤系统UI、桌面、输入法、自身 if (AppLockManager.getInstance(this).isSystemApp(pkg, cls)) { return false; } return true; } Override public void onInterrupt() { Log.i(LockService, 无障碍服务被中断); } Override protected boolean onKeyEvent(KeyEvent event) { // 锁屏状态下吞掉返回键防止用户直接返回绕过 AppLockManager manager AppLockManager.getInstance(this); if (manager.isLockShowing() event.getKeyCode() KeyEvent.KEYCODE_BACK event.getAction() KeyEvent.ACTION_DOWN) { return true; } return super.onKeyEvent(event); } }这段代码里最容易被忽略的是onKeyEvent方法。悬浮窗如果设置了FLAG_NOT_FOCUSABLE返回键会直接穿透到下层Activity用户按一下返回就绕过锁屏了。所以要在服务层把它消费掉这是防绕过的关键一环。另外shouldHandle方法里对系统App的判断要做得宽一点因为部分厂商系统UI包名很杂不确定的时候就干脆把不是目标应用的事件全部忽略。3.3 悬浮锁屏界面的构建与参数说明锁屏界面我选择用WindowManager直接addView的方式而不是跳转一个新的Activity。选择悬浮窗还有一个理由Activity方式在高版本安卓上受后台限制影响比较大而悬浮窗完全由应用层控制生命周期更简单。public class LockScreenManager { private static final String TAG LockScreenManager; private static volatile LockScreenManager instance; private WindowManager windowManager; private View lockView; private String targetPackage ; private LockScreenManager(Context context) { windowManager (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); } public static LockScreenManager getInstance(Context context) { if (instance null) { synchronized (LockScreenManager.class) { if (instance null) { instance new LockScreenManager(context.getApplicationContext()); } } } return instance; } public void showLockScreen(String pkg) { if (lockView ! null) { return; } targetPackage pkg; AppLockManager.getInstance(getContext()).setLockShowing(true); AppLockManager.getInstance(getContext()).setCurrentLockedPackage(pkg); lockView LayoutInflater.from(getContext()).inflate(R.layout.layout_lock_screen, null); initLockView(lockView); WindowManager.LayoutParams params new WindowManager.LayoutParams( WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT, Build.VERSION.SDK_INT Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_PHONE, WindowManager.LayoutParams.FLAG_FULLSCREEN | WindowManager.LayoutParams.FLAG_SECURE, PixelFormat.TRANSLUCENT ); params.gravity Gravity.CENTER; windowManager.addView(lockView, params); } public void dismissLockScreen() { if (lockView ! null) { windowManager.removeView(lockView); lockView null; } AppLockManager.getInstance(getContext()).setLockShowing(false); AppLockManager.getInstance(getContext()).setCurrentLockedPackage(); LockAccessibilityService service LockAccessibilityService.getInstance(); if (service ! null) { service.goHome(); } } private Context getContext() { return AppLockManager.getInstance().getContext(); } }这里几个参数值得展开说WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAYAndroid 8.0之后悬浮窗必须用这个类型旧的TYPE_PHONE已经被废弃高版本上会直接报错。FLAG_FULLSCREEN全屏展示盖住状态栏视觉上更像一个原生锁屏页。FLAG_SECURE禁止截屏和录屏防止锁屏界面本身被截屏软件绕过。PixelFormat.TRANSLUCENT半透明格式可以给背景加一层毛玻璃效果。有个细节要注意虽然锁屏是悬浮窗但为了EditText能正常弹出输入法不能在params里设置FLAG_NOT_FOCUSABLE同时需要注意悬浮窗抢焦点后可能再次触发无障碍事件但因为我们前面已经做了isLockShowing判断所以不会出现递归弹锁的问题。3.4 密码校验与移除浮层锁屏界面的layout很简单就是一个背景View加一个居中的Card里面放密码输入框和确认按钮LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:idid/lock_root android:layout_widthmatch_parent android:layout_heightmatch_parent android:background#CCFFFFFF android:gravitycenter android:orientationvertical LinearLayout android:layout_width280dp android:layout_heightwrap_content android:backgrounddrawable/bg_lock_card android:orientationvertical android:padding24dp TextView android:layout_widthwrap_content android:layout_heightwrap_content android:text请输入应用锁密码 android:textSize16sp android:textColorandroid:color/black / EditText android:idid/et_password android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_marginTop12dp android:inputTypenumberPassword android:hint密码 / Button android:idid/btn_confirm android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_marginTop12dp android:text确认 / /LinearLayout /LinearLayout在initLockView里绑定点击事件private void initLockView(View view) { EditText etPassword view.findViewById(R.id.et_password); view.findViewById(R.id.btn_confirm).setOnClickListener(v - { String input etPassword.getText().toString(); if (AppLockManager.getInstance(getContext()).verifyPassword(input)) { dismissLockScreen(); } else { Toast.makeText(getContext(), 密码错误, Toast.LENGTH_SHORT).show(); etPassword.setText(); } }); // 背景点击不消费防止误触 view.findViewById(R.id.lock_root).setOnClickListener(null); }密码校验通过后调用dismissLockScreen在移除浮层前最好把activity切到桌面再移除。我代码里预留了一个service.goHome()实现方式是用无障碍服务的performGlobalAction(GLOBAL_ACTION_HOME)把用户带回桌面。为什么这么做因为浮层移除的一瞬间被锁App的内容会直接暴露在屏幕上如果用户本来就在那个App里面他看到的内容等于已经被绕过了。先回桌面再移除至少能保证用户在解锁后第一眼看到的是桌面而不是被锁App的敏感内容。这个细节对应用锁隐私保护来说非常重要。3.5 应用锁管理器的实现AppLockManager把包名列表和密码逻辑统一管理public class AppLockManager { private static final String PREFS_NAME app_lock_prefs; private static final String KEY_LOCKED_PKG locked_packages; private static final String KEY_PASSWORD lock_password; private static volatile AppLockManager instance; private final Context context; private final SharedPreferences prefs; private final SetString lockedPackages new HashSet(); private boolean isLockShowing false; private String currentLockedPackage ; private String lastLockedPkg ; private long lastLockTime 0L; private AppLockManager(Context context) { this.context context.getApplicationContext(); prefs this.context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); lockedPackages.addAll(prefs.getStringSet(KEY_LOCKED_PKG, new HashSet())); } public static AppLockManager getInstance(Context context) { if (instance null) { synchronized (AppLockManager.class) { if (instance null) { instance new AppLockManager(context.getApplicationContext()); } } } return instance; } public boolean needLock(String pkg) { if (TextUtils.isEmpty(pkg) || pkg.equals(context.getPackageName())) { return false; } return lockedPackages.contains(pkg); } public boolean shouldLock(String pkg) { if (isLockShowing) { return false; } if (pkg.equals(lastLockedPkg) System.currentTimeMillis() - lastLockTime 500L) { return false; } if (!needLock(pkg)) { return false; } lastLockedPkg pkg; lastLockTime System.currentTimeMillis(); return true; } public void addLockedPackage(String pkg) { lockedPackages.add(pkg); saveLockedPackages(); } public void removeLockedPackage(String pkg) { lockedPackages.remove(pkg); saveLockedPackages(); } public void setPassword(String password) { prefs.edit().putString(KEY_PASSWORD, password).apply(); } public boolean verifyPassword(String input) { String saved prefs.getString(KEY_PASSWORD, ); return !TextUtils.isEmpty(saved) saved.equals(input); } private void saveLockedPackages() { prefs.edit().putStringSet(KEY_LOCKED_PKG, lockedPackages).apply(); } }密码存储这里我直接用了明文SharedPreferences是因为示例代码要保持简单。生产环境一定要对密码做散列或者加密存储推荐用AES结合Android Keystore或者至少用加盐的哈希不要把用户密码原样落盘。另外从Android 6.0开始putStringSet得到的Set是一个备份副本修改后需要重新put不能直接改原来的Set这个坑也提一下。4. 防绕过与性能优化那些不得不处理的坑4.1 返回键、Home键与最近任务锁屏最怕三件事返回键、Home键、最近任务。返回键的坑我在无障碍服务里用onKeyEvent处理掉了锁定时只要按返回就消费掉用户怎么按都退不出去。但要注意onKeyEvent回调对服务配置有要求必须同时声明canRequestFilterKeyEventstrue和flagRequestFilterKeyEvents否则这个方法根本不会被调用。Home键是系统级按键应用层无法拦截。用户按Home回桌面再点进被锁App理论上会再次触发窗口状态变化事件锁屏会重新弹出来所以数据安全上没有太大问题。真正影响体验的问题是用户按Home之后锁屏浮层还在不在如果不在被锁App已经处于后台浮层也被销毁下次点进去又要重新弹锁比较烦。所以我在锁屏显示时其实没有监听HOME事件去销毁浮层而是让它继续挂在屏幕上。但Android 10之后悬浮窗在后台显示可能会被系统限制这个问题要结合厂商ROM实际测试。最近任务的坑更隐蔽用户打开微信后触发锁屏此时用户虽然没有看到微信界面但系统最近任务列表里可能已经生成了微信界面的缩略图。溜一眼最近任务等于原样暴露了。解决方案是给锁屏窗口加FLAG_SECURE它可以禁止截屏录屏同时能在大多数系统上让最近任务缩略图不敏感。但对已经被系统抓取的缩略图FLAG_SECURE能不能完全覆盖不同厂商表现不一样这个只能靠测试。更好的方案是锁屏显示时立刻调用goHome切走App把用户留在桌面缩略图状态不过这会牺牲一点流畅度。4.2 锁屏界面重复弹出和死循环锁屏浮层弹出之后因为窗口变化无障碍服务会立刻再收到一个新事件。如果你在代码里没有做isLockShowing判断就可能出现下面这种死循环微信切前台收到事件showLockScreen锁屏窗口addView系统派发新的窗口状态变化事件事件里看到包名还是微信又showLockScreenaddView同一个View窗口已存在抛异常崩溃。这个坑在开发阶段特别容易踩尤其是你调试的时候发现“加了锁屏就闪退”大概率就是这里。解决办法也很简单就是我在AppLockManager里面写的if (isLockShowing) return false;这段判断必须在所有系统App过滤之前放在事件处理的最前面保证锁屏浮层还在的时候整个无障碍服务暂时“半休眠”不再处理任何新事件。等密码验证通过移除浮层后再恢复。我还遇到过一种情况锁屏View里有个EditTextEditText弹输入法会导致窗口重绘又触发一次事件。如果前面isLockShowing判断漏了锁屏界面会自己弹自己。所以这个判断越靠前越安全。4.3 高版本安卓和厂商ROM兼容性Android 8.0之后悬浮窗类型必须换成TYPE_APPLICATION_OVERLAY如果继续用旧类型轻则显示在系统UI下层重则直接崩溃。Android 10之后后台启动Activity受限如果锁屏是用Activity实现的在Android 10上会遇到“后台弹Activity被拦截”的问题而悬浮窗方案完全规避了这一点。Android 13之后通知权限默认关闭如果App依赖通知来保活需要在设置页主动引导用户开通知权限。国产ROM的坑比系统版本更多。华为、小米、OPPO、vivo都有自己的后台清理策略无障碍服务即使在前台服务里也可能被一键清理干掉。常见的处理后是有引导页教用户把App加入“自启动管理”“电池优化白名单”“后台清理白名单”。这些引导页其实就是为了解决厂商ROM激进省电策略带来的问题不要觉得烦用户按引导点一遍就再也不会疯狂被杀进程了。另外真机测试时多测几个桌面和输入法包名。因为桌面和输入法的包名在不同系统上差异很大我建议做一个“系统白名单包名库”针对主流厂商预置一批同时提供手动添加白名单的入口这样不管用户手机是什么品牌都能自己把某些系统界面加入白名单。4.4 性能与耗电优化无障碍服务如果监听所有事件类型耗电会非常明显。我只监听typeWindowStateChanged一种事件配合100毫秒的notificationTimeout实际跑下来一天的电量占用可以控制在1%以内可以忽略不计。另一个性能优化点是把包名判断提前。在事件回调里最快的路径是包名为空、包名是系统UI、包名是自身、包名不在锁定列表这些情况直接return。不要动不动就查SharedPreferences或者做复杂的字符串判断因为锁屏逻辑本身要快越快越不容易被系统判定为卡顿。锁定列表用内存里的HashSet缓存而不是每次都读SharedPreferences也是性能优化的一部分。这个优化在应用锁里虽然收益不大但代码写习惯了遇到高频回调的场景就自然知道该怎么做。5. 常见问题与排查技巧实录最后把我做过这个项目后整理的问题排查表放出来都是真实遇到的场景照着查基本能定位90%的问题。现象可能原因解决方案服务已开启但锁屏不弹事件类型配置不对或者包名判断错误检查accessibility_service_config.xml里的typeWindowStateChanged打开调试日志看回调是否进入锁屏弹出来马上就闪退WindowManager重复addView或者没有悬浮窗权限在showLockScreen开头判断lockView ! null并确认已经申请SYSTEM_ALERT_WINDOW权限锁屏显示后无法输入密码悬浮窗没有获取焦点EditText无法弹键盘不要在LayoutParams里加FLAG_NOT_FOCUSABLE需要保留焦点能力按返回键直接绕过锁屏没有在无障碍服务中拦截返回键在onKeyEvent中判断锁屏状态并消费返回事件检查canRequestFilterKeyEvents配置打开自己的App也会弹锁没有过滤自身包名在过滤条件中加入packageName.equals(context.getPackageName())锁屏时偶尔能看到被锁App的内容闪现浮层移除顺序不对先调用goHome切到桌面再移除锁屏浮层锁定的微信切后台再切回来不用输密码时间窗口去重逻辑把新窗口事件吞了检查lastLockedPkg与lastLockTime是否在正常状态机里被重置部分机型锁屏出现黑屏系统界面窗口层级高于悬浮窗尝试不设置FLAG_FULLSCREEN或调整窗口type并在厂商白名单中配置后台弹窗权限排查无障碍服务问题最实用的工具是adbadb shell dumpsys accessibility这个命令可以看到当前哪些无障碍服务处于开启状态以及它们监听的事件类型、配置参数。如果发现服务没被系统识别基本可以确定是Manifest注册或meta-data配置有问题。还可以用adb shell dumpsys window windows查看当前窗口层级确认悬浮窗到底有没有加上去是层级太低被盖住了还是根本没有addView成功。这两个命令配合Log日志比肉眼在那儿干猜高效得多。再分享一个小技巧调试的时候把无障碍服务的notificationTimeout调成0事件回调会变得非常密集这样能更快复现“重复弹锁”的问题。但生产环境一定要调回100否则极度耗电。我当时就是靠着这个方法把事件重复问题在开发阶段就全部暴露了上线之后几乎没有用户反馈过弹锁崩溃。还有做这种需要无障碍权限的应用一定要在设置页里清晰告知用户这个权限是做什么用的以及为什么需要悬浮窗权限。一方面是合规要求另一方面也方便用户理解。不要试图在用户不知情的情况下申请这些高敏感权限一旦被系统或应用商店判定为滥用后果很严重。我个人做完这个项目最大的体会是不要把“拦截Activity启动”想成必须去Hook系统底层才能实现的事。能用标准API解决的就尽量用标准API优先保证兼容性和可维护性。无障碍服务加悬浮窗这套方案虽然权限门槛高一点但它是目前在不root的前提下实现应用锁最稳、最通用的路径。你把这套核心逻辑跑通之后还可以继续加指纹验证、人脸验证、免密时间段、自定义锁屏主题等等扩展空间非常大。最后再提醒一句无障碍服务能拿到全局窗口信息属于系统敏感权限做产品的时候一定要守住合规底线别拿它去做收集用户输入之类的事这是行业里的红线碰不得。

相关新闻

Codex接入Jev推理引擎:配置教程与排错实战

Codex接入Jev推理引擎:配置教程与排错实战

最近我把手头的编码工作流从“手动改代码、手动跑测试”切成了“Codex 驱动 Jev 推理”的组合,调完那一下午,最大的感受就是:以前是推着自行车上坡,现在像是换了个电机,路还是那条路,人轻快了太多。Codex …

2026/10/3 10:59:56 阅读更多 →
Spark信用卡评分卡全流程实战:从特征工程到评分映射

Spark信用卡评分卡全流程实战:从特征工程到评分映射

简介:这份资源是面向大数据与数据分析初学者的课程设计实战包,以和鲸社区信用卡评分模型数据为数据集,用Python结合Spark完成数据预处理、指标分析与结果可视化,适合正在学习Spark框架、需要完整项目案例练手的高校学生和开发者参…

2026/10/3 10:59:56 阅读更多 →
智能体工程化实战:从Demo到业务落地的完整指南

智能体工程化实战:从Demo到业务落地的完整指南

1. 趋势观察:智能体为什么突然从“Demo”跨到了“交付”GitHub Trending 每周都在变,但这段时间的趋势有一种明显的质地变化,就是智能体项目不再以“炫酷”取胜,而是以“能跑、能交付、能算账”为核心指标。我每周刷一遍 Trending…

2026/10/3 10:59:56 阅读更多 →

最新新闻

063红黑树 (Red-Black Tree)

063红黑树 (Red-Black Tree)

红黑树 (Red-Black Tree) — 5W1H故事与需求定义 063金发姑娘的平衡:揭秘红黑树Who(谁) 设计者:Rudolf Bayer(1972年提出"对称二叉B树"),由 Leonidas Guibas 和 Robert Sedgewick 于…

2026/10/3 11:34:30 阅读更多 →
AUTOSAR MCAL CAN模块配置与源码级调试实战

AUTOSAR MCAL CAN模块配置与源码级调试实战

1. 项目概述:为什么CAN模块配置是MCAL里最常踩坑的“硬骨头” 在AUTOSAR架构下做底层驱动开发,MCAL(Microcontroller Abstraction Layer)不是个抽象概念,而是每天要和它“肉搏”的真实存在。尤其当项目进入实车调试阶段…

2026/10/3 11:34:30 阅读更多 →
Visio画网上书店数据流图:分层拆解与PDF导出避坑指南

Visio画网上书店数据流图:分层拆解与PDF导出避坑指南

简介:这份PDF面向软件工程初学者与需要掌握结构化需求分析方法的开发者,以网上书店系统为案例,讲解如何用Visio 2007绘制Gane-Sarson数据流图。内容围绕“自顶向下、逐层分析”的思路展开,完整呈现顶层、中层与底层三层数据流图的…

2026/10/3 11:34:30 阅读更多 →
AI编程助手Skills实战指南:从定义、安装到场景应用

AI编程助手Skills实战指南:从定义、安装到场景应用

最近这一年,AI编程助手的热度一直没降,而“skills”这个词被提得越来越频繁。无论你用的是Claude Code、Codex还是OpenCode,只要想让AI真正融入自己的项目、按团队规范干活,最后基本都会绕回到skills上。说白了,skills…

2026/10/3 11:34:30 阅读更多 →
从问答到执行:AI Agent 的现场交付方法

从问答到执行:AI Agent 的现场交付方法

核心命题:Agent 交付的不是对话能力,而是可控的动作执行。一句话带走:Agent 交付的核心不是「让 AI 能调工具」,而是把每个不可逆动作变成可确认、可重试、可兜底的动作。知识库问答解决的是「给出信息」,但很多企业场…

2026/10/3 11:34:30 阅读更多 →
本地大模型部署实战指南:避坑56次总结的硬件、工具与量化选型

本地大模型部署实战指南:避坑56次总结的硬件、工具与量化选型

1. 这不是“装个软件就完事”的指南,而是帮你避开37个真实坑的本地大模型部署手记 我从2022年第一次在3090上跑起LLaMA-7B开始,到2024年带团队在Jetson Orin NX上把DeepSeek-V2-16B压缩到8GB显存稳定推理,再到今年上半年用Titan RTX量化工具链…

2026/10/3 11:33:29 阅读更多 →

日新闻

把回忆蒸馏成 AI 的浪漫实验:为什么你需要前任.skill 完整指南

把回忆蒸馏成 AI 的浪漫实验:为什么你需要前任.skill 完整指南

把回忆蒸馏成 AI 的浪漫实验:为什么你需要前任.skill 完整指南 【免费下载链接】ex-skill 前任 skill 项目地址: https://gitcode.com/gh_mirrors/exsk/ex-skill 前任.skill 是一个运行在 Claude Code 上的开源 Skill:导入微信、iMessage、短信、…

2026/10/3 0:00:27 阅读更多 →
45个经典Linux面试题:从命令到网络排障的完整考点解析

45个经典Linux面试题:从命令到网络排障的完整考点解析

刚开始带应届生的时候,我最头疼的就是他们拿着一摞Linux面试题背得滚瓜烂熟,一上机全露馅。后来自己从被面的人变成面别人的人,才慢慢摸清楚:Linux面试题考的根本不是答案本身,而是你面对一个不确定的系统问题时&#…

2026/10/3 0:01:28 阅读更多 →
SAP生产预留实战指南:MB21/MB23/MB25协同与MRP集成

SAP生产预留实战指南:MB21/MB23/MB25协同与MRP集成

简介:本资源是一份面向SAP ABAP开发人员、生产计划专员及ERP实施顾问的实操型操作指南,聚焦SAP生产预留核心业务场景,系统解决物料预留创建、查询、校验与批量处理等高频问题。文档以结构化方式覆盖预留背景原理、OMC2编码规则、工厂级参数配…

2026/10/3 0:01:28 阅读更多 →

周新闻

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/10/3 9:14:33 阅读更多 →
SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/10/3 9:47:50 阅读更多 →
FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏 【免费下载链接】FireRed-OpenStoryline FireRed-OpenStoryline is an AI video editing agent that transforms manual editing into intention-driven directing through natural language …

2026/10/3 9:42:31 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/2 10:36:31 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/3 9:42:35 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/3 9:42:36 阅读更多 →