Skip to content

开发插件

集成使用

插件本质是一个 apk 文件。所以在一个空的 Android 项目上开发即可。

创建一个空的Android项目

如果已经有空项目请忽略

使用 AndroidStudio 创建一个空的项目。
这边 AGP(Android Gradle Plugin) 使用的是 8.0 版本
编译脚本使用 Kotlin DSL(build.gradle.kts)

这边的主要目录结构

shell
MyPlugin/
├── app/
 ├── src/main/java/com.sample.myplugin/
 └──MyJSViewPlugin.java
 └──build.gradle.kts
├── gradle/
├── settings.gradle.kts
└── build.gradle.kts

接下来的文档使用以上环境来讲解。如果自身项目环境有差异请自行调整,随机应变。

添加 Maven 仓库

插件的 SDK 被上传到一个私有的 Maven 上,所以要额外添加一个仓库地址。

kotlin
maven {
    // 仓库地址
    url = uri("http://nexus.cluster.qcast.cn/repository/maven-releases/")
    // 高版本 gradle 要求地址是 https 协议,添加这个忽略协议要求
    isAllowInsecureProtocol = true
}

文件./MyPlugin/settings.gradle.kts

kotlin
pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
        maven {
            url = uri("http://nexus.cluster.qcast.cn/repository/maven-releases/")
            isAllowInsecureProtocol = true
        }
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven {
            url = uri("http://nexus.cluster.qcast.cn/repository/maven-releases/")
            isAllowInsecureProtocol = true
        }
    }
}

rootProject.name = "MyPlugin"
include(":app")

添加gradle插件依赖

文件./MyPlugin/build.gradle.kts:

kotlin
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    id("com.android.application") version "8.1.1" apply false
    id("jsview-plugin-asm") version "1.0.3" apply false
}

对于老版本的 Gradle 是这样配置:

groovy
classpath "com.qcode.jsviewplugin:jsview-plugin-asm:1.0.3"

在文件./MyPlugin/app/build.gradle.kts中添加:

kotlin
plugins {
    id("com.android.application")
    id("jsview-plugin-asm")
}

...

说明

jsview-plugin-asm 1.0.3+ 会按 -PAbis 开启 ABI Split、把 APK 复制为与运行时 getPluginFileName() 一致的 .dat,并把 npm 包输出到 build/outputs/npm/{pluginName}/(md5/md564 写在这份包里)。请勿再在 defaultConfig.ndk.abiFilters 里写死 ABI(与 splits 互斥;插件会自动清空)。

1.0.2+ 支持 @JSViewPlugin 写在 project 依赖的 library 模块上。jsview-plugin-asm 仍加在最终打 APK 的 application 模块上。

添加插件 sdk 依赖

kotlin
// JSView插件依赖(请使用最新已发布版本)
annotationProcessor("com.qcode.jsviewplugin:jsview-plugin-annotation-processor:1.0.5")
implementation("com.qcode.jsviewplugin:jsview-plugin-base:1.0.3")

文件./MyPlugin/app/build.gradle.kts:

kotlin
plugins {
    id("com.android.application")
    id("jsview-plugin-asm")
}

android {
    namespace = "com.sample.myplugin"
    compileSdk = 33

    ...
}

dependencies {
    
    ...
    
    // JSView插件依赖
    annotationProcessor("com.qcode.jsviewplugin:jsview-plugin-annotation-processor:1.0.5")
    implementation("com.qcode.jsviewplugin:jsview-plugin-base:1.0.3")
}

创建插件

创建一个类继承JSViewPluginBase 并且使用 JSViewPlugin注解这个类。

java
package com.sample.myplugin;

import com.qcode.jsviewpluginbase.JSViewPlugin;
import com.qcode.jsviewpluginbase.JSViewPluginBase;
import com.qcode.jsviewpluginbase.JsviewFunctionEnc;

import java.util.Map;

@JSViewPlugin(
        // 插件英文名:写入 PluginInfo.pluginName,用于下载文件名等(如 MyPlugin-1.dat)
        // 同时作为生成的 npm 包名 / 目录名
        pluginName = "MyPlugin",
        // 插件展示名(可中文),写入 PluginInfo.name
        name = "我的插件",
        // 插件版本名(会写入生成 npm 包 package.json 的 version,须为合法 semver)
        versionName = "1.0.0",
        // 插件版本号
        versionCode = 1,
        // 插件包名,类似 apk 的包名,用于与其他插件区分。
        packageName = "com.abc.myplugin"
)
public class MyJSViewPlugin extends JSViewPluginBase {
    public MyJSViewPlugin(Map<String, Object> param, JsviewFunctionEnc funcEncRef, String pluginName) {
        super(param, funcEncRef, pluginName);
    }

