## 1. Project Overview & Quickstart (gkd-kit/gkd)

# gkd



基于 [高级选择器](https://gkd.li/guide/selector) + [订阅规则](https://gkd.li/guide/subscription) + [快照审查](https://github.com/gkd-kit/inspect) 的自定义屏幕点击 Android 应用

通过自定义规则，在指定界面，满足指定条件(如屏幕上存在特定文字)时，点击特定的节点或位置或执行其他操作

- **快捷操作**

  帮助你简化一些重复的流程, 如某些软件自动确认电脑登录

- **跳过流程**

  某些软件可能在启动时存在一些烦人的流程, 这个软件可以帮助你点击跳过这个流程

## 免责声明

**本项目遵循 [GPL-3.0](/LICENSE) 开源，项目仅供学习交流，禁止用于商业或非法用途**

## 安装

[](https://gkd.li/guide/)[](https://play.google.com/store/apps/details?id=li.songe.gkd)[](https://github.com/gkd-kit/gkd/releases)

如遇问题请先查看 [疑难解答](https://gkd.li/guide/faq)

## 截图

|                                                               |                                                               |                                                               |                                                               |
| ------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------- |
|  |  |  |  |
|  |  |  |  |

## 订阅

GKD **默认不提供规则**，需自行添加本地规则，或者通过订阅链接的方式获取远程规则

也可通过 [subscription-template](https://github.com/gkd-kit/subscription-template) 快速构建自己的远程订阅

第三方订阅列表可在 <https://github.com/topics/gkd-subscription> 查看

要加入此列表, 需点击仓库主页右上角设置图标后在 Topics 中添加 `gkd-subscription`


示例图片 - 添加至 Topics (点击展开)





## 选择器

一个类似 CSS 选择器的选择器, 能联系节点上下文信息, 更容易也更精确找到目标节点

<https://gkd.li/guide/selector>

[@[vid=\"menu\"] < [vid=\"menu_container\"] - [vid=\"dot_text_layout\"] > [text^=\"广告\"]](https://i.gkd.li/i/14881985?gkd=QFt2aWQ9Im1lbnUiXSA8IFt2aWQ9Im1lbnVfY29udGFpbmVyIl0gLSBbdmlkPSJkb3RfdGV4dF9sYXlvdXQiXSA-IFt0ZXh0Xj0i5bm_5ZGKIl0)


示例图片 - 选择器路径视图 (点击展开)

[](https://i.gkd.li/i/14881985?gkd=QFt2aWQ9Im1lbnUiXSA8IFt2aWQ9Im1lbnVfY29udGFpbmVyIl0gLSBbdmlkPSJkb3RfdGV4dF9sYXlvdXQiXSA-IFt0ZXh0Xj0i5bm_5ZGKIl0)



## 衍生

开发过程中的衍生项目，它们正在被 gkd 使用，也许对你有帮助

- [kotlin-json5](https://github.com/lisonge/kotlin-json5)
- [kotlin-loc](https://github.com/lisonge/kotlin-loc)
- [android-api-diff](https://github.com/android-cs/android-api-diff)
- [remap](https://github.com/lisonge/remap)
- [priv-kit](https://github.com/priv-kit/priv-kit)

## 捐赠

如果 GKD 对你有用, 可以通过以下链接支持该项目

<https://github.com/lisonge/sponsor>

或前往 [Google Play](https://play.google.com/store/apps/details?id=li.songe.gkd) 给个好评

## 2. Official Technical Reference & Guides (gkd-kit/docs)

## File: README.md

# 说明

GKD 文档(尚未开发完毕), 基于 [vitepress](https://vitepress.dev/zh/) 构建

可前往 [gkd.li](https://gkd.li) 预览

## 提交贡献

欢迎提交 [规则示例](./docs/guide/subscription.md) 和 [选择示例](./docs/guide/example.md)

确保你对 [vitepress](https://vitepress.dev/zh/) 有基本了解

然后 [Fork](https://github.com/gkd-kit/docs/fork) 仓库

准备环境: node20+, pnpm9+ 最好使用最新版本

安装依赖

```sh
pnpm i
```

启动文档预览, 此时修改 md 文件会自动刷新浏览器界面

```sh
pnpm dev
```

修改提交推送后发起 pull request

---

## File: api/README.md

# @gkd-kit/api

## Install

```shell
pnpm add @gkd-kit/api
```

## Usage

```ts
import type { RawSubscription } from '@gkd-kit/api';
```

---

## File: docs/guide/example.md

# 选择示例 {#title}

一些选择器及其相关作用说明

## 选择根节点 {#e-1}

- `[parent=null]`
- `[depth=0]`

## 选择子节点 {#e-2}

选择一个 `ImageView` 节点, 并要求其父节点是 `LinearLayout`

[`LinearLayout > ImageView`](https://i.gkd.li/i/16076188?gkd=TGluZWFyTGF5b3V0ID4gSW1hZ2VWaWV3)

选择一个 `ImageView` 节点, 并要求其父节点的父节点是 `ViewGroup[vid="avatar_layout"]`

[`ViewGroup[vid="avatar_layout"] >2 ImageView`](https://i.gkd.li/i/16076188?gkd=Vmlld0dyb3VwW3ZpZD0iYXZhdGFyX2xheW91dCJdID4yIEltYWdlVmlldw)

选择一个 `ImageView[vid="search_icon"]` 节点, 并要求祖先节点是 `FrameLayout[vid="content_layout"]`, `n`可以是任意正整数, 下同

[`FrameLayout[vid="content_layout"] >n ImageView[vid="search_icon"]`](https://i.gkd.li/i/16076188?gkd=RnJhbWVMYXlvdXRbdmlkPSJjb250ZW50X2xheW91dCJdIEltYWdlVmlld1t2aWQ9InNlYXJjaF9pY29uIl0)

## 末尾子节点 {#e-3}

选择一个 `ImageView` 节点, 并要求是其父节点是最后一个子节点

[`ImageView[index=parent.childCount.minus(1)]`](https://i.gkd.li/i/16076188?gkd=SW1hZ2VWaWV3W2luZGV4PXBhcmVudC5jaGlsZENvdW50Lm1pbnVzKDEpXQ)

## 选择兄弟节点 {#e-4}

选择一个 `LinearLayout[desc="搜索栏，按钮"]` 节点, 并要求其兄弟节点是 `ViewGroup[desc="个人主页，按钮"]`

- [`ViewGroup[desc="个人主页，按钮"] + LinearLayout[desc="搜索栏，按钮"]`](https://i.gkd.li/i/16076188?gkd=Vmlld0dyb3VwW2Rlc2M9IuS4quS6uuS4u-mhte-8jOaMiemSriJdICsgTGluZWFyTGF5b3V0W2Rlc2M9IuaQnOe0ouagj--8jOaMiemSriJd)
- [`@LinearLayout[desc="搜索栏，按钮"] - ViewGroup[desc="个人主页，按钮"]`](https://i.gkd.li/i/16076188?gkd=QExpbmVhckxheW91dFtkZXNjPSLmkJzntKLmoI_vvIzmjInpkq4iXSAtIFZpZXdHcm91cFtkZXNjPSLkuKrkurrkuLvpobXvvIzmjInpkq4iXQ)

选择一个 `[text$="顶级入耳"]` 节点, 并要求其兄弟节点是 `[vid="cover_layout"]`

- [`[vid="cover_layout"] +n [text$="顶级入耳"]`](https://i.gkd.li/i/16076188?gkd=W3ZpZD0iY292ZXJfbGF5b3V0Il0gK24gW3RleHQkPSLpobbnuqflhaXogLMiXQ)
- [`@[text$="顶级入耳"] -n [vid="cover_layout"]`](https://i.gkd.li/i/16076188?gkd=QFt0ZXh0JD0i6aG257qn5YWl6ICzIl0gLW4gW3ZpZD0iY292ZXJfbGF5b3V0Il0)

## 选择父节点 {#e-5}

选择一个 `ViewGroup[desc^="直播"]` 节点, 并要求其子节点是 `FrameLayout[vid="cover_layout"]` 或 `ViewGroup[vid="live_text_container"]` 或 `TextView[vid="title_v2"]`

- [`FrameLayout[vid="cover_layout"] < ViewGroup[desc^="直播"]`](https://i.gkd.li/i/16076188?gkd=RnJhbWVMYXlvdXRbdmlkPSJjb3Zlcl9sYXlvdXQiXSA8IFZpZXdHcm91cFtkZXNjXj0i55u05pKtIl0)
- [`ViewGroup[vid="live_text_container"] <3 ViewGroup[desc^="直播"]`](https://i.gkd.li/i/16076188?gkd=Vmlld0dyb3VwW3ZpZD0ibGl2ZV90ZXh0X2NvbnRhaW5lciJdIDwzIFZpZXdHcm91cFtkZXNjXj0i55u05pKtIl0)
- [`TextView[vid="title_v2"] <n ViewGroup[desc^="直播"]`](https://i.gkd.li/i/16076188?gkd=VGV4dFZpZXdbdmlkPSJ0aXRsZV92MiJdIDxuIFZpZXdHcm91cFtkZXNjXj0i55u05pKtIl0)

## 在指定节点的子节点内查找指定选择器 {#e-6}

一般在末尾节点支持 [快速查询](/guide/optimize#fast-query) 的情况下使用

在节点 `ViewGroup[vid="live_text_container"]` 的子节点内查找 `[text="直播"]`

[`@[text="直播"] <<n ViewGroup[vid="live_text_container"]`](https://i.gkd.li/i/16076188?gkd=QFt0ZXh0PSLnm7Tmkq0iXSA8PG4gVmlld0dyb3VwW3ZpZD0ibGl2ZV90ZXh0X2NvbnRhaW5lciJd)

在节点 `FrameLayout[vid="swipe_layout"]` 的子节点内查找 `LinearLayout > @[text="直播"]`

[`LinearLayout > @[text="直播"] <<n [vid="swipe_layout"]`](https://i.gkd.li/i/16076188?gkd=TGluZWFyTGF5b3V0ID4gQFt0ZXh0PSLnm7Tmkq0iXSA8PG4gW3ZpZD0ic3dpcGVfbGF5b3V0Il0)

## 组合使用 {#e-7}

[`ImageView < FrameLayout <n ViewGroup[desc^="直播"] - ViewGroup >4 FrameLayout[index=0] +2 FrameLayout > @TextView[index=parent.childCount.minus(1)] <<n FrameLayout[vid="content"]`](https://i.gkd.li/i/16076188?gkd=SW1hZ2VWaWV3IDwgRnJhbWVMYXlvdXQgPG4gVmlld0dyb3VwW2Rlc2NePSLnm7Tmkq0iXSAtIFZpZXdHcm91cCA-NCBGcmFtZUxheW91dFtpbmRleD0wXSArMiBGcmFtZUxheW91dCA-IEBUZXh0Vmlld1tpbmRleD1wYXJlbnQuY2hpbGRDb3VudC5taW51cygxKV0gPDxuIEZyYW1lTGF5b3V0W3ZpZD0iY29udGVudCJd)

## 一些方法用例 {#e-8}

### [node](/guide/node#node)类型的`getChild()`

选择一个 `ViewGroup[desc^="直播"]` 节点, 并要求其**第1位子节点**的`vid`等于 `cover_layout`

- [`ViewGroup[desc^="直播"][getChild(0).vid="cover_layout"]`](https://i.gkd.li/i/16076188?gkd=Vmlld0dyb3VwW2Rlc2NePSLnm7Tmkq0iXVtnZXRDaGlsZCgwKS52aWQ9ImNvdmVyX2xheW91dCJd)

选择一个 `ViewGroup[desc^="直播"]` 节点, 并要求其**第3位子节点内的第1位子节点**类型为 `TextView`

- [`ViewGroup[desc^="直播"][getChild(2).getChild(0).name$="TextView"]`](https://i.gkd.li/i/16076188?gkd=Vmlld0dyb3VwW2Rlc2NePSLnm7Tmkq0iXVtnZXRDaGlsZCgyKS5nZXRDaGlsZCgwKS5uYW1lJD0iVGV4dFZpZXciXQ)

选择一个 `ViewGroup[desc^="直播"]` 节点, 并要求其**末尾子节点**的`vid`等于 `more`

- [`ViewGroup[desc^="直播"][getChild(childCount.minus(1)).vid="more"]`](https://i.gkd.li/i/16076188?gkd=Vmlld0dyb3VwW2Rlc2NePSLnm7Tmkq0iXVtnZXRDaGlsZChjaGlsZENvdW50Lm1pbnVzKDEpKS52aWQ9Im1vcmUiXQ)

### [context](/guide/node#context)类型的`getPrev()`

已知符合选择器 [@[clickable=true] >(1,2) [vid="cover"]](https://i.gkd.li/i/16076188?gkd=QFtjbGlja2FibGU9dHJ1ZV0gPigxLDIpIFt2aWQ9ImNvdmVyIl0) 的节点有6个，现要求根据屏幕分辨率对节点位置作出限制。

> 思路：从右侧根节点处获取屏幕宽高，通过`getPrev()`传到左侧目标节点，随后根据要求做限制
>
> 另外由于使用了`<<n [parent=null]`，所以结果仅显示第一个，具体缘由请看[主动查询](/guide/optimize#enforce)

限制在左半屏：

- [`@[clickable=true][right<getPrev(1).width.div(2)] >(1,2) [vid="cover"] <<n [parent=null]`](https://i.gkd.li/i/16076188?gkd=QFtjbGlja2FibGU9dHJ1ZV1bcmlnaHQ8Z2V0UHJldigxKS53aWR0aC5kaXYoMildID4oMSwyKSBbdmlkPSJjb3ZlciJdIDw8biBbcGFyZW50PW51bGxd)

限制在右下(1/4)屏：

- [`@[clickable=true][left>getPrev(1).width.div(2)][top>getPrev(1).height.div(2)] >(1,2) [vid="cover"] <<n [parent=null]`](https://i.gkd.li/i/16076188?gkd=QFtjbGlja2FibGU9dHJ1ZV1bbGVmdD5nZXRQcmV2KDEpLndpZHRoLmRpdigyKV1bdG9wPmdldFByZXYoMSkuaGVpZ2h0LmRpdigyKV0gPigxLDIpIFt2aWQ9ImNvdmVyIl0gPDxuIFtwYXJlbnQ9bnVsbF0)

---

## File: docs/guide/external.md

# 外部交互 {#title}

一些无法在应用内界面上直接使用的功能

## 应用图标 {#app-icon}

GKD 有两种应用图标

- 默认图标
- 深色模式下的图标

一般来说, 第一次显示的图标取决于安装时的系统深色模式设置

而且系统貌似不会在切换深色模式自动切换图标, 你可以通过以下方式切换

- 到 系统主题-图标包 切换其他后再切换回来
- 重启手机
- 同版本覆盖安装 GKD, 无需卸载

## 跳转应用 {#scheme}

可以在浏览器或其他应用打开特定链接来跳转到应用内页面

| 页面     | 链接           |
| -------- | -------------- |
| 首页     | `gkd://page`   |
| 高级设置 | `gkd://page/1` |
| 快照记录 | `gkd://page/2` |

## 快捷开关 {#shortcut}

GKD 在通知栏面板声明了一些快捷开关, 可快捷使用一些功能

| 名称     | 功能                                         |
| -------- | -------------------------------------------- |
| GKD      | 打开或关闭无障碍权限                         |
| HTTP服务 | 打开或关闭HTTP服务                           |
| 悬浮按钮 | 打开或关闭悬浮按钮, 点击出现的按钮可捕获快照 |
| 捕获快照 | 点击后捕获当前应用界面快照                   |

## 导入数据 {#import-data}

你可以在 主页-订阅-右上角菜单-导入数据 中导入订阅规则数据

但是这样需要在打开的文件管理器中手动选择路径不太方便

另一个更方便的迁移数据的方式是

- 旧设备: 导出数据 - 选择分享到 QQ 发送给自己

- 新设备: 在 QQ 中打开分享的文件 - 用其他应用打开 - 选择 GKD 导入数据

上面的 QQ 也可以是其他传送软件, 只要支持用其他应用打开的时候有 GKD 即可

---

## File: docs/guide/faq.md

# 常见问题 {#title}

一些可能影响使用体验的问题和对应解决方案, 某些方案可能并不适用您的机型系统

如您有更好的方案或想对现有方案进行补充, 请点击底部 `为此页提供修改建议`

## 渠道区别 {#play-diff}

目前有两个渠道在使用，它们可互相安装覆盖，但存在区别

| 渠道 |                                 下载方式                                  |     自更新逻辑      | [受限制的设置](#restriction) | 无障碍工具声明 |
| :--: | :-----------------------------------------------------------------------: | :-----------------: | :--------------------------: | :------------: |
| gkd  |                          [官网/GitHub](/guide/)                           | 弹窗更新(镜像加速)  |           可能受限           |       有       |
| play | [Google Play](https://play.google.com/store/apps/details?id=li.songe.gkd) | 无(必须Google Play) |             不会             |       无       |

根据 [Google Play 政策](https://support.google.com/googleplay/android-developer/answer/10964491?hl=zh-Hans)，必须以 **支持残障人士为主要用途** 才能使用 **无障碍工具声明**

如果没有这个声明，系统会隔段时间显示通知([discussions#744](https://github.com/orgs/gkd-kit/discussions/744))，内容如下

`查看权限设置(GKD可以查看和控制您的屏幕。点按即可查看。)`

如果你无法忍受这个通知，切换到 gkd 渠道即可

## 耗电说明 {#power}

首先 GKD 默认情况下并不耗电，有的人以为关闭更新检测，关闭日志就能减少耗电，并不正确

真正的耗电在于 启用的规则 和 当前应用界面无障碍节点数量及无障碍事件刷新频率

如果想追求低耗电，那应该开启少量的规则，由于之前的默认规则数量有 1300+，很多人喜欢无脑开启规则

甚至有的用户同时开启多个订阅, 而这些订阅基本都 fork 自默认订阅, 这相当于同时开启多个重复规则

开启这么多规则，并且在这些规则匹配的界面使用手机，不耗电是不可能的

此外，某些软件如抖音的无障碍节点数量比较多，并且产生无障碍事件频率也比较高

长时间下使用此类软件的规则会比较耗电，请自行考虑后再启用

## shizuku

shizuku 是一个能让普通应用通过 ADB 或 Root 权限直接调用 Android 系统 API 的框架

简而言之， **授权 shizuku 后 GKD 可以调用更多系统 API 获取更多状态，启用更多优化**

官方原版 [RikkaApps/Shizuku](https://github.com/RikkaApps/Shizuku) 已很久未有维护记录，issue/pr 也被垃圾机器人淹没

此外在某些设备上原版 shizuku 只能部分工作，此时 GKD 会提示 `服务连接部分失败`

目前建议使用另一位开发者的分支项目 [thedjchi/Shizuku](https://github.com/thedjchi/Shizuku)，或[点击前往下载](https://github.com/thedjchi/Shizuku/releases/latest)

## 后台运行 {#persistent}

如何尽量保持后台运行不被系统暂停/杀死

- 任务切换界面给 GKD 应用卡片加锁即可

一般情况下, 使用此项即可正常后台运行, 如果不行请使用下面的步骤

- 打开 GKD 首页, 开启 `常驻通知`
- 允许后台活动, 关闭省电策略, 或者设置 GKD 的省电策略为无限制
- 允许 GKD 自启动

某些系统如 MagicOS9 需要打开 `允许后台活动`, 否则 GKD 在后台时会被系统暂停执行

<GImg src="0038.jpg" />

## 受限制的设置 {#restriction}

无障碍列表界面提示

> 受限制的设置(出于安全考虑，此设置目前不可用)

或提示

> 系统已拒绝向此应用授予访问权限



这是因为某些系统对于 不在应用商店等可信任来源安装的 不能直接开启无障碍权限

解除步骤: GKD 应用信息 - 允许受限制的设置, 注意某些设备位置不太一样



| 原生系统  应用信息 - 右上角 - 允许受限制的设置 | 小米系统  应用信息 - 高级设置 - 允许受限制的设置 |
| --- | --- |
|  |  |



如果按步骤开启后 无障碍列表 仍然提示不可用, 可尝试 **重启设备**

如果应用信息右上角没有 `允许受限制的设置` 选项, 请参考下一节方案

## 无法开启无障碍 {#unable_open_a11y}

某些系统如 ColorOS 有更严格的无障碍限制

如果你是其他来源安装或 GKD 内部更新后也会无法开启无障碍, 表现以下行为

- 提示 受限制的设置, 但是没有找到 允许受限制的设置 的选项
- 不提示 受限制的设置, 无障碍开启按钮可点击, 但点击后没有响应, 无法开启

可以通过以下方式解决, 可在 设置 - 关于 - 分享应用 获取 APK 文件

- 使用系统信任的方式进行安装, 例如在系统文件管理中查看 APK 文件后点击安装, 是覆盖安装无需卸载

- 如果使用系统安装器, 但是在冻结了应用安装器 `com.oplus.appdetail` 状态下安装的 APK, 需解冻后重新安装

- 使用 [App Ops](https://appops.rikka.app/) 在权限管理中手动授予 ACCESS_RESTRICTED_SETTINGS 权限
  <GImg src="0037.png" />

## 关闭无障碍警告弹窗 {#close_warn_dialog}

> [!TIP] 建议
> 授予 `写入安全设置权限` 后可自行开启无障碍, 而无需设置此项

这个方案不一定适用所有机型

大多数设备开启无障碍时都会出现 10s 的无障碍警告弹窗

若是只需要开启一次则无所谓但是如果开启次数比较多每次都有这个警告等待就很烦人了



你可能已经看到这个界面还有一个开关, 也就是下方的 `快捷方式` 开关

打开一次下面这个快捷方式开关后, 上面的的开启按钮就不会出现警告弹窗了, 但是你会发现屏幕侧边出现了一个应用小图标, 多数情况下我们并不需要这个图标

到无障碍界面-"无障碍"按钮-使用按钮或手势-点击切换为手势即可隐藏

以 Xiaomi HyperOS 为例子, 下面是完整的关闭弹窗流程截图



## adb 授权失败 {#adb_failed}

在使用 adb 手动授予 `写入安全设置权限` 时可能会提示权限不够，具体报错如下:

```text
adb shell pm grant li.songe.gkd android.permission.WRITE_SECURE_SETTINGS

Exception occurred while executing 'grant':
java.lang.SecurityException: grantRuntimePermission: Neither user 2000 nor current process has android.permission.GRANT_RUNTIME_PERMISSIONS.
  at android.app.ContextImpl.enforce(ContextImpl.java:2384)
  at android.app.ContextImpl.enforceCallingOrSelfPermission(ContextImpl.java:2412)
  at com.android.server.pm.permission.PermissionManagerServiceImpl.grantRuntimePermissionInternal(PermissionManagerServiceImpl.java:1383)
  at com.android.server.pm.permission.PermissionManagerServiceImpl.grantRuntimePermission(PermissionManagerServiceImpl.java:1365)
  at com.android.server.pm.permission.PermissionManagerService.grantRuntimePermission(PermissionManagerService.java:573)
  at android.permission.PermissionManager.grantRuntimePermission(PermissionManager.java:610)
  at com.android.server.pm.PackageManagerShellCommand.runGrantRevokePermission(PackageManagerShellCommand.java:2717)
  at com.android.server.pm.PackageManagerShellCommand.onCommand(PackageManagerShellCommand.java:301)
  at com.android.modules.utils.BasicShellCommandHandler.exec(BasicShellCommandHandler.java:97)
  at android.os.ShellCommand.exec(ShellCommand.java:38)
  at com.android.server.pm.PackageManagerService$IPackageManagerImpl.onShellCommand(PackageManagerService.java:6840)
  at android.os.Binder.shellCommand(Binder.java:1092)
  at android.os.Binder.onTransact(Binder.java:912)
  at android.content.pm.IPackageManager$Stub.onTransact(IPackageManager.java:4352)
  at com.android.server.pm.PackageManagerService$IPackageManagerImpl.onTransact(PackageManagerService.java:6824)
  at android.os.Binder.execTransactInternal(Binder.java:1392)
  at android.os.Binder.execTransact(Binder.java:1299)
```

下面参考 [shizuku 文档](https://shizuku.rikka.app/guide/setup/#faq) 的解决方案

- HyperOS/MIUI（小米、POCO）\
  在 `开发者选项` 中开启 `USB 调试（安全设置）`，注意不是 `USB 调试`\
  如果开启 `USB 调试（安全设置）`后继续报错，可以尝试在 adb 命令前添加 sudo \
  提升权限后再执行 adb 命令

- ColorOS（OPPO & OnePlus）\
  在 `开发者选项` 中关闭 `权限监控` 或打开 `禁止权限监控`

- Flyme（魅族）\
  在 `开发者选项` 中关闭 `Flyme 支付保护`

关闭限制后重新运行命令即可

---

## File: docs/guide/index.md

# 开始使用 {#title}

## 安装 {#install}

<GkdOnly>

> [!TIP] 下载提示
> 检测到您在应用内无法下载 APK，请点击右上角-外部打开后下载

</GkdOnly>

<ApkTable />

::: details 手机浏览器下载失败?
某些手机浏览器存在自定义拦截会导致下载失败, 建议使用 [Via](https://viayoo.com) 浏览器下载
:::

其它下载方式

- [Google Play](https://play.google.com/store/apps/details?id=li.songe.gkd) &nbsp; [区别](./faq.md#play-diff)
- [Github Releases](https://github.com/gkd-kit/gkd/releases)

下载完毕后, 在您的 Android 手机上安装并打开

## 使用 {#usage}

打开后进入 GKD 的首页, 要想 GKD 正常工作需要授权无障碍权限

点击无障碍权限右侧的 `授权` 按钮即可前往设置的无障碍界面授权

以 Xiaomi HyperOS 为例, 下面为完整的授权流程截图



如遇到问题可在 [常见问题](/guide/faq) 查阅解决方案

---

## File: docs/guide/node.md

# 属性方法 {#title}

此章节介绍各个类型可用的属性及其方法

## null

> [!NOTE] 提示
> `null` 无任何属性方法, 并且在无特殊说明时, 属性方法当调用者或者参数是 `null` 时, 均返回 `null`

当 `name` 是 `null` 时, `name.length` 为 `null`

当 `parent` 是 `null` 时, `parent.getChild(0)` 为 `null`

当 `index` 是 `null` 时, `parent.getChild(index)` 为 `null`

## boolean

| 方法名 | 参数      | 返回类型  | 描述                                                    |
| ------ | --------- | --------- | ------------------------------------------------------- |
| toInt  |           | `int`     | 转为 0 或 1                                             |
| or     | `boolean` | `boolean` | a \|\| b                                                |
| and    | `boolean` | `boolean` | a && b                                                  |
| not    |           | `boolean` | !a                                                      |
| ifElse | `T`, `T`  | `T`       | if(a) T else T  T 是任意类型, 两个参数均支持为 null |

## int

| 方法名    | 参数  | 返回类型              | 描述                 |
| --------- | ----- | --------------------- | -------------------- |
| toString  |       | [`string`](#string)   | 转为10进制字符串     |
| toString  | `int` | `string`              | 转为对应进制的字符串 |
| plus      | `int` | `string`              | 加上                 |
| minus     | `int` | `string`              | 减去                 |
| times     | `int` | `string`              | 乘以                 |
| div       | `int` | `string`              | 除以                 |
| rem       | `int` | `string`              | 取余                 |
| more      | `int` | [`boolean`](#boolean) | a > b                |
| moreEqual | `int` | `boolean`             | a >= b               |
| less      | `int` | `boolean`             | a < b                |
| lessEqual | `int` | `boolean`             | a <= b               |

## string

| 标识符 | 属性类型      | 描述       |
| ------ | ------------- | ---------- |
| length | [`int`](#int) | 字符串长度 |

| 方法名    | 参数           | 返回类型 | 描述                                  |
| --------- | -------------- | -------- | ------------------------------------- |
| get       | [`int`](#int)  | `string` | 获取对应索引字符串                    |
| at        | `int`          | `string` | 同上,但是参数传负数时从最后一个字符取 |
| substring | `int`          | `string` | 截取指定索引到结尾字符串              |
| substring | `int`,`int`    | `string` | 截取指定间隔字符串                    |
| toInt     |                | `int`    | 转为10进制数字                        |
| toInt     | `int`          | `int`    | 转为指定进制的数字                    |
| indexOf   | `string`       | `int`    | 查找指定字符串的索引                  |
| indexOf   | `string`,`int` | `int`    | 从指定索引开始查找指定字符串的索引    |

## node

> [!IMPORTANT] 重要提示
> 此类型下以 \_ 开头的属性只能在网页快照调试工具上使用, 真机不可用

| 标识符        | 属性类型            | 描述       |
| ------------- | ------------------- | ---------- |
| \_id          | [`int`](#int)       |            |
| \_pid         | `int`               |            |
| id            | [`string`](#string) |            |
| vid           | `string`            |            |
| name          | `string`            |            |
| text          | `string`            |            |
| desc          | `string`            |            |
| clickable     | `boolean`           |            |
| focusable     | `boolean`           |            |
| checkable     | `boolean`           |            |
| checked       | `boolean`           |            |
| editable      | `boolean`           |            |
| longClickable | `boolean`           |            |
| visibleToUser | `boolean`           |            |
| left          | `int`               |            |
| top           | `int`               |            |
| right         | `int`               |            |
| bottom        | `int`               |            |
| width         | `int`               |            |
| height        | `int`               |            |
| childCount    | `int`               |            |
| index         | `int`               |            |
| depth         | `int`               |            |
| parent        | [`node`](#node)     | 获取父节点 |

| 方法名   | 参数  | 返回类型        | 描述                 |
| -------- | ----- | --------------- | -------------------- |
| getChild | `int` | [`node`](#node) | 获取指定索引的子节点 |

## context

context 具有 [node](#node) 的所有属性方法, 下面只介绍额外属性方法

| 标识符  | 属性类型              | 描述                                               |
| ------- | --------------------- | -------------------------------------------------- |
| prev    | [`context`](#context) | 右侧属性选择器的节点上下文  最右侧的 prev=null |
| current | [`node`](#node)       | 当前节点  current.id=id                        |

示例

```text
FrameLayout[prev.name$='ImageView'] > ImageView[current.name=name]
```

中的 prev 就是 FrameLayout 右侧的 ImageView, current 就是 ImageView

| 方法名  | 参数 | 返回类型              | 描述                                                                                                     |
| ------- | ---- | --------------------- | -------------------------------------------------------------------------------------------------------- |
| getPrev | int  | [`context`](#context) | 快捷获取深层 prev  getPrev(0) -> prev  getPrev(1) -> prev.prev  getPrev(2) -> prev.prev.prev |

## global

global 具有 [context](#context) 的所有属性方法, 并且没有任何属性方法能获取 global 的引用, 下面只介绍额外属性方法

| 方法名   | 参数     | 返回类型              | 描述                                  |
| -------- | -------- | --------------------- | ------------------------------------- |
| equal    | `T`, `T` | [`boolean`](#boolean) | a==b  T 是任意类型, a,b 允许 null |
| notEqual | `T`, `T` | `boolean`             | a!=b                                  |

示例

```text
[name=''][prev.name=''][current.name=''][parent.name='']
```

- 第一个 `name` 来自 [global](#global), 实际上任何 `xxx`/`xxx()` 都来自 global
- 第二个 `prev.name` 来自 [context](#context)
- 后续的 `current.name`/`parent.name` 都来自 [node](#node)

---

## File: docs/guide/optimize.md

# 查询优化 {#title}

一些关于查询优化的说明

## 查找根节点 {#query-root}

选择器通过满足下列格式的属性选择器标识根节点

- `[parent=null]`
- `A[id='xxx'][parent=null][text='xxx']`

即只需存在一个 `[parent=null]` 就满足条件, 以下是对此的优化

### 主动查询 {#enforce}

一般的选择器如 `LinearLayout > TextView`, 选择器需要从 根节点/事件节点 使用深度先序顺序遍历子孙节点并判断是否满足条件

如果屏幕上有 n 个节点, 判断逻辑需要运行 n 次

但是如果选择器是类似 `TextView <3 LinearLayout <2 [parent=null]`, 由于在右侧标明了从根节点查找

因此上面的 `深度先序顺序遍历子孙节点` 将省略, 只进行一次判断, 不需要判断 n 次

但是如果你在子查询里使用 `<<n`, 例如 `TextView <<n [parent=null]`

根据上面说明的优化, 虽然也只有一次判断, 但是由于 `<<n` 导致内部的子判断也是 n 次, 实际上没有优化判断次数

> [!TIP] 提示
> 实际上从根节点开始匹配并且目标不是根节点的选择器如 `A > B` 都可等价为 `A > @B <<n [parent=null]`

### 祖先节点 {#ancestor}

在示例 `A <3 [parent=null] >n TextView` 中, 选择器找到 `TextView` 后, 根据 `>n` 表示任意祖先节点

在默认情况下, 如果 `TextView` 的层级很深, 那必然需要多次调用 获取父节点 才能最终匹配

但是如果左侧的选择器是 `[parent=null] >n`, 这表示找到根节点, 选择器此时不会去执行多次调用 获取父节点

在 GKD 内部, 根节点可以直接通过 API 获取, 因此 `[parent=null] >n` 只需要调用一次方法

## 快速查询 {#fast-query}

> [!NOTE] 注意
> 此优化需要设置 [fastQuery](/api/interfaces/RawCommonProps#fastquery) 来启用\
> 并且节点在快照面板被标识 `可快速查找`, 否则查找不到

### 查询场景 {#query-scene}

一般情况下, 选择器 `[vid="name"]` 需要从 根节点/事件节点 使用深度先序顺序遍历子孙节点并判断是否满足条件

但是 Android 提供了如下两个快速获取节点的 Api

- [findAccessibilityNodeInfosByViewId](https://developer.android.google.cn/reference/android/view/accessibility/AccessibilityNodeInfo#findAccessibilityNodeInfosByViewId(java.lang.String))
- [findAccessibilityNodeInfosByText](https://developer.android.google.cn/reference/android/view/accessibility/AccessibilityNodeInfo#findAccessibilityNodeInfosByText(java.lang.String))

这使得可以通过 `id`/`vid`/`text` 直接获取子孙节点

对此我们需要规定符合特定条件的选择器来调用这些 Api 从而跳过手动遍历子孙节点

### 规定格式 {#rule-format}

所有 `末尾属性选择器`的`第一个属性选择表达式`符合下面的结构之一

- `[id='abc']`
- `[vid='abc']`
- `[text='abc']`
- `[text^='abc']`
- `[text*='abc']`
- `[text$='abc']`

或者使用 `||` 将它们连接形成的逻辑表达式也符合条件, 即如下格式

- `[id='abc' || id='abc2']`
- `[id='abc' || vid='abc' || text='abc' || text^='abc' || text*='abc' || text$='abc']`

这些规定的格式都是为了提取 `id`/`vid`/`text` 来调用上面的两个 Android Api

下面给出实际示例: ✅ 表示符合格式, ❎ 表示不符合格式

- `A > B + C[id='x'][childCount=2]` ✅
- `A > B + C[childCount=2][id='x']` ❎
- `A > B + C[id='x' || text='manbaout' || text*='ikun'][childCount=2]` ✅
- `A > B + C[childCount=2][id='x' || text='manbaout' || text*='ikun']` ❎

这样一个选择器只能在右侧使用快速查询, 为了在中间的子选择器也能使用

额外规定如果属性选择器如果符合上面格式并且右侧是 `<<n`, 也能在局部使用快速查找

示例 `A > B + C[id='x'][childCount=2] <<n D` 中的 `C[id='x'][childCount=2] <<n` 可以使用局部快速查找

下面给出满足局部查询优化的示例: ✅ 表示符合格式, ❎ 表示不符合格式

- `A > B + C[id='x'][childCount=2] <<n D` ✅
- `A > B + C[childCount=2][id='x'] <<n D` ❎

上面介绍的是只有一个局部选择器的情况, 下面给出多个局部快速查找的的示例

如 `A > C[id='x'] <<n D[id='y'] <<n E`, 其中的 `C[id='x'] <<n` 和 `D[id='y'] <<n` 都可以使用局部快速查找

如果使用 `||`, `&&` 连接多个选择器时, 快速查询为它们的并集

如果选择器被 `!(xxx)` 包裹, 则不符合快速查询格式, 也就是 `!` 会取消其内部所有的快速查询

- `(A + B[id='b']) || (C + D[id='d'])` 的快速查询为 `id='b'`, `id='d'`
- `(A + B[id='b']) && (C + D[id='d'])` 的快速查询为 `id='b'`, `id='d'`
- `(A + B[id='b']) && !(C + D[id='d'])` 的快速查询为 `id='b'`

需要注意的是它们内部的 局部快速查找 仍然独立成立

- `(X[id='x'] <<n A + B[id='b']) && !(Y[id='y'] <<n C + D[id='d'])` 的快速查询为 `id='b'`, 局部快速查询分别是 `id='x'`, `id='y'`

> [!NOTE] 注意
> 如果选择器不存在满足快速查找的格式, fastQuery 是否开启都不影响查询复杂度

### 优化示例 {#optimize-example}

以 [`[vid="image"] <<n [vid="recyclerView"] <<n [vid="content_layout"]`](https://i.gkd.li/i/16201605?gkd=W3ZpZD0iaW1hZ2UiXSA8PG4gW3ZpZD0icmVjeWNsZXJWaWV3Il0gPDxuIFt2aWQ9ImNvbnRlbnRfbGF5b3V0Il0) 为例

上面的选择器存在 3 个快速查找优化

- `[vid="content_layout"]` 从 根节点 优化查询找到 `content_layout`
- `[vid="recyclerView"]` 从 `content_layout` 优化查询找到 `recyclerView`
- `[vid="image"]` 从 `recyclerView` 优化查询找到 `image`

也就是只需要调用 `findAccessibilityNodeInfosByViewId` 3 次即可得到目标节点

如果是从根节点开始查询, 根据快照内的 选择器路径视图

- `[vid="content_layout"]` 从 根节点 需要 7 次找到 `content_layout`
- `[vid="recyclerView"]` 从 `content_layout` 需要 48 次找到 `recyclerView`
- `[vid="image"]` 从 `recyclerView` 需要 44 次找到 `image`

---

## File: docs/guide/privacy.md

# 隐私政策 {#title}

**版本号：1.0**  
**生效日期：以应用版本发布日期为准**

本隐私政策旨在说明我们如何处理您的数据。

GKD 应用秉持 “本地优先、最小权限、完全离线” 原则。

**我们不会收集、存储或上传任何用户隐私数据**。

## 一、我们如何处理您的数据 {#p1}

| 数据类型              | 使用目的                 | 是否上传 | 存储位置       |
| --------------------- | ------------------------ | -------- | -------------- |
| 本地规则与配置        | 用户自定义自动化行为     | 否       | 本地           |
| 屏幕截图（仅调试）    | 用户调试辅助功能         | 否       | 本地           |
| 屏幕文本信息          | 文本识别以实现查询功能   | 否       | 本地           |
| 本地日志              | 保存运行或报错信息供调试 | 否       | 本地           |
| 数据导出（配置/规则） | 用户自行备份与迁移       | 否       | 本地或自行分享 |

## 二、权限使用说明 {#p2}

| 权限           | 是否必须 | 用途说明                                 |
| -------------- | -------- | ---------------------------------------- |
| 无障碍服务     | ❌ 否    | 用于模拟点击和界面读取（选用功能）       |
| 屏幕截图权限   | ❌ 否    | 调试界面内容（选用功能）                 |
| 网络权限       | ❌ 否    | 检测更新或导入订阅或网络调试（选用功能） |
| 外部存储写权限 | ❌ 否    | 部分旧设备导出文件时使用（选用功能）     |

## 三、数据导入与导出说明 {#p3}

- 所有规则、配置、日志等数据仅保存在用户本地；
- 用户可通过系统分享功能导出文件，无需申请权限；
- 部分旧设备或旧系统中，普通版导出文件可能会申请写入下载目录权限，且用途仅限导出功能。

## 四、第三方库说明 {#p4}

本应用所使用的第三方库均为开源项目，不包含任何用户跟踪、统计或联网行为。完整依赖列表可在项目开源仓库中查看。

## 五、用户权利 {#p5}

您有权：

- 拒绝授予任何权限
- 随时导出、编辑或删除本地数据
- 卸载应用以终止使用

## 六、隐私政策变更 {#p6}

如隐私政策有更新，将随应用更新发布。您可在官网或应用内查阅最新版，继续使用即视为接受变更。

---

## File: docs/guide/selector.md

# 选择器 {#title}

一个类似 CSS 选择器的选择器, 能联系节点上下文信息, 更容易也更精确找到目标节点

## 为什么需要选择器 {#why}

我们的目的是查询获取屏幕上的某个节点, 最简单的情况下, 目标节点已经有唯一的 id/text 标识

这时只需要配置 id/text 就能简单执行点击, 但实际情况很多节点都缺失了这些信息, 无法简单查询获取

节点还存在很多额外信息没有被利用到, 节点所有属性可以在 [快照-14045424](https://i.gkd.li/i/14045424) 的属性面板看到

另一种常见的情况是节点没有有效属性, 这时我们需要根据周围的节点去定位

选择器就是为了解决这些问题而开发的, 下面是对选择器语法的详细介绍

> [!TIP] 提示
> 本文是从 **抽象语法树(AST)** 的角度去完备地介绍选择器结构\
> 如果你是初学者, 建议你直接看 [选择器-示例](./example.md)

## 语法 {#syntax}

与 CSS 类似, 一个选择器由 属性选择器 和 关系选择器 交叉组成, 并且开头末尾必须是 属性选择器

示例 `div > img` 的结构是 `属性选择器 关系选择器 属性选择器`, 它表示选择父节点是 `div` 的 `img` 节点, 这与 [相同 CSS 语法](https://developer.mozilla.org/zh-CN/docs/Web/CSS/Child_combinator) 语义一致

另外 属性选择器 和 关系选择器 之间必须**强制**用 空白字符(空格/换行/回车/制表) 隔开

即 `div>img` 非法, 必须写成 `div > img`

下面分别介绍 [属性选择器](#attr) 和 [关系选择器](#connect)

## 属性选择 {#attr}

它和 CSS 语法的 属性选择器很相似, 但更强大, 如下是一个示例

`@TextView[a=1][b^='2'][c*='a'||d.length>7&&e=false][!(f=true)][g.plus(1)>0]`

`@` 表示选择此节点, 一条规则最后属性选择器 `@` 生效, 如果没有 `@`, 取最后一个属性选择器

`TextView` 代表节点的 name 属性, 而且与 CSS 相似, `*` 表示匹配任意属性

由于该选择器主要用于 Android 平台, 节点的 name 都是 java 类如 android.text.TextView 这种形式

为了方便书写规则, 上述 `TextView` 等价 `[name='TextView'||name$='.TextView']`

`[]` 内部是一个 逻辑表达式/布尔表达式/取反表达式

- 逻辑表达式 -> `name='TextView'||name$='.TextView'`
- 布尔表达式 -> `name='TextView'`
- 取反表达式 -> `!(name$='TextView')`, `!(name='TextView'||name$='.TextView')`

注意 取反表达式中的 `!` 后面必须是 `(...)`, `!!(...)` 是非法的

逻辑表达式 有两个操作符 `||` 和 `&&`. `&&` 优先级更高

即 `[a>1||b>1&&c>1||d>1]` 等价于 `[a>1||(b>1&&c>1)||d>1]`

并列的 `[]` 视为使用 `&&` 的逻辑表达式, 即 `[a=1][b=1]` 等价于 `[a=1&&b=1]`

布尔表达式 由 `左值 操作符 右值` 构成, 左值/右值 是一个 值表达式

下面分别介绍 [值表达式](#value-exp) 和 [操作符](#attr-operator)

## 值表达式 {#value-exp}

值表达式分两类: 变量 和 字面量, 下图是关系表格



| 值表达式 | 示例 |  |
| --- | --- | --- |
| 变量 | 标识符 | ` a ` |
| 成员表达式 | `a.b` |  |
| 调用表达式 | `a(b,c)` |  |
| 字面量 | null | `null` |
| boolean | `false` |  |
| int | `114514` |  |
| string | `'ikun'` |  |



每个值表达式都有其类型, 可细分为两类

基础类型: `null` `boolean` `int` `string`

对象类型: `object`

对象类型可细分为 `context` 和 `node` 两种类型

比如选择器 `[parent=null]` 代表选择一个父节点是 null 的节点, 即根节点

上面的 `parent` 属于 值表达式/变量/标识符, 类型是 `node`

`context` 类型指代当前节点的上下文, 当想使用不属于 `node` 上的属性方法时就需要 `context`

你可以在 [选择器-属性方法](./node.md) 查看所有类型和对应属性方法

### 变量 {#var}

首先需要了解 **标识符**: 正则匹配 `^[_a-zA-Z][a-zA-Z0-9_]*$` 并且不是 `null`/`true`/`false` 的字符串

示例合法变量名: `a` `ikun` `manbaout`, 下面可输入字符测试否是合法

::: raw

:::

---

接下来了解 **成员表达式** `a.b`, 它被 `.` 分为两个部分, 前部分是另一个变量, 后部分是一个标识符作为属性

同理 `a.b.c` 也是一个 成员表达式, 其中 `a.b` 是它的变量部分, `c` 是一个合法的标识符作为属性

根据上面标识符的规则, `a.1`, `a.null`, `a.true` 都是非法成员表达式

---

最后了解 **调用表达式** `a(b,c)`, 它由两个部分构成, `(` 的左侧 `a` 是一个变量作为 调用者

`(b,c)` 作为调用参数(值类型), 调用参数数量可以是 0 或任意个, 即 `a()` 也是合法的

合法的其它例子: `a.b(c,d).e(f).g(1,2,true)`

需要注意调用者不能是 调用表达式, 即 `a()()` 非法

### 字面量 {#literal}

根据上面的表格, 字面量有 4 种: `null`, `boolean`, `int`, `string`

- null
- boolean 使用 `true`/`false`
- int 匹配 `^-?[0-9]$`, 即十进制整数, 示例 `-1`,`0`,`1`, 不支持 `+1` 这种写法
- string 使用 ' &#96; " 之一成对包裹, 内部字符转义使用 `\`\
   所有的转义字符示例 `\\`, `\'`, `\"`, `` \` ``, `\n`, `\r`, `\t`, `\b`, `\xfF`, `\uffFF`\
   不支持多行字符, 处于 `[0, 0x1F]` 的控制字符必须使用转义字符表示

此外使用 string 时需要了解 [嵌套转义字符](#nest-escape) 以避免出现错误

## 操作符 {#attr-operator}

操作符 用于连接两个 值表达式

| 操作符 |     名称      |     说明      |
| :----: | :-----------: | :-----------: |
|   =    |     equal     |     等于      |
|   !=   |   notEqual    |    不等于     |
|   >    |     more      |     大于      |
|   >=   |   moreEqual   |  大于或等于   |
|   <    |     less      |     小于      |
|   <=   |   lessEqual   |  小于或等于   |
|   ^=   |  startsWith   |   以...开头   |
|  !^=   | notStartsWith |  不以...开头  |
|  \*=   |   contains    |    包含...    |
|  !\*=  |  notContains  |   不包含...   |
|   $=   |   endsWith    |   以...结尾   |
|  !$=   |  notEndsWith  |  不以...结尾  |
|   ~=   |    matches    |  正则匹配...  |
|  !~=   |  notMatches   | 正则不匹配... |

附加说明: `matches`/`notMatches` 要求 值 必须是合法的 [Java/Kotlin 正则表达式](https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html), 否则提示语法错误

并且 `matches`/`notMatches` 右侧的值只能是 字面量/字符串

操作符只能使用在对应的类型的值, 比如 `a>''` 类型不匹配, 将提示 `非法类型`

下面表格中 `-` 表示类型不匹配

|      |   null   | boolean  |   int    |  string  |
| :--: | :------: | :------: | :------: | :------: |
|  =   | &#10004; | &#10004; | &#10004; | &#10004; |
|  !=  | &#10004; | &#10004; | &#10004; | &#10004; |
|  >   |    -     |    -     | &#10004; |    -     |
|  >=  |    -     |    -     | &#10004; |    -     |
|  <   |    -     |    -     | &#10004; |    -     |
|  <=  |    -     |    -     | &#10004; |    -     |
|  ^=  |    -     |    -     |    -     | &#10004; |
| !^=  |    -     |    -     |    -     | &#10004; |
| \*=  |    -     |    -     |    -     | &#10004; |
| !\*= |    -     |    -     |    -     | &#10004; |
|  $=  |    -     |    -     |    -     | &#10004; |
| !$=  |    -     |    -     |    -     | &#10004; |
|  ~=  |    -     |    -     |    -     | &#10004; |
| !~=  |    -     |    -     |    -     | &#10004; |

除 `=`/`!=` 以外的操作符, 当节点属性是 null 时表达式为 `false`

- a > 233
- a >= 233
- a < 233
- a <= 233
- a ^= 'xxx'
- a !^= 'xxx'
- a \*= 'xxx'
- a !\* 'xxx'
- a $= 'xxx'
- a !$= 'xxx'
- a ~= 'xxx'
- a !~= 'xxx'

即当 a 是 `null` 时以上表达式为 `false`

你可能会对 `a !$= 'xxx'` 在 a 是 `null` 表达式为 `false` 感到奇怪

那你可以换一种写法使用 `a=null || a!$='xxx'` 或 `!(a$='xxx')`

## 关系选择 {#connect}

关系选择器 由 关系操作符 和 关系表达式 构成, 用于连接两个属性选择器

简单示例: `div > a`, 它 表示/约束 两个节点之间的关系, 下面介绍 [关系操作符](#connect-operator) 和 [关系表达式](#connect-exp)

### 关系表达式 {#connect-exp}

关系表达式 表示查找节点的范围, 有两种

- 元组表达式 `(a1,a2,a3,a_n)`, 其中 a1, a2, a3, a_n 是常量有序递增正整数, 示例 `(1)`, `(2,3,5)`
- 多项式表达式 `(an+b)`, 其中 a 和 b 是常量整数, 它是元组表达式的另一种表示, 这个元组的数满足集合 `{an+b|an+b>=1,n>=1}` 如果集合为空集则表达式非法\
  当 a<=0 时, 它具有等价的元组表达式\
  示例 `(-n+4)` 等价于 `(1,2,3)`\
  示例 `(-3n+10)` 等价于 `(1,4,7)`\
  当 a>0 时, 它表示无限的元组表达式\
  示例 `(n)`, 它表示 `(1,2,3,...)` 一个无限的元组\
  示例 `(2n-1)`, 它表示 `(1,3,5,...)` 一个无限的元组

### 关系操作符 {#connect-operator}

关系操作符 表示查找节点的方向, 有 6 种关系操作符

- 前 5 种为 `+`, `-`, `>`, `<`, `<<`, 表示节点的相对位置

<NImageGroup>

| 操作符 |        名称        |              图例               |                                  选择器                                   |
| :----: | :----------------: | :-----------------------------: | :-----------------------------------------------------------------------: |
|   +    |    前置兄弟节点    | <GImg src="0020.png" w-250px /> |   [\* + \[\_id=33\]](https://i.gkd.li/i/14045424?gkd=KiArIFtfaWQ9MzNd)    |
|   -    |    后置兄弟节点    | <GImg src="0021.png" w-250px /> |   [\* - \[\_id=32\]](https://i.gkd.li/i/14045424?gkd=KiAtIFtfaWQ9MzJd)    |
|   >    |      祖先节点      | <GImg src="0022.png" w-250px /> |   [\* > \[\_id=90\]](https://i.gkd.li/i/14045424?gkd=KiA-IFtfaWQ9OTBd)    |
|   <    |     直接子节点     | <GImg src="0023.png" w-250px /> |   [\* < \[\_id=89\]](https://i.gkd.li/i/14045424?gkd=KiA8IFtfaWQ9ODld)    |
|   <<   | 子孙节点(深度先序) | <GImg src="0024.png" w-250px /> | [\* <<2 \[\_id=29\]](https://i.gkd.li/i/14045424?gkd=KiA8PDIgW19pZD0yOV0) |

</NImageGroup>

- 另外一种为 `->`, 用于查询过程中, 表示当前节点的之前节点

比如 `C ->1 B > A`, 由 A 找到 B 后, 此时的 C 就是 B 的之前节点即 A, 这适合想快速去之前节点重新查询关系的场景

将 关系操作符 和 关系表达式 连接起来就得到了 关系选择器

`A +(a1,a2,a3,a_n) B` : A 是 B 的前置兄弟节点, 并且 A.index 满足 B.index-(a_m), 其中 a_m 是元组的任意一个数字

`A -(a1,a2,a3,a_n) B` : A 是 B 的后置兄弟节点, 并且 A.index 满足 B.index+(a_m)

`A >(a1,a2,a3,a_n) B` : A 是 B 的祖先节点, 并且 A.depth 满足 B.depth-(a_m), 根节点的 depth=0

`A <(a1,a2,a3,a_n) B` : A 是 B 的直接子节点, 并且 A.index 满足 a_m-1

`A <<(a1,a2,a3,a_n) B` : A 是 B 的子孙节点, 并且 A.order 满足 a_m-1, A.order 是深度优先先序遍历的索引

`N ->(a1,a2,a3,a_n) B > A`, N 是 B 的之前(右侧)的已查询节点, 并且 N.index 满足 B.index-(a_m), index 顺序为 (n,n-1,...,2,1,0)

### 表达式简写 {#connect-shortcut}

一般情况下, 并不需要写严格完整的表达式, 使用简化写法更方便快捷

下面是一些特殊情况下的简写表示

当 a=0 或 b=0 时, 括号可以省略, 以 `+` 为例

- `A +(3n+0) B` -> `A +(3n) B` -> `A +3n B`
- `A +(0n+3) B` -> `A +(+3) B` -> `A +3 B`

当 a=0 且 b=1 时, an+b 可以省略, 以 `+` 为例

- `A +(0n+1) B` -> `A + B`

此外 `A + B`,`A > B` 都与等价的 CSS 语法语义相同

当 a=1 且 b=0 且操作符是 `>`, 可以进一步简写, 比如

- `A >(1n+0) B` -> `A >n B` -> `A B`

这与等价的 CSS 语法语义相同

## 进阶 {#advanced}

正如上述的 [属性选择](#attr) 一样, 选择器也可以组合为 逻辑表达式/取反表达式

只有一个格式要求: 单个选择器必须强制使用 `()` 包裹

- 一般选择器 -> `A + B` 或 `(A + B)`

- 逻辑或表达式 -> `(A + B) || (M > N)`, 表示匹配节点满足 `A + B` 或 `M > N`

  匹配结果遵循 `||` 的短路原则, 优先判断左侧的 `A + B`, 如果满足 `A + B` 那就是 `B`. 不满足 `A + B` 时, 判断右侧表达式, 如果满足 `M > N` 那就是 `N`, 当然也可以在内部使用 `@` 自定义目标节点

- 逻辑且表达式 -> `(A + B) && (M > N)`, 表示匹配节点满足 `A + B` 和 `M > N`, 结果取右侧表达式结果, 结果为 `N`, 当然也可以在内部使用 `@` 自定义目标节点

- 取反表达式 -> `!(A + B)`, 表示匹配节点应不满足 `A + B`, 需要注意的是 `!(@A + B)` 中的 `@` 是无效的, 它的匹配结果只能是末尾节点 `B`

此外 逻辑且表达式 的优先级更高, 即 `(A + B) || (C + B) && (M > N)` 等价于 `(A + B) || ((C + B) && (M > N))`

## 示例 {#example}

```txt
@LinearLayout > TextView[id=`com.byted.pangle:id/tt_item_tv`][text=`不感兴趣`]
```

选择器从末尾开始查找, 首先找到 id=&#96;com.byted.pangle:id/tt_item_tv&#96; 和 text=&#96;不感兴趣&#96; 的 TextView, 并且父节点是 LinearLayout 的节点

此时我们得到两个节点 [LinearLayout, TextView] 根据 `@` 知道目标节点是 LinearLayout

实际上它与

```txt
TextView[id=`com.byted.pangle:id/tt_item_tv`][text=`不感兴趣`] <n LinearLayout
```

的目标匹配节点是等价的, 但是在查询算法时间复杂度上, 后者更慢

如下是网页无障碍快照审查工具, 使用它的搜索框的选择器查询可以实时测试编写的选择器

- [快照-14045424](https://i.gkd.li/i/14045424)
- [快照-14034770](https://i.gkd.li/i/14034770)
- [快照-14031920](https://i.gkd.li/i/14031920)
- [快照-14018243](https://i.gkd.li/i/14018243)
- [快照-14011298](https://i.gkd.li/i/14011298)
- [快照-13999908](https://i.gkd.li/i/13999908)

## 一些注意 {#note}

### 匹配顺序 {#match-order}

选择器的匹配顺序是 **从右往左匹配**

例如 `FrameLayout > TextView`, 它是先从 根节点/事件节点 找到 `TextView`, 然后再判断 parent 是不是 `FrameLayout`

并不是先找到 `FrameLayout` 然后再判断 child 是否有 `TextView`

同时在 [选择器路径视图](https://i.gkd.li/i/15747758?gkd=RnJhbWVMYXlvdXQgPiBUZXh0Vmlldw) 也会标明匹配顺序和查找方向

### 正则表达式优化 {#regex-optimization}

对 `matches`/`notMatches` 的优化: 如果正则表达式满足下面的条件, 选择器将使用内置的简单的函数匹配, 而不是真正地去运行一个正则表达式

- `[text~="(?is)abc.*"]` -> `startsWith('abc', ignoreCase = true)`
- `[text~="(?is).*abc.*"]` -> `contains('abc', ignoreCase = true)`
- `[text~="(?is).*abc"]` -> `endsWith('abc', ignoreCase = true)`
- `[text!~="(?is)abc.*"]` -> `notStartsWith('abc', ignoreCase = true)`
- `[text!~="(?is).*abc.*"]` -> `notContains('abc', ignoreCase = true)`
- `[text!~="(?is).*abc"]` -> `notEndsWith('abc', ignoreCase = true)`

上面的 `abc` 指代不包含 `\^$.?*|+()[]{}` 这类特殊正则字符的任意字符串, 如 `ikun` 符合, `ikun?` 不符合, `ignoreCase = true` 表示忽略大小写

简单来说就是如果你只想忽略大小写去简单匹配或不匹配一些字符串, 那么直接使用上面的格式

### 正则表达式一致性 {#regex-multiplatform}

由于 选择器 需要同时满足 浏览器/Js(审查工具), Android/Java(GKD) 运行, 而这两个平台的正则表达式的底层实现和语法表示略有不同

因此为了在 Js 端实现和 Java 一致的正则表达式规范, 网页审查工具借助 [Kotlin Wasm](https://kotlinlang.org/docs/wasm-overview.html) 将正则表达式的 matches 函数接口编译为 wasm 提供给 Js 调用

Kotlin Wasm 需要你的浏览器支持 [WasmGC](https://developer.chrome.com/blog/wasmgc?hl=zh-cn), 也就是版本需要满足下列条件

<GImg src="0025.png" />

如果你在 nodejs 使用 gkd 选择器, 需要 node>=22 版本

如果你的 浏览器/nodejs 版本不满足, 正则表达式将自动回退到 Js 端实现, 以下是在 Js 端使用正则表达式需要注意的地方

比如上面的正则表达式优化例子中开头的 `(?is)` 是 Java 正则表达式的 inline flags 语法, 但实际上 Js 并不支持这样写, 只是选择器内部做了一些兼容让它支持

并且选择器的 Js 端只兼容在开头的 flags, 在内部的 flags 不支持, 此外 Java 和 Js 支持的 flags 也有不同, 某些特殊的表达式表现也不一致

总之不要使用太过复杂(多复杂我也不知道)的正则表达式, 某些正则表达式有可能在审查工具上匹配, 但是在 GKD 上不匹配

如果你能确保正则表达式在 Js 和 Java/Kotlin 的匹配行为一致, 那就没问题

总之两种情况

- 高版本 浏览器/nodejs 可以使用 **完整一致** 的正则表达式
- 低版本 浏览器/nodejs 使用 **小范围不完整不一致** 的正则表达式

如果你的选择器没有使用正则表达式则不用关心此问题

### 嵌套转义字符 {#nest-escape}

转义字符一般用于表示不能直接显示具体特殊意义的字符

对于 GKD 的选择器而言, 转义字符使用示例如下

```txt
TextView[text="\\\n"]
```

这表示选择一个 TextView, 它的 text 长度为 2, 开头是 `\`, 末尾是换行

这和平常写的代码里面的 string 声明用法一模一样

但是选择器最终放在规则的字段里面, 也就是这个选择器字符串处于 JSON 的 string 内

如果你想当然地直接手动复制插入到编辑器里出现下面这种规则

```json5
{
  matches: 'TextView[text="\\\n"]',
}
```

由于 JSON 也有自己的转义, 规则与 GKD 的转义基本一致, 因此你的实际选择器实际变成了

```txt
TextView[text="\
"]
```

这明显是一个非法的选择器

解决方式也很简单, 手动插入的时候将你的 `\` 变成 `\\`, 正确的规则如下

```json5
{
  matches: 'TextView[text="\\\\\\n"]',
}
```

这是普通字符串的情况, 如果你使用 `~=`/`!~=`, 则还需要多转义一次, 因为正则表达式也有自己的转义字符

比如我们想匹配一个 TextView 并且 text 是任意一个数字, 我们想当然地用 `\d`, 所以你可能会写

```txt
TextView[text~="\d"]
```

但是你在网页审查工具输入并回车搜索会提示 非法选择器, 这是因为 `\d` 处于 GKD 的 string 内, 会被转义一次, 正确的用法是写 `\\d`

```txt
TextView[text~="\\d"]
```

实际上这部分等价的 Kotlin/Java 代码可以简单是

```txt
Regex("\\d").matches(text)
```

接下来把选择器放到规则里面, 根据上面的描述, 规则应该是

```json5
{
  matches: 'TextView[text~="\\\\d"]',
}
```

如果想用正则匹配一个 `\` 字符, 最终规则应该写成

```json5
{
  matches: 'TextView[text~="\\\\\\\\"]',
}
```

实现和上面相同的结果, 即用 `=` 匹配一个 `\` 字符, 最终规则应该写成

```json5
{
  matches: 'TextView[text="\\\\"]',
}
```

类似的问题 [gkd-kit/gkd#603](https://github.com/gkd-kit/gkd/issues/603)