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.1")
implementation("com.qcode.jsviewplugin:jsview-plugin-base:1.0.1")

文件./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.0")
    implementation("com.qcode.jsviewplugin:jsview-plugin-base:1.0.0")
}

创建插件

创建一个类继承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(
        // 插件名称
        pluginName = "我的插件",
        // 插件版本名
        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);
}

编译插件

点击编译项目后就会得到两个文件: ./MyPlugin/app/build/generated/ap_generated_sources/debug/out/js/MyJSViewPlugin.js
./MyPlugin/app/build/outputs/apk/release/app-release-unsigned.apk

使用插件

  • 将 apk 文件部署到 http 服务器上。
  • 修改 MyJSViewPlugin.js 文件里的 apk 文件下载地址跟apk 文件 md5.
javascript
let PluginInfo={
    //插件下载地址
    downloadUrl:"http://192.168.2.179:8092/app/build/outputs/apk/release/app-release-unsigned.apk", 
    // apk 文件的md5
    md5:"e7c9ef87c1bd283c2ab4be10852e2358", 
    packageName:"com.abc.myplugin", // 插件包名
    name:"我的插件", // 插件名
    version:"1.0",  //插件需要的版本号
    versionCodeMin:1,
    versionCodeMax:1,
    bridgeName:"jPMyJSViewPlugin", //插件bridge注册到jsview的名称
    className:"com.sample.myplugin.MyJSViewPluginCreator",  //插件初始化类名称
    initMethod:"createInstance",   //插件初始化方法
    listener:"__MyJSViewPluginPluginLoadResult",  //插件加载结果回调
    listener2: "__MyJSViewPluginPluginStatus",
};

...

注意: $\color{red}{正式上线一般不需要配置下载地址和md5}$ ,插件会根据包名和版本自动去对应后台下载。具体插件管理后台联系相关人员。

  • 小程序集成MyJSViewPlugin.js

MyJSViewPlugin.js放到自己的小程序项目中并引用。

javascript
import { MyJSViewPlugin } from "./MyJSViewPlugin";
  • 加载插件
javascript
// 调用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:版本检查失败,比如已经加载了其他版本,此版本不能再加载。
 *
 */
})
  • 调用插件接口
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.java 封装了所有插件管理模块支持的功能

java
/******************************************************************************
 * 本文件封装了插件管理模块支持的接口。使用者调用前需要先初始化,把插件管理模块初始化插件时
 * 传入的参数传给构造函数。
 ******************************************************************************/
package com.qcode.jsvplayer;

import android.os.Bundle;
import android.util.Log;
import android.widget.FrameLayout;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;

public class JsviewFunctionEnc {
    private String TAG = "JsviewFunctionEnc";
    private Map mParam;

    public interface HoleStyleChangeListener {
        void onEvent(Bundle data);
    }

    public interface NetStateChangeListener {
        void onEvent(int status);
    }

    JsviewFunctionEnc(Map param){
        mParam = param;
    }

