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.0" apply false
}

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

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

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

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

...

添加插件 sdk 依赖

kotlin
// JSView插件依赖(请使用最新已发布版本)
annotationProcessor("com.qcode.jsviewplugin:jsview-plugin-annotation-processor:1.0.2")
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.2")
    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 = "我的插件",
        // 插件版本名
        versionName = "1.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 添加自己的接口

}

添加给 js 调用的接口

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

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

编译插件

点击编译项目后会得到:

  1. npm 桥接包(目录名 = 注解 pluginName):
text
./MyPlugin/app/build/generated/ap_generated_sources/debug/out/npm/MyPlugin/
├── package.json
├── README.md
├── index.js
├── plugin-info.js
├── plugin-runtime.js
└── plugin-bridge.js
  1. 插件 apk

./MyPlugin/app/build/outputs/apk/release/app-release-unsigned.apk

使用插件

安装 npm 包

将生成的 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:版本检查失败,比如已经加载了其他版本,此版本不能再加载。
 *
 */
})

注意: 正式上线一般不需要手动配置下载地址和 md5,插件会按包名/版本走后台下载。本地调试时可设置:

javascript
import { PluginInfo } from "MyPlugin";

PluginInfo.downloadUrl = "http://192.168.2.179:8092/.../plugin.dat";
PluginInfo.md5 = "e7c9ef87c1bd283c2ab4be10852e2358";

未手动指定时,运行时会按 pluginBaseUrl 拼装下载地址。文件名规则:

  • 32 位设备:{pluginName}-{versionCode}.dat(如 MyPlugin-1.dat
  • 64 位设备(window.JsvCoreApi.ProcessType === "a64"):{pluginName}-a64-{versionCode}.dat(如 MyPlugin-a64-1.dat

生成的 PluginInfo 主要字段示意:

javascript
{
  packageName: "com.abc.myplugin",
  pluginName: "MyPlugin",      // 英文名 / 下载文件名 / npm 包名
  name: "我的插件",             // 展示名
  version: "1.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);