    // TODO 添加自己的接口

}

注意:versionName 与 npm 版本格式

注解 versionName 会原样写入生成 npm 桥接包的 package.jsonversion
npm 要求该字段为合法 semver(如 1.0.01.2.3)。若写成 1.01 等非标准格式,小程序侧执行 npm install / npm ci 时会报:

text
npm error Invalid Version: 1.0

请始终使用至少三段式版本号(主.次.修订),例如 1.0.0

添加给 js 调用的接口

MyJSViewPlugin.java创建自己的方法,用@JavascriptInterface注解,
参数和返回值类型只支持基本数据类型。

java
@JavascriptInterface
public void testFunction(String text){
    Log.d("MyPlugin", "testFunction: "+text);
}

编译插件

推荐使用 asm 插件提供的任务(会打 APK、输出 .dat、产出最终 npm):

bash
# 默认只打 32 位(armeabi-v7a)
./gradlew :app:assembleJsViewPlugin

# 只 32 / 只 64 / 两个都打
./gradlew :app:assembleJsViewPlugin -PAbis=32
./gradlew :app:assembleJsViewPlugin -PAbis=64
./gradlew :app:assembleJsViewPlugin -PAbis=32,64

# debug
./gradlew :app:assembleJsViewPluginDebug -PAbis=32,64

-PAbis 支持:armeabi-v7a / arm64-v8a,以及别名 32/v7a/arm3264/a64/arm64。未传时默认按 32 位处理。

也可继续用 assembleRelease / assembleDebug,同样会触发 .dat 复制与最终 npm 输出(同样受 -PAbis 控制)。

编译完成后会得到:

  1. npm 桥接包(目录名 = 注解 pluginName;md5/md564 已按本次打出的 APK 写入):
text
./MyPlugin/app/build/outputs/npm/MyPlugin/
├── package.json
├── README.md
├── index.js
├── plugin-info.js      # md5 / md564 已回填;含 processType
├── plugin-runtime.js
└── plugin-bridge.js
  1. 插件包(.dat,与运行时下载文件名一致)
text
./MyPlugin/app/build/outputs/apk/release/
├── MyPlugin-1.dat              # 32 位(-PAbis 含 32 时)
├── MyPlugin-a64-1.dat          # 64 位(-PAbis 含 64 时)
└── *.apk                       # AGP 原始产物,仍会保留

文件名规则与 plugin-runtime.jsgetPluginFileName() 一致:

  • 32 位:{pluginName}-{versionCodeMax}.dat
  • 64 位:{pluginName}-a64-{versionCodeMax}.dat
-PAbisAPK / .datplugin-info.jsoutputs/npm
未传或仅 32只打 32只写 md5,注释 md564
仅 64只打 64只写 md564,注释 md5
32,64两个都打md5 + md564(分别对应两个包的哈希)

可选关闭自动回填与最终 npm 拷贝:

kotlin
jsviewPlugin {
    updateMd5.set(false)
}

使用插件

安装 npm 包

app/build/outputs/npm/MyPlugin 目录拷到小程序工程后安装:

bash
npm install ./path/to/MyPlugin

或在小程序 package.json 中:

json
{
  "dependencies": {
    "MyPlugin": "file:./path/to/MyPlugin"
  }
}

包内 README.md 也包含安装与调用说明。

引入与加载

javascript
import { MyJSViewPlugin } from "MyPlugin";

// 调用 globalLoadPlugin 接口加载插件
MyJSViewPlugin.globalLoadPlugin((obj)=>{
/**
 * 插件加载状态回调。
 * 回调函数的参数定义如下:
 * object结构,包含status和code两个变量:
 * 1)开始加载插件:
 *    obj.status=1;
 *    obj.code表示是否是首次加载:1:首次加载,此时插件需要经历下载、解压等过程,用时较长,可以考虑给用户相关提示;
 *                             2:非首次加载,此时插件加载过程很短,可以不用出现用户提示界面。
 * 2)插件加载中,加载新插件时上报此状态完整状态,加载旧插件时,只上报dexload完成状态(code=3):
 *    obj.status=2;
 *    obj.code表示加载过程中的状态:1:插件下载进度,目前只上报下载结束;2:插件解压完成;3:插件dexload完成。
 *    obj.progress:当code=1时(下载进度),progress为实际下载进度,百分制,目前只有100(100%,完成状态)。
 * 3)插件加载成功
 *    obj.status=3;
 *    obj.code无效。
 * 4)插件加载失败
 *    obj.status=4;
 *    obj.code表示插件加载失败原因:1:插件管理模块不存在(未真正开始加载插件),下面的负值为插件管理模块返回的错误;
 *                              -1:请求插件加载的参数不正确,需要确认构造的PluginInfo内容;
 *                              -2:未找到插件更新链接;
 *                              -3:插件HTTP请求失败;
 *                              -4:插件下载失败;
 *                              -5:插件MD5校验失败;
 *                              -6:解压失败;
 *                              -7:文件大小为0;
 *                              -8:未找到dex文件;
 *                              -9:dex文件load失败;
 *                              -10:插件初始化失败;
 *                              -11:取消插件下载;
 *                              -12:版本检查失败,比如已经加载了其他版本,此版本不能再加载。
 *
 */
})

