# Service 系统
# 1. 概念
Service 是 AutumnBox 插件体系中的核心抽象。它是一个注册在 IoC(控制反转)容器 中的类,任何插件或宿主代码都可以通过容器获取其实例。
# IoC 容器模式
AutumnBox 采用经典的 IoC 容器模式管理 Service 的生命周期与依赖关系。其核心思想是:
- Service 不主动创建自己的依赖。它在构造函数中接收
ServiceContainer,从中获取所需的其他 Service。 - 容器负责实例化与缓存。调用者只声明"我需要什么",而不关心"如何创建"。
- 懒实例化。Service 不在注册时创建,而是在第一次
getService()调用时才被实例化。 - 默认单例。同一个 Service 类在容器中只有一个实例,所有调用方共享同一对象。
┌─────────────────────────────────────────┐
│ ServiceContainer │
│ │
│ ┌─ DevicesService (lazy singleton) ──┐ │
│ │ │ │
│ │ constructor(container) { │ │
│ │ this.driver = container │ │
│ │ .getService(DriverService) │ │
│ │ .getAdbDriver(); │ │
│ │ } │ │
│ └────────────────────────────────────┘ │
│ │
│ ┌─ ShellService (lazy singleton) ────┐ │
│ │ ... │ │
│ └────────────────────────────────────┘ │
│ │
│ ┌─ 插件注册的自定义 Service ─────────┐ │
│ │ ... │ │
│ └────────────────────────────────────┘ │
└─────────────────────────────────────────┘
# 2. 创建一个 Service
# 约定
- 将文件放在
src/services/目录下。 - 导出的类名必须以
Service结尾。 - 构造函数必须接收
ServiceContainer作为唯一参数。
# 目录结构
my-plugin/
├── src/
│ ├── services/
│ │ ├── ScrcpyBridgeService.ts # 文件形式
│ │ └── DeviceMonitorService/
│ │ └── index.ts # 目录形式(也受支持)
│ ├── apps/
│ │ └── RemoteControlApp.tsx
│ └── main.ts
├── package.json
# 完整示例
// src/services/ScrcpyBridgeService.ts
import type { ServiceContainer } from '@autumnbox/sdk/common';
import type { AdbDeviceHandle } from '@autumnbox/sdk/interfaces';
import { ShellService } from '@autumnbox/sdk/services';
export class ScrcpyBridgeService {
private readonly shell: ShellService;
constructor(container: ServiceContainer) {
this.shell = container.getService(ShellService);
}
/** 在目标设备上启动 scrcpy server。 */
async startServer(device: AdbDeviceHandle, version: string): Promise<void> {
await this.shell.exec(device, [
'CLASSPATH=/data/local/tmp/scrcpy-server.jar',
'app_process',
'/',
'com.genymobile.scrcpy.Server',
version,
]);
}
/** 检查 scrcpy server 是否已部署到设备。 */
async isServerDeployed(device: AdbDeviceHandle): Promise<boolean> {
const output = await this.shell.exec(device, 'ls /data/local/tmp/scrcpy-server.jar');
return !output.includes('No such file');
}
}
构建时 SDK 自动发现这个类并生成注册代码,无需手动注册。
# 3. ServiceContainer 完整 API
ServiceContainer 是 IoC 容器的实现,位于 @autumnbox/common 包中。
# ServiceClass 类型
type ServiceClass<T = unknown> = new (container: ServiceContainer) => T;
任何构造函数签名为 (container: ServiceContainer) => T 的类都满足此约束。容器在实例化时会将自身作为参数传入。
# ServiceFeats 接口
interface ServiceFeats {
/** 覆盖自动派生的名称。 */
name?: string;
/** 是否强制单例注册,默认 true。 */
singleton?: boolean;
}
# registerService
registerService<T>(Clazz: ServiceClass<T>, feats?: ServiceFeats): void;
将一个 Service 类注册到容器中。
行为细节:
- 名称派生:若未指定
feats.name,则从Clazz.name自动派生(详见第 4 节)。 - 默认单例:
feats.singleton默认为true。 - 双重注册:一次
registerService同时在类 token 和字符串名称两个维度注册,两种方式都能检索到同一个实例。 - 单例冲突检查:当
singleton = true时,若该名称下已存在注册,则抛出异常。
container.registerService(ScrcpyBridgeService);
// 等价于:
// container.registerService(ScrcpyBridgeService, { singleton: true });
// 名称自动派生为 'scrcpyBridge'
// 指定自定义名称和非单例模式
container.registerService(MyWorker, { name: 'worker', singleton: false });
异常:
Singleton service "scrcpyBridge" conflicts with an existing registration
Cannot register service "worker": it is already registered as a singleton
# registerInstance
registerInstance(name: string, instance: unknown): void;
注册一个预先创建的对象实例。适用于不通过类构造函数创建的外部对象,如驱动实例、文件系统等。
// 宿主在启动时注册 ADB 驱动
container.registerInstance('adb', webAdbDriver);
container.registerInstance('fs', fileSystemImpl);
注意: registerInstance 只通过字符串名称注册,不关联类 token。若该名称已被单例占据,同样会抛出异常。
# getService(两个重载)
// 重载 1:按类 token 获取(类型安全)
getService<T>(token: ServiceClass<T>): T;
// 重载 2:按字符串名称获取
getService(name: string): unknown;
按类 token 获取 是推荐方式,返回值具有完整的 TypeScript 类型推导:
const shell = container.getService(ShellService);
// shell 的类型是 ShellService — 完全类型安全
const devices = container.getService(DevicesService);
// devices 的类型是 DevicesService
按字符串名称获取 返回 unknown,需要自行断言类型:
const adb = container.getService('adb') as IAdbDriver;
const fs = container.getService('fs') as IFileSystem;
异常: 若未注册对应的 Service,则抛出 Error:
Service "xxx" is not registered
Service class "XxxService" is not registered
# getServices
getServices(name: string): unknown[];
获取某个名称下注册的所有 Service 实例(数组)。当同一名称下以 singleton: false 注册了多个 Service 时,此方法返回全部实例。
// 假设多个插件都注册了 name='themeProvider' 的非单例服务
const providers = container.getServices('themeProvider');
for (const provider of providers) {
(provider as IThemeProvider).apply();
}
异常: 若该名称下没有任何注册,则抛出 Error。
# 4. 名称派生规则(deriveServiceName)
当 registerService 未指定 feats.name 时,容器从类名自动派生注册名称。
# 算法
1. 若类名以 "Service" 结尾 → 去掉 "Service" 后缀 → 首字母小写
2. 否则 → 直接首字母小写
3. 若去掉后缀后为空字符串 → 返回原类名全小写
# 源码实现
function deriveServiceName(className: string): string {
const stripped = className.endsWith('Service')
? className.slice(0, -'Service'.length)
: className;
if (stripped.length === 0) return className.toLowerCase();
return stripped[0]!.toLowerCase() + stripped.slice(1);
}
# 完整示例表
| 类名 | 去掉 Service | 首字母小写 | 最终 token |
|---|---|---|---|
AbcService | Abc | abc | 'abc' |
DeviceMonitorService | DeviceMonitor | deviceMonitor | 'deviceMonitor' |
ScrcpyBridgeService | ScrcpyBridge | scrcpyBridge | 'scrcpyBridge' |
ShellService | Shell | shell | 'shell' |
NotificationService | Notification | notification | 'notification' |
LanguageService | Language | language | 'language' |
MyHelper(不以 Service 结尾) | MyHelper | myHelper | 'myHelper' |
Service(边界情况) | 空串 | — | 'service' |
# 5. 懒创建与单例行为
# 懒创建
Service 实例不在注册时创建,而是在第一次 getService() 调用时才被实例化。这意味着:
- 如果一个 Service 从未被任何代码请求,它永远不会被创建。
- 构造函数中的副作用(如订阅、定时器)只在实际使用时才触发。
container.registerService(HeavyService);
// 此时 HeavyService 尚未被创建
const instance = container.getService(HeavyService);
// 第一次调用,触发 new HeavyService(container)
# 构造函数中的依赖注入
构造函数接收 ServiceContainer 参数,从中获取其他 Service。由于懒创建,被依赖的 Service 也会在此时按需创建,形成递归的依赖解析链。
export class MyService {
private readonly shell: ShellService;
constructor(container: ServiceContainer) {
// 若 ShellService 尚未实例化,此处触发其构造
this.shell = container.getService(ShellService);
}
}
# 单例模式(默认)
默认情况下,singleton: true。同一个 Service 类无论被 getService() 调用多少次,始终返回同一个实例:
const a = container.getService(ShellService);
const b = container.getService(ShellService);
console.log(a === b); // true
# 非单例模式
将 singleton 设为 false 可允许同一名称下注册多个 Service(如多个插件各自注册一个同类型的 Service):
container.registerService(ThemeProviderA, { name: 'themeProvider', singleton: false });
container.registerService(ThemeProviderB, { name: 'themeProvider', singleton: false });
// 通过 getServices 获取全部
const providers = container.getServices('themeProvider');
// providers.length === 2
注意:非单例注册的 Service 仍然对每个类 token 实行懒创建和缓存——同一个 entry 不会被构造两次。
# 6. 宿主内置 Service
以下 Service 由 AutumnBox 宿主提供(位于 @autumnbox/core 包),插件可直接使用,无需注册。
# DevicesService
管理 Android 设备发现。通过轮询 IAdbDriver.listDevices() 获取设备列表,并以响应式 IReadonlyState 属性暴露结果。
AutumnBox 不存在"全局选中设备"的概念——每个 App 自行决定操作哪个设备。
import type { IReadonlyState, ServiceContainer } from '@autumnbox/sdk/common';
import type { AdbDeviceHandle } from '@autumnbox/sdk/interfaces';
class DevicesService {
/** 当前已连接的设备列表(响应式)。 */
readonly devices: IReadonlyState<readonly AdbDeviceHandle[]>;
constructor(container: ServiceContainer);
/** 立即刷新一次设备列表。 */
async refresh(): Promise<void>;
/** 开始定期轮询设备列表。默认 2000ms 间隔。 */
startPolling(intervalMs?: number): void;
/** 停止轮询。 */
stopPolling(): void;
}
使用示例:
constructor(container: ServiceContainer) {
const devicesService = container.getService(DevicesService);
// 订阅设备列表变化
devicesService.devices.subscribe((list) => {
console.log('当前设备:', list.map(d => d.sn));
});
}
# ShellService
在 Android 设备上执行 shell 命令。所有操作以 AdbDeviceHandle 为第一参数。
class ShellService {
constructor(container: ServiceContainer);
/**
* 在设备上启动一个 shell 进程。
* 返回 IAdbShellProcess,支持流式读取 stdout/stderr、写入 stdin、PTY 等。
*/
async spawn(
device: AdbDeviceHandle,
command: string | string[],
options?: IAdbSpawnOptions,
): Promise<IAdbShellProcess>;
/**
* 在设备上执行命令,等待完成后返回 stdout 完整字符串。
* 适用于短命令、不需要流式处理的场景。
*/
async exec(
device: AdbDeviceHandle,
command: string | string[],
): Promise<string>;
}
IAdbSpawnOptions 可选参数:
interface IAdbSpawnOptions {
cwd?: string; // 工作目录
env?: Record<string, string>; // 环境变量
signal?: AbortSignal; // 取消信号
stdin?: BinarySource | string; // 预填充 stdin
pty?: IAdbPtyOptions; // 伪终端配置
timeoutMs?: number; // 超时(毫秒)
separateStderr?: boolean; // 是否分离 stderr
}
使用示例:
// 简单执行
const output = await shell.exec(device, 'getprop ro.build.version.release');
console.log('Android 版本:', output.trim());
// 流式处理(交互式命令)
const proc = await shell.spawn(device, 'logcat', {
signal: AbortSignal.timeout(10000),
});
for await (const chunk of proc.stdout) {
process.stdout.write(new TextDecoder().decode(chunk));
}
# DeviceFileSystemService
设备文件操作:推送、拉取、列目录。
class DeviceFileSystemService {
constructor(container: ServiceContainer);
/** 将文件推送到设备指定目录。 */
async push(
device: AdbDeviceHandle,
deviceDir: string,
source: PushSource,
signal?: AbortSignal,
): Promise<void>;
/** 从设备拉取文件。 */
async pull(
device: AdbDeviceHandle,
devicePath: string,
dest: PullDest,
signal?: AbortSignal,
): Promise<void>;
/** 列出设备上远程目录的内容(自动过滤 `.` 和 `..`)。 */
async listDir(
device: AdbDeviceHandle,
remotePath: string,
): Promise<readonly IAdbFileEntry[]>;
}
相关类型:
// 推送源:本地路径字符串,或内存中的文件数据
type PushSource = string | IFileData;
interface IFileData {
data: Blob | ArrayBuffer;
name: string;
size: number;
}
// 拉取目标:写入本地文件,或通过回调逐块读取
type PullDest = IPullToLocal | IPullToReader;
interface IPullToLocal {
type: 'local';
path: string;
}
interface IPullToReader {
type: 'reader';
chunkCallback: (chunk: Uint8Array) => Promise<void>;
}
// 文件条目
interface IAdbFileEntry {
path: string;
name: string;
size: number;
mode: number;
isDirectory: boolean;
modifiedAt?: Date;
}
使用示例:
// 推送 APK 到设备
await fileService.push(device, '/data/local/tmp/', apkFileData);
// 列出目录
const entries = await fileService.listDir(device, '/sdcard/Download/');
for (const entry of entries) {
console.log(`${entry.isDirectory ? '[DIR]' : ' '} ${entry.name} ${entry.size} bytes`);
}
// 拉取文件到内存
const chunks: Uint8Array[] = [];
await fileService.pull(device, '/sdcard/log.txt', {
type: 'reader',
chunkCallback: async (chunk) => { chunks.push(chunk); },
});
# PackageService
Android 应用包管理:安装 APK、列出已安装包。
class PackageService {
constructor(container: ServiceContainer);
/** 列出设备上已安装的所有包名。 */
async listPackages(device: AdbDeviceHandle): Promise<string[]>;
/**
* 安装 APK 到设备。
* 内部逻辑:先 push 到 /data/local/tmp/,再执行 pm install -r,最后清理临时文件。
*/
async installApk(device: AdbDeviceHandle, source: PushSource): Promise<void>;
}
使用示例:
// 列出已安装应用
const packages = await packageService.listPackages(device);
console.log('已安装:', packages.length, '个应用');
// 安装 APK
await packageService.installApk(device, {
data: apkBlob,
name: 'my-app.apk',
size: apkBlob.size,
});
# RebootService
重启 Android 设备到不同目标模式。
class RebootService {
constructor(container: ServiceContainer);
/** 重启设备。target 省略时为正常重启。 */
async reboot(device: AdbDeviceHandle, target?: AdbRebootTarget): Promise<void>;
}
AdbRebootTarget 取值:
type AdbRebootTarget =
| 'system' // 正常重启(默认)
| 'bootloader' // 重启到 bootloader
| 'recovery' // 重启到 recovery
| 'sideload' // 重启到 sideload 模式
| 'sideload-auto-reboot'; // sideload 完成后自动重启
使用示例:
// 正常重启
await rebootService.reboot(device);
// 重启到 recovery
await rebootService.reboot(device, 'recovery');
// 重启到 bootloader(用于刷机)
await rebootService.reboot(device, 'bootloader');
# LocalFileSystemService
访问宿主文件系统(非设备文件系统)。通过字符串名称 'fs' 从容器获取 IFileSystem 实例。
class LocalFileSystemService {
constructor(container: ServiceContainer);
/** 返回宿主环境的 IFileSystem 实现。 */
getFileSystem(): IFileSystem;
}
IFileSystem 接口:
interface IFileSystem {
readonly description: string;
autumnHome(): string;
readFile(path: string): Promise<ReadableStream<Uint8Array>>;
writeFile(path: string, data: Blob): Promise<void>;
stat(path: string): Promise<IFileStat>;
remove(path: string): Promise<void>;
exists(path: string): Promise<boolean>;
mkdir(path: string): Promise<void>;
readdir(path: string): Promise<string[]>;
resolve(...segments: string[]): string;
tmpdir(): string;
}
interface IFileStat {
size: number;
mode: number;
isDirectory: boolean;
modifiedAt: Date;
}
使用示例:
const fsService = container.getService(LocalFileSystemService);
const fs = fsService.getFileSystem();
const home = fs.autumnHome();
const configPath = fs.resolve(home, 'config.json');
if (await fs.exists(configPath)) {
const stream = await fs.readFile(configPath);
// 读取 stream...
}
# NotificationService
系统通知管理。提供响应式的通知列表和未读数。
interface INotification {
id: string;
type: 'error' | 'warning' | 'info' | 'success';
title: string;
message: string;
timestamp: Date;
read: boolean;
}
class NotificationService {
/** 全部通知列表(响应式,最新在前)。 */
readonly notifications: IReadonlyState<readonly INotification[]>;
/** 未读通知数(响应式)。 */
readonly unreadCount: IReadonlyState<number>;
constructor(container: ServiceContainer);
/** 推送一条新通知。 */
push(type: INotification['type'], title: string, message: string): void;
/** 将指定通知标记为已读。 */
markRead(id: string): void;
/** 将全部通知标记为已读。 */
markAllRead(): void;
/** 清空所有通知。 */
clear(): void;
}
使用示例:
const notif = container.getService(NotificationService);
// 推送通知
notif.push('success', '安装完成', 'my-app.apk 已成功安装到设备');
notif.push('error', '连接失败', '无法连接到设备 192.168.1.100:5555');
// 监听未读数变化
notif.unreadCount.subscribe((count) => {
console.log('未读通知:', count);
});
# LanguageService
响应式国际化(i18n)服务。管理多语言文本,支持按 locale 切换,翻译结果通过 IReadonlyState 响应式更新 UI。
class LanguageService {
/** 当前活跃的 locale(BCP 47 代码,如 'zh-CN', 'en-US')。 */
readonly locale: IReadonlyState<string>;
constructor(container: ServiceContainer);
/**
* 批量加载翻译文本。
* @param id - 来源标识(如插件 ID),用于后续 unload
* @param locale - BCP 47 locale 代码
* @param content - key → value 翻译映射
*/
load(id: string, locale: string, content: Record<string, string>): void;
/**
* 卸载翻译。
* - unload(id) — 移除该 id 的所有 locale 翻译
* - unload(id, locale) — 仅移除该 id 在指定 locale 下的翻译
*/
unload(id: string, locale?: string): void;
/** 注册单条翻译。 */
addText(key: string, locale: string, value: string): void;
/** 移除单条翻译。 */
removeText(key: string, locale: string): void;
/** 切换活跃 locale,触发全局翻译刷新。 */
switchLocale(locale: string): void;
/**
* 获取指定 key 的响应式翻译状态。
* 返回的 IReadonlyState<string> 在 locale 切换或文本变化时自动更新。
* 同一 key 返回同一对象(已缓存)。
*
* 回退链:当前 locale → 'en-US' → key 本身
*/
getT(key: string): IReadonlyState<string>;
}
使用示例:
const lang = container.getService(LanguageService);
// 加载插件翻译
lang.load('com.my-plugin', 'zh-CN', {
'my-plugin.title': '我的插件',
'my-plugin.description': '一个很酷的插件',
});
lang.load('com.my-plugin', 'en-US', {
'my-plugin.title': 'My Plugin',
'my-plugin.description': 'A cool plugin',
});
// 获取响应式翻译
const titleState = lang.getT('my-plugin.title');
console.log(titleState.value); // 取决于当前 locale
// locale 切换后自动更新
titleState.subscribe((text) => {
console.log('标题变了:', text);
});
lang.switchLocale('en-US');
// 输出: "标题变了: My Plugin"
# 7. 响应式状态系统
AutumnBox 的 Service 层和 UI 层通过响应式状态原语通信,位于 @autumnbox/common 包中。
# 核心接口
/** 只读响应式状态。消费者只能读取和订阅。 */
interface IReadonlyState<V> {
readonly value: V;
subscribe(listener: (value: V) => void): () => void;
}
/** 可变响应式状态。持有者可以读写。 */
interface IState<V> extends IReadonlyState<V> {
value: V; // 可写
}
# 工厂函数
/**
* 创建一个可变状态。
* 适用于 Service 内部的私有状态管理。
*/
function createState<V>(initial: V): IState<V>;
/**
* 创建一个只读状态,返回 [只读视图, setter 函数]。
* Service 暴露 IReadonlyState 给外部,自己持有 setter。
* 模式类似 React 的 useState 解构。
*/
function createReadonlyState<V>(initial: V): [IReadonlyState<V>, (value: V) => void];
# 在 Service 中使用
典型模式:Service 对外暴露 IReadonlyState(只读),内部保留 setter。
import { createReadonlyState } from '@autumnbox/sdk/common';
import type { IReadonlyState, ServiceContainer } from '@autumnbox/sdk/common';
export class DownloadProgressService {
/** 外部只能读和订阅。 */
readonly progress: IReadonlyState<number>;
/** 内部持有 setter。 */
private readonly setProgress: (v: number) => void;
constructor(_container: ServiceContainer) {
const [progress, setProgress] = createReadonlyState<number>(0);
this.progress = progress;
this.setProgress = setProgress;
}
async download(url: string): Promise<void> {
// 模拟下载过程中更新进度
this.setProgress(0);
// ... 下载逻辑 ...
this.setProgress(50);
// ... 继续 ...
this.setProgress(100);
}
}
# subscribe 返回值
subscribe 返回一个取消订阅函数。调用它可停止接收后续更新:
const unsub = state.subscribe((value) => {
console.log('新值:', value);
});
// 不再需要时取消订阅
unsub();
# 在 React 中使用(useServiceState)
UI 层通过 useServiceState hook 订阅状态变化,自动触发组件重渲染:
import { useServiceState } from '@autumnbox/sdk/hooks';
import { DevicesService } from '@autumnbox/sdk/services';
const DeviceList: React.FC = () => {
// 方式一:传入 Service 类 + 属性名
const [devices] = useServiceState(DevicesService, 'devices');
// 方式二:传入 IReadonlyState 对象
const devicesService = useService(DevicesService);
const [devices2] = useServiceState(devicesService.devices);
return (
<ul>
{devices.map((d) => (
<li key={d.sn}>{d.sn}</li>
))}
</ul>
);
};
useServiceState 的完整重载签名:
// 直接传入 IState → 返回 [V, setter]
function useServiceState<V>(state: IState<V>): [V, (v: V) => void];
// 直接传入 IReadonlyState → 返回 [V](无 setter)
function useServiceState<V>(state: IReadonlyState<V>): [V];
// 传入 Service 实例或类 + 属性名 → 自动推断返回类型
function useServiceState<T, K extends StateKeysOf<T>>(
service: T | ServiceClass<T>,
stateName: K,
): UseServiceStateReturn<T[K]>;
useServiceState 会自动检测状态是否可写(IState vs IReadonlyState),分别返回带或不带 setter 的元组。
# 8. Handle-first API 设计
AutumnBox 的所有设备操作 API 都采用 Handle-first 模式:第一个参数始终是 AdbDeviceHandle。
# AdbDeviceHandle 类型
type AdbDeviceHandle = Readonly<{
sn: string; // 设备序列号(如 "emulator-5554", "192.168.1.100:5555")
}>;
AdbDeviceHandle 是一个不可变的轻量对象,仅携带设备标识信息。它由 DevicesService.devices 提供,或通过 useRequiredDevice() hook 获取。
# 设计理念
- 没有"全局选中设备"——每个 App 各自维护自己操作的目标设备。
- 多设备操作天然支持——同一个 Service 实例可同时对不同设备执行命令。
- Handle 不可变——不会被意外修改,传递安全。
// 同时操作两台设备
const [deviceA, deviceB] = devices;
const outputA = await shell.exec(deviceA, 'getprop ro.build.display.id');
const outputB = await shell.exec(deviceB, 'getprop ro.build.display.id');
# 在 App 中获取设备
若 App 定义中声明了 shallSelectAdbDevice: true,系统会在打开 App 前要求用户选择设备。在 App 组件内通过 useRequiredDevice() 获取:
import { useRequiredDevice } from '@autumnbox/sdk/hooks';
const MyApp: React.FC = () => {
const device = useRequiredDevice();
// device 保证不为 null
// ...
};
# 9. Service 间的依赖注入
Service 之间通过构造函数注入依赖。这是 AutumnBox IoC 的核心机制。
# 模式
export class MyFeatureService {
private readonly shell: ShellService;
private readonly devices: DevicesService;
private readonly notif: NotificationService;
constructor(container: ServiceContainer) {
// 从容器中获取所需的其他 Service
this.shell = container.getService(ShellService);
this.devices = container.getService(DevicesService);
this.notif = container.getService(NotificationService);
}
async doSomething(device: AdbDeviceHandle): Promise<void> {
const output = await this.shell.exec(device, 'id');
this.notif.push('info', '命令结果', output.trim());
}
}
# 依赖解析顺序
由于懒创建,依赖链会递归解析。例如:
MyFeatureService
└─ getService(ShellService)
└─ getService(DriverService) ← 也是懒创建
└─ getService('adb') ← 返回预注册的驱动实例
所有这些步骤在 new MyFeatureService(container) 执行过程中同步完成。
# 循环依赖
容器不检测循环依赖。如果 A 的构造函数获取 B,B 的构造函数又获取 A,会导致无限递归栈溢出。设计 Service 时应避免循环引用。
# 10. 跨插件 Service 共享
AutumnBox 的 Service 系统对所有插件是完全开放的。
# 规则
- 一个插件注册的 Service 可以被任何其他已加载的插件获取。
- 不存在可见性限制或命名空间隔离。
- 所有插件共享同一个
ServiceContainer实例。
# 名称冲突
由于默认单例,如果两个插件注册了相同派生名称的 Service,第二个注册会抛出异常:
Singleton service "scrcpyBridge" conflicts with an existing registration
建议: 为跨插件共享的 Service 使用带前缀的名称,如 com.myplugin.ScrcpyBridgeService,或在 feats.name 中指定唯一名称。
# 依赖其他插件的 Service
若插件 B 依赖插件 A 注册的 Service,需确保插件 A 先于 B 加载。目前,宿主按加载顺序初始化插件。
// 插件 A 注册
export class ScrcpyBridgeService { /* ... */ }
// 插件 B 使用(假设 A 已加载)
import type { ServiceContainer } from '@autumnbox/sdk/common';
export class RemoteViewService {
constructor(container: ServiceContainer) {
// 因为不能直接 import 插件 A 的类,只能用字符串名称
const bridge = container.getService('scrcpyBridge') as ScrcpyBridgeType;
}
}
# 11. 在 React 组件中使用 Service
AutumnBox 通过 @autumnbox/app 包提供了一组 React hooks,简化 Service 在 UI 层的使用。
# useService
从 IoC 容器中获取 Service 单例:
import { useService } from '@autumnbox/sdk/hooks';
function useService<T>(Clazz: ServiceClass<T>): T;
import { useService } from '@autumnbox/sdk/hooks';
import { ShellService, DevicesService } from '@autumnbox/sdk/services';
const MyComponent: React.FC = () => {
const shell = useService(ShellService);
const devices = useService(DevicesService);
// ...
};
# useServiceState
订阅 Service 上的响应式状态,状态变化时自动重渲染(详见第 7 节)。
# useT
国际化翻译 hook,内部使用 LanguageService.getT():
import { useT } from '@autumnbox/sdk/hooks';
const MyComponent: React.FC = () => {
const title = useT('my-plugin.title');
return <h1>{title}</h1>;
};
useT 在 locale 切换时自动重渲染。
# useShell
增强版 shell hook,返回带 exitCode 的结果:
import { useShell } from '@autumnbox/sdk/hooks';
const MyComponent: React.FC = () => {
const shell = useShell();
const handleClick = async () => {
const device = /* ... */;
// exec: 返回 { stdout, exitCode }
const result = await shell.exec(device, 'whoami');
console.log(result.stdout, result.exitCode);
// execOrThrow: exitCode 非零时抛异常
const output = await shell.execOrThrow(device, 'ls /sdcard');
};
return <button onClick={handleClick}>执行</button>;
};
# 完整组件示例
import { useEffect, useState } from 'react';
import { Button, List, Tag, Space } from 'antd';
import { useService, useServiceState, useRequiredDevice } from '@autumnbox/sdk/hooks';
import { PackageService, DevicesService, NotificationService } from '@autumnbox/sdk/services';
const PackageManagerApp: React.FC = () => {
const device = useRequiredDevice();
const packageService = useService(PackageService);
const notif = useService(NotificationService);
const [devices] = useServiceState(DevicesService, 'devices');
const [packages, setPackages] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const loadPackages = async () => {
setLoading(true);
try {
const list = await packageService.listPackages(device);
setPackages(list);
notif.push('success', '加载完成', `找到 ${list.length} 个应用`);
} catch (err) {
notif.push('error', '加载失败', String(err));
} finally {
setLoading(false);
}
};
useEffect(() => {
void loadPackages();
}, [device]);
return (
<div>
<Space style={{ marginBottom: 16 }}>
<Tag color="blue">设备: {device.sn}</Tag>
<Tag>在线设备: {devices.length}</Tag>
<Button onClick={loadPackages} loading={loading}>刷新</Button>
</Space>
<List
dataSource={packages}
renderItem={(pkg) => <List.Item>{pkg}</List.Item>}
/>
</div>
);
};
# 12. 自动发现机制
autumnbox-sdk build 在构建时自动扫描插件源码,发现 Service、App、Card 并生成注册代码。
# 扫描规则
SDK 扫描 src/services/ 目录下的所有 .ts / .tsx 文件,使用以下正则匹配:
const SERVICE_PATTERN = /export\s+class\s+(\w+Service)\b/;
只要文件中包含 export class XxxService 形式的导出,即被识别为 Service。
# 支持的文件模式
| 模式 | 路径示例 |
|---|---|
| 文件形式 | src/services/AbcService.ts |
| 文件形式(TSX) | src/services/AbcService.tsx |
| 目录形式 | src/services/AbcService/index.ts |
| 目录形式(TSX) | src/services/AbcService/index.tsx |
# 生成的代码
SDK 在 src/__entry__.ts 生成如下代码(每次构建覆盖,请勿手动编辑):
// 此文件由 autumnbox-sdk build 自动生成,请勿手动修改。
import type { PluginContext } from '@autumnbox/sdk';
import { ScrcpyBridgeService } from './services/ScrcpyBridgeService';
import { DeviceMonitorService } from './services/DeviceMonitorService/index';
export function __autumnbox_entry__(context: PluginContext): () => void {
const disposers: Array<() => void> = [];
// Services
context.serviceContainer.registerService(ScrcpyBridgeService);
context.serviceContainer.registerService(DeviceMonitorService);
// Apps
// ...
return () => {
for (const dispose of disposers) dispose();
};
}
# 注意事项
- 一个文件可以导出多个 Service 类,都会被发现。
- 类名必须以
Service结尾才能被自动发现。 - 自动发现只扫描
src/services/目录,不递归到更深的子目录。
# 13. 命名约定表
| 规则 | 说明 |
|---|---|
| 文件位置 | src/services/ 目录,支持子目录 XxxService/index.ts |
| 导出模式 | export class XxxService,必须以 Service 结尾 |
| 构造函数 | 必须接收 ServiceContainer 作为唯一参数 |
| 注册方式 | 默认单例,一个派生名称只能注册一次 |
| 名称派生 | 去掉 Service 后缀,首字母小写:AbcService -> 'abc' |
| 接口命名 | 接口用 I 前缀:INotification、IAdbDriver |
| Handle 命名 | Handle 类型用 *Handle 后缀:AdbDeviceHandle |
| 状态属性 | 对外暴露 IReadonlyState<T>,内部持有 setter |
| 导入规范 | 类型导入用 import type {}(verbatimModuleSyntax) |
# 14. 完整实战示例
以下是一个完整的"设备性能监视器"Service 实现,展示了构造函数注入、公开方法、响应式状态、以及在 React App 组件中的使用。
# Service 定义
// src/services/DevicePerfMonitorService.ts
import { createReadonlyState } from '@autumnbox/sdk/common';
import type { IReadonlyState, ServiceContainer } from '@autumnbox/sdk/common';
import type { AdbDeviceHandle } from '@autumnbox/sdk/interfaces';
import { ShellService } from '@autumnbox/sdk/services';
/** 一次采样的性能数据。 */
export interface IPerfSnapshot {
cpuUsage: number; // 百分比 0-100
memTotal: number; // 总内存 KB
memAvailable: number; // 可用内存 KB
timestamp: Date;
}
/**
* 设备性能监视服务。
*
* 定时采样目标设备的 CPU 和内存使用率,
* 通过响应式状态暴露最新数据和历史记录。
*/
export class DevicePerfMonitorService {
/** 最新一次采样(响应式)。 */
readonly latestSnapshot: IReadonlyState<IPerfSnapshot | null>;
/** 历史采样记录(响应式,最多保留 60 条)。 */
readonly history: IReadonlyState<readonly IPerfSnapshot[]>;
/** 是否正在监控(响应式)。 */
readonly monitoring: IReadonlyState<boolean>;
private readonly setLatest: (v: IPerfSnapshot | null) => void;
private readonly setHistory: (v: readonly IPerfSnapshot[]) => void;
private readonly setMonitoring: (v: boolean) => void;
private readonly shell: ShellService;
private timer: ReturnType<typeof setInterval> | null = null;
private static readonly MAX_HISTORY = 60;
constructor(container: ServiceContainer) {
this.shell = container.getService(ShellService);
const [latestSnapshot, setLatest] = createReadonlyState<IPerfSnapshot | null>(null);
const [history, setHistory] = createReadonlyState<readonly IPerfSnapshot[]>([]);
const [monitoring, setMonitoring] = createReadonlyState<boolean>(false);
this.latestSnapshot = latestSnapshot;
this.setLatest = setLatest;
this.history = history;
this.setHistory = setHistory;
this.monitoring = monitoring;
this.setMonitoring = setMonitoring;
}
/** 开始监控指定设备,intervalMs 为采样间隔。 */
start(device: AdbDeviceHandle, intervalMs = 2000): void {
this.stop();
this.setMonitoring(true);
const sample = async (): Promise<void> => {
try {
const snapshot = await this.takeSample(device);
this.setLatest(snapshot);
const prev = this.history.value;
const next = [...prev, snapshot].slice(-DevicePerfMonitorService.MAX_HISTORY);
this.setHistory(next);
} catch {
// 采样失败时不中断监控
}
};
void sample();
this.timer = setInterval(() => void sample(), intervalMs);
}
/** 停止监控。 */
stop(): void {
if (this.timer !== null) {
clearInterval(this.timer);
this.timer = null;
}
this.setMonitoring(false);
}
/** 采集一次性能数据。 */
private async takeSample(device: AdbDeviceHandle): Promise<IPerfSnapshot> {
// 获取 CPU 使用率(简化)
const cpuOutput = await this.shell.exec(
device,
"top -n 1 -b | head -5 | grep '%cpu'",
);
const cpuMatch = /(\d+)%idle/.exec(cpuOutput);
const idle = cpuMatch ? Number(cpuMatch[1]) : 0;
const cpuUsage = 100 - idle;
// 获取内存信息
const memOutput = await this.shell.exec(device, 'cat /proc/meminfo');
const totalMatch = /MemTotal:\s+(\d+)/.exec(memOutput);
const availMatch = /MemAvailable:\s+(\d+)/.exec(memOutput);
return {
cpuUsage,
memTotal: totalMatch ? Number(totalMatch[1]) : 0,
memAvailable: availMatch ? Number(availMatch[1]) : 0,
timestamp: new Date(),
};
}
}
# App 组件
// src/apps/PerfMonitorApp.tsx
import { useEffect } from 'react';
import { Card, Statistic, Row, Col, Button, Tag } from 'antd';
import {
useService,
useServiceState,
useRequiredDevice,
useT,
} from '@autumnbox/sdk/hooks';
import type { AutumnApp } from '@autumnbox/sdk';
import { DevicePerfMonitorService } from '../services/DevicePerfMonitorService';
const PerfMonitorView: React.FC = () => {
const device = useRequiredDevice();
const monitor = useService(DevicePerfMonitorService);
const [snapshot] = useServiceState(monitor.latestSnapshot);
const [isMonitoring] = useServiceState(monitor.monitoring);
const [history] = useServiceState(monitor.history);
const title = useT('perf-monitor.title');
useEffect(() => {
monitor.start(device, 3000);
return () => monitor.stop();
}, [device, monitor]);
const memPercent = snapshot
? Math.round(((snapshot.memTotal - snapshot.memAvailable) / snapshot.memTotal) * 100)
: 0;
return (
<div style={{ padding: 24 }}>
<h2>{title}</h2>
<Tag color={isMonitoring ? 'green' : 'default'}>
{isMonitoring ? '监控中' : '已停止'}
</Tag>
<Tag>采样: {history.length} 条</Tag>
{snapshot && (
<Row gutter={16} style={{ marginTop: 16 }}>
<Col span={8}>
<Card>
<Statistic title="CPU 使用率" value={snapshot.cpuUsage} suffix="%" />
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic title="内存使用率" value={memPercent} suffix="%" />
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic
title="可用内存"
value={Math.round(snapshot.memAvailable / 1024)}
suffix="MB"
/>
</Card>
</Col>
</Row>
)}
<Button
style={{ marginTop: 16 }}
type={isMonitoring ? 'default' : 'primary'}
onClick={() => (isMonitoring ? monitor.stop() : monitor.start(device))}
>
{isMonitoring ? '停止监控' : '开始监控'}
</Button>
</div>
);
};
export const PerfMonitorApp: AutumnApp = {
id: 'perf-monitor',
title: 'perf-monitor.title',
shallSelectAdbDevice: true,
component: PerfMonitorView,
};
# 国际化
resources/
lang/
zh-CN.json
en-US.json
// resources/lang/zh-CN.json
{
"perf-monitor.title": "性能监视器"
}
// resources/lang/en-US.json
{
"perf-monitor.title": "Performance Monitor"
}
# 构建与运行
cd my-plugin
pnpm run build # autumnbox-sdk build → 自动发现 DevicePerfMonitorService + PerfMonitorApp
SDK 会生成 src/__entry__.ts,其中包含:
context.serviceContainer.registerService(DevicePerfMonitorService);
disposers.push(context.registerApp(PerfMonitorApp));
构建产物为 .atmb 文件,放入 builtin-plugins/ 目录即可加载。
# 15. 速查表
| 需求 | API |
|---|---|
| 注册 Service 类 | container.registerService(MyService) |
| 注册预创建实例 | container.registerInstance('name', obj) |
| 按类获取(类型安全) | container.getService(MyService) |
| 按名获取 | container.getService('name') |
| 获取同名全部实例 | container.getServices('name') |
| React 中获取 Service | useService(MyService) |
| 订阅响应式状态 | useServiceState(service.state) 或 useServiceState(MyService, 'state') |
| 国际化翻译 | useT('key') |
| 获取目标设备 | useRequiredDevice() |
| Shell 带 exit code | useShell().exec(device, cmd) |
| 创建只读状态 | createReadonlyState<T>(initial) |
| 创建可变状态 | createState<T>(initial) |