Appearance
开发插件
集成使用
插件本质是一个 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);
}编译插件
点击编译项目后会得到:
- 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- 插件 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);