注意: 正式上线一般不需要手动配置 downloadUrl;未手动指定时,运行时会按小程序启动参数 pluginBaseUrljJsvRuntimeBridge.getPluginBaseUrl())动态拼装。本地调试时可设置:

javascript
import { PluginInfo } from "MyPlugin";

PluginInfo.downloadUrl = "http://192.168.2.179:8092/.../plugin.dat";
PluginInfo.md5 = "e7c9ef87c1bd283c2ab4be10852e2358";
// PluginInfo.md564 = "..."; // 64 位包 md5(可选;未设则 a64 回退用 md5)

pluginBaseUrl 拼装规则(annotation-processor 1.0.5+)

最终 URL:

text
{baseUrl}{packageName}/{fileName}{suffix}[?|&]md5={md5}
说明
baseUrl / suffixpluginBaseUrl 支持 |split|:前半为 CDN 根路径(末尾自动补 /),后半为可选后缀(如 .gz,或 ?token=xxx 鉴权片段)
文件名(32 位){pluginName}-{versionCode}.dat,如 MyPlugin-1.dat
文件名(64 位){pluginName}-a64-{versionCode}.dat,如 MyPlugin-a64-1.datJsvCoreApi.ProcessType === "a64"
md532 位用 PluginInfo.md5;a64 优先 PluginInfo.md564,未设置则回退 md5有值才写入 query;后缀已含 ? 时用 &md5=,避免出现 ?md5=undefined 或双 ?
version(a64)LoadPlugin 前,若 ProcessType === "a64",会把 PluginInfo.version 拼成 1.0.0_a64(已带后缀不重复)。空 downloadUrl 时供 PluginManager 请求 version 服务区分架构
本地目录(PluginManager 1.3.19+)processType 落盘:32 位 {versionCode},a64 为 {versionCode}_a64
备用地址未手动指定 downloadUrl 时,若存在 getBackupPluginBaseUrl(),按其返回的 JSON 数组按相同规则生成 backupDownloadUrl[]

示例:

text
https://cdn.example.com/plugins/com.abc.myplugin/MyPlugin-a64-1.dat?md5=abc123
https://cdn.example.com/plugins/com.abc.myplugin/MyPlugin-1.dat.gz?md5=abc123
https://cdn.example.com/plugins/com.abc.myplugin/MyPlugin-1.dat?token=xxx&md5=abc123

生成的 PluginInfo 主要字段示意:

javascript
{
  // md5 / md564 由 jsview-plugin-asm 打包后写入 build/outputs/npm/{pluginName}/
  // md5: "...",        // 32 位包
  // md564: "...",      // 64 位包
  processType: window.JsvCoreApi.ProcessType,
  packageName: "com.abc.myplugin",
  pluginName: "MyPlugin",      // 英文名 / 下载文件名 / npm 包名
  name: "我的插件",             // 展示名
  version: "1.0.0",
  versionCodeMin: 1,
  versionCodeMax: 1,
  bridgeName: "JPMyJSViewPlugin",
  className: "com.sample.myplugin.MyJSViewPluginCreator",
  initMethod: "createInstance",
  listener: "__MyJSViewPluginPluginLoadResult",
  listener2: "__MyJSViewPluginPluginStatus",
}
  • 调用插件接口
javascript
// 插件加载成功后可以调用 java 中定义好的接口
MyJSViewPlugin.testFunction("来自javascript的调用!")

Android 如何调用 js 的方法

如果 java 部分有些事件发生想通知到 js 可以在插件的 java 代码里添加一个方法然后用@JavascriptFunction注解:

java

/**
 * 调用此方法会通知到 js 层
 * 方法名不限制,但返回值和参数只支持基本数据类型,
 * 方法里不要写逻辑
 */
@JavascriptFunction
public String javaEvent(int event){
    return null;
}

在 js 代码中实现这个方法:

javascript
// 注意:方法名要跟 java 一样,参数数量也要对应上
MyJSViewPlugin.nativeCallback.javaEvent = (event)=>{
    console.log("来自 java 的调用:"+event)
    return "这是来自 js 的字符串";
}

