- 移动开发
- 原生移动
- GUI 自动化
【免费下载链接】SKIP
自动跳过APP开屏广告
本文围绕 SKIP 的「应用白名单」功能展开:它用于列出设备上所有已安装的应用,并允许用户把不想被 SKIP 干预的应用加入白名单。读完本文,你不仅能掌握白名单的操作方式(含「显示系统应用」开关),还能理解它从 DataStore 持久化到无障碍服务点击拦截的完整实现链路。
功能概述:白名单解决什么问题
SKIP 的核心职责是通过无障碍服务自动点击目标控件来跳过应用开屏广告。但自动点击并不总是安全的——对某些应用(例如支付类、系统类或需要人工交互的应用),SKIP 的自动点击可能造成干扰。应用白名单功能就是为此设计的隔离开关,其行为规则为:
- 白名单页面会列出所有已经安装的应用;
- 页面右上角菜单中提供「显示系统应用」选项,由于系统应用数量较多,该选项默认不勾选;
- 当把一个应用加入白名单(即把它的开关打开)后,SKIP 就不会对该应用执行任何「点击」动作,无论该应用是否在配置文件中定义了跳过规则;
- 因此白名单可以用来防止 SKIP 对某些应用的干扰。
也就是说,白名单的优先级高于配置规则:即使配置文件中为某个应用写好了skipIds/skipTexts规则,只要该应用的包名出现在白名单中,SKIP 的自动点击逻辑就会整体跳过它。
功能入口与页面结构
白名单入口位于主界面,MainActivity.kt 中的WhiteListButton通过startActivity跳转到WhiteListActivity。
WhiteListActivity.kt 是一个基于 Jetpack Compose 的页面,整体结构分为两部分:
- 内容区(
Column):上方是搜索栏SearchTextField,下方是应用列表AppListColumn; - 右上角菜单:包含「功能介绍」和「显示系统应用」两个菜单项。
对应地,该页面注入了 5 个 ViewModel,各自承担独立职责:
| ViewModel | 职责 | 源码位置 |
|---|---|---|
AppListViewModel | 应用列表的分页加载与刷新 | AppListViewModel.kt |
ShowSystemViewModel | 「显示系统应用」勾选状态 | ShowSystemViewModel.kt |
WhiteListViewModel | 白名单开关的变更与持久化 | WhiteListViewModel.kt |
SwitchThemeViewModel | 全局主题切换 | 位于ui/settings/theme目录 |
SearchViewModel | 搜索关键字状态 | SearchViewModel.kt |
在onCreate中,Activity 建立了两处状态联动(见 WhiteListActivity.kt):
showSystemViewModel.isShowSystem变化时,调用appListViewModel.reloadData(it, searchViewModel.getQuery())重新加载列表;searchViewModel.query变化时,调用appListViewModel.reloadData(showSystemViewModel.getIsShowSystem(), it)按新关键字刷新。
因此无论用户勾选「显示系统应用」还是输入搜索词,列表都会以两者组合作为条件整体重建。菜单中的「显示系统应用」由 ShowSystemButton.kt 渲染为一个Checkbox,其默认值在ShowSystemViewModel中初始化为false——这与文档中「因为系统应用较多,所以默认是不列出的」的描述完全一致。
此外,菜单里的「功能介绍」项会打开WebViewActivity加载官方文档页面,字符串常量定义在 strings.xml:whitelist(应用白名单)、whitelist_show_system(显示系统应用)、whitelist_dot(存储键前缀WHITELIST_DOT)等。
应用列表:系统应用过滤、搜索与分页
列表数据的获取实现在 AppListRepository.kt,其getData(currentPage, pageSize, isShowSystem, query)方法分三步处理:
第一步:读取全部已安装应用
private val appInfos: MutableList<ApplicationInfo> by lazy { MyApp.context.packageManager.getInstalledApplications(PackageManager.GET_META_DATA) }应用信息只通过PackageManager枚举一次并缓存在内存中(by lazy),后续过滤、搜索、分页都在这份缓存上进行。
第二步:按「是否显示系统应用」过滤
val apps = if (isShowSystem) { appInfos } else { appInfos.filter { (it.flags and ApplicationInfo.FLAG_SYSTEM) == 0 } }从源码结构看,这里判断的是ApplicationInfo.FLAG_SYSTEM标志位:默认(未勾选「显示系统应用」)时,只要应用带有系统应用标志就被过滤掉;勾选后则展示完整列表。
第三步:按关键字过滤并分页截取
val filteredApps = if (query.isNotEmpty()) { apps.filter { it.loadLabel(pkgManager).toString().contains(query, ignoreCase = true) } } else { apps } val fromIndex = currentPage * pageSize val toIndex = minOf(fromIndex + pageSize, filteredApps.size) return filteredApps.subList(fromIndex, toIndex) .map { AppListItem( it.loadLabel(pkgManager).toString(), // 应用名 it.packageName, // 包名(白名单的判定依据) it.loadIcon(pkgManager), // 图标 whiteListRepository.isAppInWhiteList(it.packageName) // 白名单状态 ) }搜索是按应用显示名(loadLabel)做忽略大小写的子串匹配,且每行都会实时查询该包名是否已在白名单中(isAppInWhiteList),用于渲染开关的初始状态。数据模型见 AppListItem.kt,包含appName、packageName、appIcon、checked四个字段。
分页由 Paging3 驱动:AppListViewModel.kt 中PagingConfig设置pageSize = 20且enablePlaceholders = false;reloadData会先invalidate()旧的AppListPagingSource,再创建携带新isShowSystem/query参数的新Pager。翻页逻辑在 AppListPagingSource.kt 中:以页码为 key,返回空页时nextKey置null表示没有更多数据。列表 UI(AppListColumn.kt)用LazyColumn+collectAsLazyPagingItems渲染,并处理了加载中、「加载更多...」与加载失败三种LoadState。搜索框 SearchTextField.kt 是一个 56dp 高、圆角 16dp 的OutlinedTextField,输入即触发SearchViewModel.changeQuery。
白名单状态的存储:DataStore 中的布尔键值对
白名单的持久化是整个功能中最值得注意的部分。每个应用的白名单状态并不是存在一个数组里,而是以「前缀 + 包名」为键的独立布尔键写入 Preferences DataStore:
WhiteListViewModel.kt 中,开关变更时执行:
fun updateWhiteList(packageName: String, checked: Boolean) { whiteListRepository.updateWhiteList(packageName, checked) // 1. 先更新内存态 viewModelScope.launch { withContext(Dispatchers.IO) { DataStoreUtils.putData(getString(R.string.whitelist_dot) + packageName, checked) // 2. 再落盘 } } }即最终写入的键形如WHITELIST_DOTcom.jd.app.reader(前缀常量WHITELIST_DOT定义在 strings.xml),值为true/false,由 DataStoreUtils.kt 的saveBooleanData写入名为SKIPAPPDataStore的 Preferences 文件(见 DataStoreUtils.kt)。
读取侧在 WhiteListRepository.kt 中,它是一个@Singleton,进程内只初始化一次:
@Singleton class WhiteListRepository @Inject constructor() { private val _whiteList: MutableList<String> by lazy { runBlocking { initWhitelist().toMutableList() } } private suspend fun initWhitelist() = withContext(Dispatchers.IO) { val allPreferences = MyApp.context.dataStore.data.first() allPreferences.asMap() .filter { it.key.name.startsWith(getString(R.string.whitelist_dot)) && it.value == true }.keys.map { it.name.substringAfter(getString(R.string.whitelist_dot)) } } // ... }其初始化逻辑是:把 DataStore 中所有键名以WHITELIST_DOT开头且值为true的键筛选出来,去掉前缀后即得到已加入白名单的包名集合,缓存为内存中的_whiteList。之后:
updateWhiteList(packageName, checked):勾选时向列表添加包名,取消勾选时移除(仅改内存态,落盘由 ViewModel 负责);isAppInWhiteList(packageName):通过indexOf(packageName) != -1判断包名是否在白名单中。
这种「前缀键 + 布尔值」的设计让每个应用的白名单状态可以独立读写、互不干扰,新增/移除应用也无需重建整个存储结构。
拦截机制:白名单应用如何被「完全放行」
白名单真正生效的位置在无障碍服务的事件回调中。MyAccessibilityService.kt 的onAccessibilityEvent是 SKIP 每次屏幕事件到达时的入口,其中第 95 行是整个自动点击流程的总闸:
val rootNodePackageName = rootNode.packageName.toString() if (rootNodePackageName != appPackageName) { scanTimes = 0 clickedRect.clear() appPackageName = rootNodePackageName } if (!whiteListRepository.isAppInWhiteList(rootNodePackageName) && (isStrict || scanTimes < 50)) { // 通过配置查找目标控件 rect,找到后 dispatchGesture 执行点击 ... }可以确认以下几点:
- 白名单检查发生在最外层:
!whiteListRepository.isAppInWhiteList(rootNodePackageName)为假时,整段「查配置 → 找 rect → 发点击手势」的逻辑直接不执行。这与文档中「不会对这个应用执行任何点击的动作(无论是否在配置中定义)」的语义一致——白名单优先级高于配置文件中packageName/skipIds/skipTexts/skipBounds的规则(配置示例见 skip_config_v2.yaml)。 - 判定依据是当前焦点窗口的根节点包名:SKIP 以
rootInActiveWindow的packageName识别「当前前台应用」,前台应用切换时会重置scanTimes并清空已点击区域缓存clickedRect,保证白名单判定始终针对当前所在应用。 - 点击动作本身通过无障碍手势派发:命中目标后会构造一条 100ms 的
GestureDescription,用dispatchGesture完成模拟点击(见 MyAccessibilityService.kt)。白名单应用因为走不到这一步,天然免疫所有此类点击。
另外,从源码结构看,isStrict(严格模式)与scanTimes < 50(扫描次数上限)是与白名单并列的两道控制条件,但它们只影响「是否继续尝试匹配点击」,而白名单是唯一能彻底关闭某应用点击行为的用户级开关。
小结:相关文件索引
| 关注点 | 文件 |
|---|---|
| 功能说明文档 | docs/guide/white-list/intro.md |
| 白名单页面(搜索栏 + 列表 + 菜单) | app/src/main/java/com/android/skip/ui/whitelist/WhiteListActivity.kt |
| 白名单内存态 + DataStore 键解析 | app/src/main/java/com/android/skip/ui/whitelist/WhiteListRepository.kt |
| 开关持久化写入 | app/src/main/java/com/android/skip/ui/whitelist/WhiteListViewModel.kt |
| 已安装应用枚举、系统应用过滤、搜索、分页 | app/src/main/java/com/android/skip/ui/whitelist/list/AppListRepository.kt |
| Paging3 分页配置 | app/src/main/java/com/android/skip/ui/whitelist/list/AppListViewModel.kt |
DataStore 工具与SKIPAPPDataStore | app/src/main/java/com/android/skip/util/DataStoreUtils.kt |
| 点击拦截入口(白名单判定) | app/src/main/java/com/android/skip/service/MyAccessibilityService.kt |
字符串常量(WHITELIST_DOT等) | app/src/main/res/values/strings.xml |
总结来说:SKIP 的白名单是一个「按包名一票否决」的隔离机制——UI 层通过PackageManager枚举应用并用 Paging3 分页展示,状态层用WHITELIST_DOT前缀布尔键写入 Preferences DataStore,执行层则在无障碍事件回调的最外圈用isAppInWhiteList短路整个点击流程,从而保证被加入白名单的应用永远不会收到 SKIP 发出的任何自动点击。
- 移动开发
- 原生移动
- GUI 自动化
【免费下载链接】SKIP
自动跳过APP开屏广告
相关推荐
深入理解Riva-Translate-4B-Instruct-v1.1的聊天模板:自定义翻译指令的终极技巧
深入理解Riva Translate 4B Instruct v1.1的聊天模板:自定义翻译指令的终极技巧 Riva Translate 4B Instruct
AutoClicker自动点击器完全使用指南
AutoClicker自动点击器完全使用指南 AutoClicker是一款功能强大的桌面应用程序,专门用于自动化鼠标点击任务。该工具基于C 和WPF技术开发,提
桌面应用GUI 自动化NocoBase 多应用应用单点登录:子应用自动登录、用户名映射与自动注册实践
NocoBase 多应用应用单点登录:子应用自动登录、用户名映射与自动注册实践 应用单点登录(App SSO)是 NocoBase 多应用场景下的一项认证能力:
低代码后端前端人工智能AI 应用工作流自动化
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考