我最早在 Tachiyomi/Mihon 上看到插件化的实现。Mihon 是一个漫画阅读容器,而它的漫画源是以插件的形式提供内容的。后来使用 fcitx-android 的时候,又一次看到了插件化的功能实现。
虽然我不是 android 开发者,但是一直对这个设计充满好奇。于是在实现 Kime 的时候,想到联想词的模型、语音转文本功能、表情包功能应该是多样化的。比如,联想词在我这里需要在数据集上加入我自己的聊天记录,语音转文本功能可能需要不同的 ASR 模型,表情包也可能每个人使用的都不一样。
这正好可以被我用来试验插件化的功能。
研究
我对fcitx-android 和mihon的插件系统都做了一些研究。 事实上,我是优先选择fcitx-android做为借鉴对象,因为同为输入法,想来应该很简单吧?
并没有。
这里说大概说一下android 插件的实现思路,简单来说,就是 classloader。通过classloader 加载插件代码,如果有资源包,则宿主assets 管理去把资源包复制过来。 这是一种同进程交互的常规做法。与其说是插件,不如说是假装成apk的资源包,因为它没有自己的生命周期。
还有一种更复杂的事跨进程通信的,插件有自己的生命周期,但是是否可行我没试过。
由于权限问题,2个apk的通信有非常非常多的限制,所以 classloader 能干的事非常的少。
插件化思路
Mihon
Mihon(继承自 Tachiyomi)采用代码加载型插件架构,扩展本质上是独立编译的 APK,通过 DexClassLoader 在运行时动态加载代码类。
Fcitx5 Android
Fcitx5 Android 插件系统相对比较复杂,它有2类插件:数据插件、服务插件。
数据(native 库文件)插件: 纯数据提供者,像:rime、anthy、hangul、chewing 等输入法引擎插件仅在启动时将数据合并到主应用数据目录。
服务插件:通过 IPC 与主应用实时交互,比如clipboard-filter。
插件对比
| Fcitx5 Android | Mihon |
|---|---|
| 1. PackageManager 查询 | 1. PackageManager 查询 |
| 2. 解析 plugin.xml | 2. 检查 Feature 声明 |
| 3. API 版本验证 | 3. 签名哈希提取 |
| 4. 解析 DataDescriptor | 4. 信任列表验证 |
| 5. 合入 DataHierarchy | 5. 版本范围检查 |
| 6. 计算 Diff | 6. 创建 ClassLoader |
| 7. 执行文件操作 | 7. 反射加载源类 |
| 8. (可选) 绑定 IPC Service | 8. 实例化 Source |
| 失败类型: | 失败类型: |
| --------------------------------- | ------------------------------ |
| - 路径冲突 | - Untrusted (签名未信任) |
| - 元数据解析错误 | - Error (加载失败) |
| - API 不兼容 |
通过对比,发现 Mihon 方案更适合我。
实验
kime 1的时候,我有三大功能想插件化:
- 表情,斗图
- 联想词
- 语音转文本
联想词想插件化的原因很简单,因为我需要训练属于我自己联想词模型,由于训练数据会带上我的私人数据,这肯定不适合开源出来。而可能别人也有类似的需要呢?插件化不错的选择。
语音转文本说到底就是ASR模型,无论是线上api接口还是纯本地的小模型,也很适合做成插件。
困难
kime 1的时候,在实现表情和联想词的时候就已经碰到了很多困难,再实现语音转文本时,已经变得不可持续。
比如 Pro Guard 规则和R8混淆,如果不开启R8混淆,安装包会非常的大,而如果开启R8混淆,插件和主app之间会因为名称混淆造成找不到类的问题。还有一些插件权限,依赖路径冲突都需要严格对齐。这些问题让开发插件简单化变成不可能的事。如果这些问题无法和主app 解耦,那插件化没有一点意义。
但是也明白一个事情,想要简单化,插件本身就不能引用其他依赖,只能依赖主app的依赖。
放弃
经过一番挣扎, 最终 ,kime 2只保留了斗图表情插件,联想词和语音转文本都内置进了主app。
除了上面的原因,还有一个原因是,联想词和语音转文本的实现是比较复杂的,它不仅需要开发插件的人会android ,还得会模型推理。如果联想词和语音转文本都引入了onnxruntime,而版本还不一样,主app又如何应对? Pro Guard又如何应对? 而且多个同类型的插件又如何处理?
相反,表情插件归根到底只是个资源包,假装成apk只是为了方便安装和卸载,同时也不会有什么复杂的逻辑,就很适合插件化。