NPatch Remote API 开发指南
NPatch Remote API 用于在模块 App(设置界面)与本地模式目标进程之间共享少量偏好设置和文件。数据由 NPatch Manager 保存,并按模块包名隔离。
SDK 已在 7723mod/NPatch-Remote-API 独立开源;公共客户端、版本、AAR、底层 API 与安全说明均集中在本页,仓库仅用于源码和构建发布。
它不是通用 IPC,也不会取代模块作用域:
- 模块 App 通过
NPatchRemoteClient取得标准、可读写的IXposedService - API 101/102 模块在目标进程中通过 libxposed
XposedInterface取得只读的 Remote Preferences / Remote Files - Manager 负责把模块 App 的写入变化通知给已注入的 API 101/102 runtime
- Legacy
XSharedPreferences不会自动迁移;Legacy 模块必须明确采用此 API
与 libxposed 的关系
NPatch Remote API 不是另一套 Xposed API,也不取代 libxposed。
两者处于不同进程和生命周期。XposedInterface 是注入目标 App 后提供给模块运行时的接口;XposedService 是模块本体/设置 App 与框架或 Manager 通讯的服务。NPatch Remote API 不参与目标进程注入,也不提供 XposedInterface,只在 NPatch Local 模式下补充模块侧取得同一份 API 102 IXposedService 的入口。
| 通道 | 所在进程 | 用途 | 获取方式 |
|---|---|---|---|
XposedInterface | 被注入的目标 App | 模块入口、Hook、目标进程生命周期 | XposedModule.attachFramework(...) 等 libxposed 生命周期回调 |
XposedService | 模块本体或设置 App | 作用域、Remote Preferences、Remote Files、热重载 | <模块包名>.XposedService + XposedServiceHelper.registerListener(...) |
NPatchRemoteClient | 模块设置 App | NPatch Local 的备用/显式连接 | 经过身份校验的 Manager Provider,返回同一 API 102 合约 |
模块设置 App 推荐使用 NPatchRemoteClient,这样可以在免 Root、无 Shizuku 的本地模式下获得更完整一致的设置体验;若模块已经通过 libxposed 标准路径收到 XposedService,可继续使用 XposedServiceHelper.registerListener(...)。目标进程代码不要创建 NPatchRemoteClient。
适用范围
公开的 NPatchRemoteClient 只适用于安装了 NPatch Manager 的本地模式。集成模式没有 Manager Provider,使用的是目标 App 自己的 host-local Store,模块 App 无法通过公开客户端连接它。
要求:
- NPatch v1.0.7 或更高版本
- Android 9(API 28)或更高版本
- 模块已安装并被 NPatch Manager 识别
- 模块 App 的实际包名与 NPatch 中登记的模块包名一致
不需要 Root、Shizuku 或额外 Android 权限。
添加 SDK
从 NPatch Remote API Releases 下载 AAR:
npatch-remote-api-v<SDK 版本>-release.aar复制到模块 App 的 app/libs/,然后添加依赖:
dependencies {
implementation(files("libs/npatch-remote-api-v1.0.0-release.aar"))
// 如果项目已经依赖 io.github.libxposed:service:102.0.0,
// 通常会传递取得 interface;否则显式加入这一项。
implementation("io.github.libxposed:interface:102.0.0")
}模块 App 需要声明对 NPatch Provider 的包可见性,否则 isAvailable() 在 Android 11 以上的设备会解析不到 Provider:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<provider android:authorities="top.nkbe.npatch.remote" />
</queries>
<application>
<!-- 模块 App 原有内容 -->
</application>
</manifest>建立连接
连接可能需要启动 NPatch Manager 进程。UI 请优先使用异步接口:
import top.nkbe.npatch.remote.NPatchRemoteClient
if (!NPatchRemoteClient.isAvailable(applicationContext)) {
// Provider 不存在:隐藏 NPatch 专用设置或改用模块自己的本地存储。
return
}
NPatchRemoteClient.connectAsync(applicationContext)
.whenComplete { client, error ->
runOnUiThread {
if (error != null) {
// 显示「NPatch 未安装、模块未登记或服务暂时不可用」
return@runOnUiThread
}
useRemoteApi(client)
}
}isAvailable() 只检查 Provider 是否可解析,不代表模块身份一定能通过验证;最终仍以连接结果为准。
connect(context) 会使用调用 App 自己的包名。一般不要使用可指定包名的 overload;Provider 会验证 Binder 调用 UID 是否真正拥有请求的模块包名。
若自行编译的 Manager 使用其他 application ID,可同时传入模块包名与 ${applicationId}.remote authority:
val client = NPatchRemoteClient.connect(
applicationContext,
applicationContext.packageName,
"your.manager.application.id.remote"
)同步 connect() 最多等待 3 秒,适合后台线程。connectAsync() 的 completion 运行在线程池,不是主线程。
API 参考
公共入口只有 top.nkbe.npatch.remote.NPatchRemoteClient。
常量
DEFAULT_AUTHORITY:官方 NPatch Manager 的 Remote Provider authority,值为top.nkbe.npatch.remote。
探测
isAvailable(Context):检查系统能否解析官方 Manager provider。它只代表 provider 存在,不代表当前模块一定能通过验证。isAvailable(Context, String authority):检查自定义 Manager authority。
连接
connect(Context):使用调用 App 的 package name 与官方 authority 同步连接。connect(Context, String modulePackageName):指定模块 package name,使用官方 authority。connect(Context, String modulePackageName, String authority):完整连接入口,调用会在三秒后超时。connectAsync(...):对应同步入口的CompletableFuture版本。connectService(...):进阶入口,直接返回 libxposedIXposedService。应用若只需要存储功能,优先使用 client 封装以减少对 Binder 细节的依赖。
Remote Preferences
getRemotePreferences(String group):取得实现 AndroidSharedPreferences的远程群组。同一 client 会缓存同名群组。deleteRemotePreferences(String group):删除远程群组内容,并清空当前 client 的缓存快照。
Remote Files
listRemoteFiles():返回已排序的单层文件名数组。openRemoteFile(String name):取得可读写的ParcelFileDescriptor。调用端负责关闭 descriptor 及建立于其上的 stream。deleteRemoteFile(String name):删除指定远程文件并返回是否成功。
线程
client 可在多线程共用;Preferences 快照及 listener 集合具备并发保护。同步 Binder 与同步连接操作仍可能阻塞,UI 应使用异步入口或自行切换线程。
Remote Preferences
模块 App 端取得的是标准 SharedPreferences:
private fun useRemoteApi(client: NPatchRemoteClient) {
val preferences = client.getRemotePreferences("settings")
val enabled = preferences.getBoolean("enabled", false)
val saved = preferences.edit()
.putBoolean("enabled", !enabled)
.putString("mode", "safe")
.commit()
if (!saved) {
// Binder 或持久化失败;不要把 UI 当作已经保存
}
}支持的值类型为:
BooleanIntLongFloatStringSet<String>
群组名称不可为空,也不能包含 / 或 \。
commit() 会等待 Manager 确认写入并返回结果。apply() 会先更新客户端内存,再在后台写入;Android 的 SharedPreferences.apply() 本来就无法回报异步失败,且如果进程在写入完成前结束,apply() 可能尚未落盘。
OnSharedPreferenceChangeListener 会收到同一个客户端实例产生的本地变更。另一个模块 UI 进程的写入不会主动刷新现有客户端快照;重新连接或重新取得客户端后可读取最新值。目标进程内的 API 101/102 Remote Preferences 则由 Manager Binder 接收变化通知。
删除整个 group:
client.deleteRemotePreferences("settings")Remote Files
Remote Files 只接受单层文件名,不允许 /、\、. 或 ..:
import java.io.FileInputStream
import java.io.FileOutputStream
client.openRemoteFile("rules.json").use { descriptor ->
FileOutputStream(descriptor.fileDescriptor).use { output ->
output.write("""{"enabled":true}""".toByteArray())
}
}
client.openRemoteFile("rules.json").use { descriptor ->
FileInputStream(descriptor.fileDescriptor).bufferedReader().use { input ->
val json = input.readText()
}
}
val names: Array<String> = client.listRemoteFiles()
val deleted: Boolean = client.deleteRemoteFile("rules.json")模块 App 取得的文件描述符可读写。目标进程内 API 101/102 的 openRemoteFile() 只读。
API 101/102 模块运行时
目标进程内不要建立 NPatchRemoteClient。使用 libxposed 提供给模块的 XposedInterface:
SharedPreferences preferences = getRemotePreferences("settings");
boolean enabled = preferences.getBoolean("enabled", false);
try (ParcelFileDescriptor file = openRemoteFile("rules.json")) {
// read only
}Injected Remote Preferences 是只读实现;调用 edit() 会抛出 UnsupportedOperationException。写入应由模块 App 完成。这一分层与 Vector / libxposed API 101/102 的服务模型一致。
模块可先检查框架属性:
boolean supported =
(getFrameworkProperties() & XposedInterface.PROP_CAP_REMOTE) != 0;安全性与数据边界
模块 App 通过 ContentResolver 向 Manager 的 RemoteApiProvider 发起 getRemoteService 调用,取得一个 IXposedService Binder。Provider 虽然必须 exported 才能服务模块 App,但不以调用端传入的包名作为唯一信任来源;Manager 同时确认:
- 包已存在于 NPatch 的模块数据;
- 包属于 Binder 调用 UID;
- 返回的 service Binder 后续每次调用仍来自原始 UID。
因此只伪造 extras 内的包名无法读取另一模块的数据。
Preferences 与 Files 都以模块包分区。群组和文件名会拒绝路径分隔符,Files 仅允许单层文件名,以防止路径穿越。
被修补目标程序使用另一条框架内部、只读的 injected service。此 SDK 不包含该 AIDL,也不提供取得它的公共方法,避免模块 App 或第三方 App 伪装成被注入目标。SDK 取得的 IXposedService 仍可能包含作用域、运行中目标或热重载等 API 102 方法;这些方法由 NPatch Manager 按模块 UID 验证,Remote SDK 不另外封装或放宽权限。
威胁模型限制:
- Android 若允许攻击者以相同 UID 运行程序,该程序本来就共享应用信任边界。
isAvailable只探测 provider,不等于身份验证成功。- Binder 对象不应传给其他程序;Manager 会拒绝不同 UID 的后续调用。
- SDK 不为 Remote Files 提供内容加密。数据位于 Manager 私有目录,安全性依赖 Android sandbox 与设备本身。
错误处理
常见异常:
IllegalStateException:NPatch 未安装、Provider 无法启动或连接超时SecurityException:模块未被 NPatch 识别、请求包名不属于调用 UIDRemoteException:Manager Binder 在操作期间断开FileNotFoundException:Manager 未返回有效的文件描述符;当前可写端通常会在文件不存在时创建它
Provider 返回的 Binder 会绑定首次调用 UID,不能取得后再转交给其他 App。作用域目标只能取得只读 injected Binder,不能冒充模块 App 写入数据。
Manager 更新、被系统停止或进程重建后,既有 Binder 可能失效。操作遇到 RemoteException 时应舍弃 client,稍后重新连接。
Remote Store 适合设置、规则和小型状态文件。大量数据、数据库同步或高频消息请使用模块自己的 IPC / ContentProvider。