最后在 java 中调用javaEvent接口即可:

java
String str = javaEvent(123);

插件管理模块功能

JsviewFunctionEnc(包名 com.qcode.jsviewpluginbase,依赖 jsview-plugin-base)封装了宿主通过 PluginBaseInterface 暴露给插件的能力。插件基类构造时会注入 JsviewFunctionEnc 实例,业务代码通过 mFuncEncRef 调用即可。

注意

插件 DEX 的 ClassLoader 与宿主不同,不要在插件代码里直接引用宿主的 PluginBaseInterface 及其内部回调类型(如 ScreenShotCallback)。带回调的接口请一律使用 JsviewFunctionEnc 提供的 Listener(内部已用 Proxy 桥接到宿主)。

调用示例:

java
// 获取小程序信息
String url = mFuncEncRef.getMiniAppUrl();
String uuid = mFuncEncRef.getUUID();

// 监听小程序退出 / reload(scene:1=reload,2=release)
mFuncEncRef.registerMiniAppReleaseListener("myPlugin", scene -> {
    // 释放插件资源
});

// 截屏(使用 JsviewFunctionEnc.ScreenShotListener,不要用宿主 ScreenShotCallback)
mFuncEncRef.screenShot((width, height, buffer) -> {
    // 处理像素数据
});

完整接口以 SDK 源码为准;下面按能力分组说明(jsview-plugin-base 1.0.3+)。

Bridge / 事件

方法说明
addJsvBridge(Object bridge)注册 js interface,供 js 调用插件穿透接口
emitEvent(String key, String value)向 js 端发送事件(value 一般为 json string)
evaluateJsFunction(String callback, String value)调用 js 端注册的回调

View / 焦点 / 打洞

方法说明
getBackgroundRootView()获取 back view(在 JsView 后面,需打洞才可见,常用于播放器)
getFrontRootView()获取 front view(盖在 JsView 前面)
releaseFocus()通知 JsView 释放焦点,供 back/front view 抢焦点
registerHoleStyleChange(String trackId, HoleStyleChangeListener)监听打洞区域尺寸等变化
unregisterHoleStyleChange(String trackId)取消打洞监听

网络 / 生命周期

方法说明
registerNetStateChange(String name, NetStateChangeListener)注册网络状态变化(1 连接 / 0 断开)
unregisterNetStateChange(String name)注销网络状态监听
registerMiniAppReleaseListener(String name, MiniAppReleaseListener)注册小程序退出或 reload 监听
unregisterMiniAppReleaseListener(String name)注销上述监听
registerPluginInstance(String name, Object listener)向 JsView 注册插件实例

终端 / 小程序信息

方法说明
getMarketCode()渠道号
getUUID()终端唯一标识
getEthMac() / getWifiMac()有线 / 无线 mac
getReferrer()加载者信息:appname|urlMd5
checkSignKey(String signKey)校验小程序签名
getMiniAppUrl()当前小程序 URL
getMiniAppConfig()宿主小程序基本信息(Bundle)
getEnvironment()运行环境,如 jsview / webview
isUseTexture()是否 TextureView(与宿主保持一致)
isDebugMode()是否调试模式
getExternalObject(String key)按 key 获取宿主注入的外部对象

JsView 能力(1.0.2 起)

方法说明
screenShot(ScreenShotListener)截屏,回调宽高与 IntBuffer 像素
reload()重新加载当前小程序
closeView()关闭当前小程序 View
addFont(String name, Typeface tf)注册字体
applyEmojiFont(Typeface tf)应用 emoji 字体
enableConsoleRecord(int capacity) / disableConsoleRecord()开启 / 关闭 console 录制
getRecordedConsole() / saveRecordedConsole(String fileName)读取 / 落盘录制内容
addKeysMapping(String jsonMap)按键映射
clearDebugFlags() / addDebugFlag(String key, String value)调试标记
preDownloadSdk(String versionWithBranch, SdkReadyListener)预下载指定内核版本
isRevisionReady(String revisionWithBranch)指定内核版本是否已就绪
appendNetTraceHeader(String key, String value)追加网络追踪 Header
configDiskCacheMaxSize(long maxSizeInByte)配置磁盘缓存上限

插件加载的 UI 显示

插件在下载和加载的时候SDK 会负责显示 UI。 如果不想显示 UI 可以使用setDefaultLoadUI 去设置。

java
// 加载插件不要有 UI 显示。
PluginEntity.setDefaultLoadUI(PluginEntity.LOAD_UI_NONE);

// 或者只允许显示插件加载错误的 UI
PluginEntity.setDefaultLoadUI(PluginEntity.LOAD_UI_ERRORS_ONLY);