    /***************************************
     * 注册js interface
     * @param bridge js interface对象
     * @return {int} 0表示成功,-1表示失败
     ***************************************/
    public int addJsvBridge(Object bridge){
        if(mParam.containsKey("jsView") && mParam.containsKey("bridgeName") && mParam.containsKey("addJsvBridge")) {
            String bridge_name = (String) mParam.get("bridgeName");
            Method add_bridge = (Method) mParam.get("addJsvBridge");
            Object js_view = (Object) mParam.get("jsView");
            try {
                add_bridge.setAccessible(true);//调用方法前,设置访问标志
                add_bridge.invoke(js_view, bridge_name, bridge);//使用方法
                return 0;
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }else {
            Log.d(TAG, "Add bridge failed, no enough params!");
        }

        return -1;
    }

    /*********************************************
     * 发送事件给js端
     * @param key {string}   事件名称
     * @param value {string} json string,事件值
     * @return {int} 0表示成功,-1表示失败
     *********************************************/
    public int emitEvent(String key, String value){
        if(mParam!=null && mParam.containsKey("jsView") && mParam.containsKey("emitEvent")) {
            Method emit_event = (Method) mParam.get("emitEvent");
            Object js_view = (Object) mParam.get("jsView");

            try {
                emit_event.setAccessible(true);
                emit_event.invoke(js_view, key, value);
                return 0;
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }else {
            Log.d(TAG, "Send event failed, no enough params!");
        }

        return -1;
    }

    /*************************************************
     * 调用js回调函数
     * @param callback {string} js注册下来的回调函数名称
     * @param value {string} json string,回调函数参数
     * @return {int} 0表示成功,-1表示失败
     *************************************************/
    public int evaluateJsFunction(String callback, String value){
        if(mParam!=null && mParam.containsKey("jsView") && mParam.containsKey("evaluateJsFunction")) {
            Method evaluateJsFunction = (Method) mParam.get("evaluateJsFunction");
            Object js_view = (Object) mParam.get("jsView");

            try {
                evaluateJsFunction.setAccessible(true);
                evaluateJsFunction.invoke(js_view, callback, value);
                return 0;
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }else {
            Log.d(TAG, "Send callback failed, no enough params!");
        }

        return -1;
    }

    /*********************************************************************
     * 获取jsview(小程序所有在view)的back view,插件支持back view和front view,
     * 其中back view在jsview后面,需要给jsview打个洞,才可以透出来,一般用于播放
     * 器插件等场景,front view在jsview前面,直接盖在jsview上面。
     * @return {FrameLayout} back view对象
     *********************************************************************/
    public FrameLayout getBackgroundRootView(){
        if(mParam.containsKey("jsView") && mParam.containsKey("getRootView")) {
            Method get_rootview = (Method) mParam.get("getRootView");
            Object js_view = (Object) mParam.get("jsView");
            try {
                get_rootview.setAccessible(true);
                return (FrameLayout) get_rootview.invoke(js_view, true);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }else {
            Log.d(TAG, "Get background root view failed, no enough params!");
        }

        return null;
    }

    /*********************************************************************
     * 获取jsview(小程序所有在view)的front view,插件支持back view和front view,
     * 其中back view在jsview后面,需要给jsview打个洞,才可以透出来,一般用于播放
     * 器插件等场景,front view在jsview前面,直接盖在jsview上面。
     * @return {FrameLayout} front view对象
     *********************************************************************/
    public FrameLayout getFrontRootView(){
        if(mParam.containsKey("jsView") && mParam.containsKey("getRootView")) {
            Method get_rootview = (Method) mParam.get("getRootView");
            Object js_view = (Object) mParam.get("jsView");
            try {
                get_rootview.setAccessible(true);
                return (FrameLayout) get_rootview.invoke(js_view, false);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }else {
            Log.d(TAG, "Get front root view failed, no enough params!");
        }

        return null;
    }

    /*************************************
     * 通知jsview释放焦点,用于back view或者
     * front view需要焦点的场景
     *************************************/
    public void releaseFocus(){
        if(mParam.containsKey("jsView") && mParam.containsKey("releaseFocus")) {
            Method release_focus = (Method) mParam.get("releaseFocus");
            Object js_view = (Object) mParam.get("jsView");
            try {
                release_focus.setAccessible(true);
                release_focus.invoke(js_view);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }else {
            Log.d(TAG, "Release focus failed, no enough params!");
        }
    }

    /***********************************************
     * 监听在jsview上打的洞的状态变化,主要是尺寸的变化
     * @param track_id {string} 打的洞的索引
     * @param listener 回调函数
     * @return {int} 0表示成功,-1表示失败
     ***********************************************/
    public int registerHoleStyleChange(String track_id, HoleStyleChangeListener listener){
        if(mParam.containsKey("jsView") && mParam.containsKey("registerHoleStyleChange")) {
            Method register_hole_style_change = (Method) mParam.get("registerHoleStyleChange");
            Object js_view = (Object) mParam.get("jsView");

            try {
                register_hole_style_change.setAccessible(true);
                try {
                    register_hole_style_change.invoke(js_view, track_id, listener.getClass().getMethod("onEvent", Bundle.class), listener);
                    return 0;
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }else {
            Log.d(TAG, "Register hole style change failed, no enough params!");
        }

        return -1;
    }

    /**************************************
     * 取消对所打的洞的变化的监听
     * @param track_id {string} 打的洞的索引
     * @return {int} 0表示成功,-1表示失败
     **************************************/
    public int unregisterHoleStyleChange(String track_id){
        if(mParam.containsKey("jsView") && mParam.containsKey("unregisterHoleStyleChange")) {
            Method unregister_hole_style_change = (Method) mParam.get("unregisterHoleStyleChange");
            Object js_view = (Object) mParam.get("jsView");

            try {
                unregister_hole_style_change.setAccessible(true);
                unregister_hole_style_change.invoke(js_view, track_id);
                return 0;
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }else {
            Log.d(TAG, "Unregister hole style change failed, no enough params!");
        }

        return -1;
    }


    /*****************************************
     * 注册网络状态变化监听
     * @param name {string} key,用于注销时传入
     * @param listener   回调函数
     * @return {int} 0表示成功,-1表示失败
     *****************************************/
    public int registerNetStateChange(String name, NetStateChangeListener listener){
        if(mParam.containsKey("jsView") && mParam.containsKey("registerNetStateChange")) {
            Method register_net_state_change = (Method) mParam.get("registerNetStateChange");
            Object js_view = (Object) mParam.get("jsView");

            try {
                register_net_state_change.setAccessible(true);
                try {
                    register_net_state_change.invoke(js_view, name, listener.getClass().getMethod("onEvent", Integer.class), listener);
                    return 0;
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }else {
            Log.d(TAG, "Register net state change failed, no enough params!");
        }

        return -1;
    }

    /**********************************************
     * 注销网络状态变化监听
     * @param name {string }key,使用注册时传入的值
     * @return {int} 0表示成功,-1表示失败
     **********************************************/
    public int unregisterNetStateChange(String name){
        if(mParam.containsKey("jsView") && mParam.containsKey("unregisterNetStateChange")) {
            Method unregister_net_state_change = (Method) mParam.get("unregisterNetStateChange");
            Object js_view = (Object) mParam.get("jsView");

            try {
                unregister_net_state_change.setAccessible(true);
                unregister_net_state_change.invoke(js_view, name);
                return 0;
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }else {
            Log.d(TAG, "Unregister net state change failed, no enough params!");
        }

        return -1;
    }

    /*********************************
     * 获取电视小程序渠道号
     * @return {string} 渠道号
     *********************************/
    public String getMarketCode(){
        if(mParam.containsKey("jsView") && mParam.containsKey("getMarketCode")) {
            Method get_market_code = (Method) mParam.get("getMarketCode");
            Object js_view = (Object) mParam.get("jsView");

            try {
                get_market_code.setAccessible(true);
                return (String)get_market_code.invoke(js_view);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }else {
            Log.d(TAG, "Get market code failed, no enough params!");
        }

        return null;
    }


    /*****************************************
     * 获取电视小程序唯一标识,uuid
     * @return {string} uuid
     *****************************************/
    public String getUUID(){
        if(mParam.containsKey("jsView") && mParam.containsKey("getUUID")) {
            Method get_uuid = (Method) mParam.get("getUUID");
            Object js_view = (Object) mParam.get("jsView");

            try {
                get_uuid.setAccessible(true);
                return (String)get_uuid.invoke(js_view);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }else {
            Log.d(TAG, "Get uuid failed, no enough params!");
        }

        return null;
    }

    /***************************
     * 获取以太网mac地址
     * @return {string} mac地址
     ***************************/
    public String getEthMac(){
        if(mParam.containsKey("jsView") && mParam.containsKey("getEthMac")) {
            Method get_eth_mac = (Method) mParam.get("getEthMac");
            Object js_view = (Object) mParam.get("jsView");

            try {
                get_eth_mac.setAccessible(true);
                return (String)get_eth_mac.invoke(js_view);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }else {
            Log.d(TAG, "Get ethernet mac failed, no enough params!");
        }

        return null;
    }

    /******************************
     * 获取wifi mac地址
     * @return {string} mac地址
     ******************************/
    public String getWifiMac(){
        if(mParam.containsKey("jsView") && mParam.containsKey("getWifiMac")) {
            Method get_wifi_mac = (Method) mParam.get("getWifiMac");
            Object js_view = (Object) mParam.get("jsView");

            try {
                get_wifi_mac.setAccessible(true);
                return (String)get_wifi_mac.invoke(js_view);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }else {
            Log.d(TAG, "Get wifi mac failed, no enough params!");
        }

        return null;
    }

    /*********************************************************
     * 获取调用小程序的信息,值为小程序的appname+"|"+小程序链接的md5;
     * 主要用于识别加载插件的小程序的信息。
     * @return {string} refeffer,加载者信息
     *********************************************************/
    public String getReferrer(){
        if(mParam.containsKey("jsView") && mParam.containsKey("getReferrer")) {
            Method get_referrer = (Method) mParam.get("getReferrer");
            Object js_view = (Object) mParam.get("jsView");

            try {
                get_referrer.setAccessible(true);
                return (String)get_referrer.invoke(js_view);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }else {
            Log.d(TAG, "Get referrer failed, no enough params!");
        }

        return null;
    }

    /**********************************************
     * 校验小程序签名
     * @param signKey {string} 支持的小程序签名
     * @return {boolean} true表示通过,false表示失败
     **********************************************/
    public boolean checkSignKey(String signKey){
        if(mParam.containsKey("jsView") && mParam.containsKey("checkSignKey")) {
            Method check_sign_key = (Method) mParam.get("checkSignKey");
            Object js_view = (Object) mParam.get("jsView");

            try {
                check_sign_key.setAccessible(true);
                return (boolean)check_sign_key.invoke(js_view, signKey);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }else {
            Log.d(TAG, "Unregister net state change failed, no enough params!");
        }

        return false;
    }
}

支持的接口如下

addJsvBridge

注册js interface,插件模块将js interface接口进行注册,以便js端可以正确访问插件提供的js穿透接口。

emitEvent

插件发送事件给js端,一般用于状态的通报。

evaluateJsFunction

调用插件js端注册的回调函数,一般用于异步调用完成后的结果通知。

getBackgroundRootView

插件管理模块给插件提供了back view和front view,其中back view显示在jsview(加载小程序的view)后面,front view显示在jsview的前面。插件如果需要进行界面展示,可以根据不同场景需求,选择对应的view。
本接口获取back view

getFrontRootView

获取front view

releaseFocus

如果back view或者front view需要获取焦点,通知jsview失去焦点。

registerHoleStyleChange

对于back view,需要在jsview上打洞,back view才可见,本接口用于通知洞的状态变化。

unregisterHoleStyleChange

注销jsview洞的状态变化通知函数。

registerNetStateChange

注册网络状态变化通知接口。

unregisterNetStateChange

注销网络状态变化通知接口。

getMarketCode

获取电视小程序apk的渠道号。

getUUID

获取终端唯一标识。

getEthMac

获取终端以太网(有线)mac地址。

getWifiMac

获取终端wifi mac(无线mac)地址。

getReferrer

获取加载插件的小程序信息,值为小程序的appname+"|"+小程序链接(url)的md5。

checkSignKey

校验小程序签名。

插件加载的 UI 显示

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

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

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