Skip to content

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模块设置 AppNPatch 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:

text
npatch-remote-api-v<SDK 版本>-release.aar

复制到模块 App 的 app/libs/,然后添加依赖:

kotlin
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:

xml
<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 请优先使用异步接口:

kotlin
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:

kotlin
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(...):进阶入口,直接返回 libxposed IXposedService。应用若只需要存储功能,优先使用 client 封装以减少对 Binder 细节的依赖。

Remote Preferences

  • getRemotePreferences(String group):取得实现 Android SharedPreferences 的远程群组。同一 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

kotlin
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 当作已经保存
    }
}

支持的值类型为:

  • Boolean
  • Int
  • Long
  • Float
  • String
  • Set<String>

群组名称不可为空,也不能包含 /\

commit() 会等待 Manager 确认写入并返回结果。apply() 会先更新客户端内存,再在后台写入;Android 的 SharedPreferences.apply() 本来就无法回报异步失败,且如果进程在写入完成前结束,apply() 可能尚未落盘。

OnSharedPreferenceChangeListener 会收到同一个客户端实例产生的本地变更。另一个模块 UI 进程的写入不会主动刷新现有客户端快照;重新连接或重新取得客户端后可读取最新值。目标进程内的 API 101/102 Remote Preferences 则由 Manager Binder 接收变化通知。

删除整个 group:

kotlin
client.deleteRemotePreferences("settings")

Remote Files

Remote Files 只接受单层文件名,不允许 /\...

kotlin
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

java
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 的服务模型一致。

模块可先检查框架属性:

java
boolean supported =
        (getFrameworkProperties() & XposedInterface.PROP_CAP_REMOTE) != 0;

安全性与数据边界

模块 App 通过 ContentResolver 向 Manager 的 RemoteApiProvider 发起 getRemoteService 调用,取得一个 IXposedService Binder。Provider 虽然必须 exported 才能服务模块 App,但不以调用端传入的包名作为唯一信任来源;Manager 同时确认:

  1. 包已存在于 NPatch 的模块数据;
  2. 包属于 Binder 调用 UID;
  3. 返回的 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 识别、请求包名不属于调用 UID
  • RemoteException:Manager Binder 在操作期间断开
  • FileNotFoundException:Manager 未返回有效的文件描述符;当前可写端通常会在文件不存在时创建它

Provider 返回的 Binder 会绑定首次调用 UID,不能取得后再转交给其他 App。作用域目标只能取得只读 injected Binder,不能冒充模块 App 写入数据。

Manager 更新、被系统停止或进程重建后,既有 Binder 可能失效。操作遇到 RemoteException 时应舍弃 client,稍后重新连接。

Remote Store 适合设置、规则和小型状态文件。大量数据、数据库同步或高频消息请使用模块自己的 IPC / ContentProvider。

NPatch 官方介绍页面