Android 原生插件开发指引
一、开发环境准备
根据原生插件工具操作指引生成一个多端插件工程项目
参考下方文档,安装 微信开发者工具、JDK 以及 Android Studio(Android SDK)
环境依赖:
- JAVA版本:
JAVA8
<= JAVA 版本 <=JAVA15
- gradle 版本:
6.7.1
- 微信开发者工具请使用
1.06.2311282
及以上的版本
- JAVA版本:
二、插件工程介绍
- 下面以工具生成的多端插件工程项目为例:
- 根目录下的 Android 目录就是原生插件的项目工程
工程目录
.
├── app // 示例应用,基本无需改动,也不建议改动(云构建下您无法修改 app 下的逻辑)
├── build.gradle // Gradle 构建脚本
├── compile // 存放了证书
├── gradle // 存储 Gradle Wrapper 的目录
├── gradle.properties // Gradle 属性文件
├── gradlew // Gradle Wrapper 的启动脚本
├── gradlew.bat // Gradle Wrapper 的启动脚本
├── miniapp.json // 运行应用时的一些配置文件,基本可以不用动
├── miniapp.plugin.json // 插件相关配置,后面具体说明
├── plugin // 插件目录,插件开发主要需要修改该目录代码,后面具体说明
└── settings.gradle // Gradle 设置文件
miniapp.plugin.json
{
"pluginId": "wx45c6d53ff86fd405", // 插件 ID,自动生成
"pluginVersion": "0.0.1", // 插件 版本,构建时需要用到
"debugSaaAVersion": "1.0.0", // 依赖的 saaa sdk 版本(该 sdk 仅包含基本渲染功能用于插件开发,进行插件开发时请勿调用除了插件暴露的 jsapi 外的方法,即wx.request等 wx api 均不支持)
"debugWithAar": false, // 默认为 false,在构建 aar 产物后可以设成 true 进行测试,详情看下方插件构建流程
"pluginPackageName": "com.donut.wx45c6d53ff86fd405" // 自动生成的插件包名
}
plugin 目录
.
├── build.gradle // Gradle 构建脚本
├── consumer-rules.pro
├── gradle.properties
├── libs // 存放依赖的 sdk,sdk 会自动下载
├── proguard-rules.pro
└── src
└── main
├── AndroidManifest.xml
├── java
│ └── com
│ └── donut
│ └── wx45c6d53ff86fd405
│ └── PluginManager.kt // 插件入口文件,后面详细说明
└── resources
└── META-INF
└── services
└── com.tencent.luggage.wxa.SaaA.plugin.NativePluginInterface // 声明注册插件,如果上面的PluginManager类有变更,需要同步修改这里
三、运行多端插件项目
1. 工具上切换到 Android 模式后点运行,会直接拉起 Android Stuido
本地环境要求:
- windows 需要安装在 C 盘 (安装在 C 盘以外的目录请手动打开Android Stuido导入项目)
- mac 可能需要先创建命令行脚本
如果点运行拉起 Android Stuido 失败,你也可以自己手动在 Android Stuido 导入打开项目,不影响后续流程
注:导入的时候有可能会报unsupported Java
请确认你的 Android Studio 里使用的 java 版本:
2. Android Studio 中点击「运行」按钮,即可看到示例工程运行效果。
3. 先点击「加载多端插件」,加载成功后再点击「调用多端插件」,即可通过 vconsole 看到插件方法的返回。
注:1.06.2311282
及以上的版本的开发者工具不需要手动构建Android资源包,开发者工具保持运行时,在Android Studio 中点击「运行」时会自动构建最新的小程序代码包资源
构建日志如图:
如果这里显示 url 为空,请确保 在「设置」->「安全设置」中多端插件服务端口已经开启
四、插件开发
多端应用与多端插件的调用时序
在开始 Android 多端插件的开发前,理解多端应用与多端插件的调用时序将更好帮助开发者进行开发。
请注意:
- Android pluginInstance 的插件实例运行在小程序的子进程中。
- 在此情境下获取的 Activity 是小程序运行时的 Activity。
注册加载流程 sdk 会实现,开发者只需要继承NativePluginInterface,实现需要暴露给 js 侧的方法
import com.tencent.luggage.wxa.SaaA.plugin.NativePluginInterface
import com.tencent.luggage.wxa.SaaA.plugin.SyncJsApi
import com.tencent.luggage.wxa.SaaA.plugin.AsyncJsApi
import org.json.JSONObject
class TestNativePlugin: NativePluginInterface {
private val TAG = "TestNativePlugin"
override fun getPluginID(): String {
android.util.Log.i(TAG, "getPluginID")
return BuildConfig.PLUGIN_ID
}
// 同步方法
// js 侧调用 plugin.mySyncFunc 会触发 test 函数
@SyncJsApi(methodName = "mySyncFunc")
fun test(data: JSONObject?): String {
android.util.Log.i(TAG, data.toString())
return "test"
}
// 异步方法
// js 侧调用 plugin.myAsyncFuncwithCallback 会触发 testAsync 函数
@AsyncJsApi(methodName = "myAsyncFuncwithCallback")
fun testAsync(data: JSONObject?, callback: (data: Any) -> Unit) {
android.util.Log.i(TAG, data.toString())
callback("async testAsync")
}
}
注册同步方法
- 使用
SyncJsApi
注解
参数 | 类型 | 必填 | 说明 |
---|---|---|---|
methodName | String | 是 | 在 js 层可用获取的 pluginInstance.methodName 进行方法调用 |
- 同步方法的入参与出参
参数 | 类型 | 必填 | 说明 |
---|---|---|---|
入参 1 | JSONObject? | 否 | 在 js 层调用 pluginInstance.methodName 时传入的 Object 类型参数 |
入参 2 | Activity | 否 | 小程序当前运行的 Activity,要正常获取该参数debugSaaAVersion 要更新到 1.0.1 及以上版本 |
出参 | 可序列化类型 | 否 | 在 js 层调用 pluginInstance.methodName 的返回 |
- 代码示例
Android 侧的插件同步方法实现
@SyncJsApi(methodName = "mySyncFunc")
fun test(data: JSONObject?): String {
android.util.Log.i(TAG, data.toString())
return "test"
}
如果要获取 activity:
@SyncJsApi(methodName = "mySyncFunc")
fun test(data: JSONObject?, activity: Activity): String {
android.util.Log.i(TAG, data.toString())
val componentName = activity.componentName.toString()
android.util.Log.i(TAG, componentName)
return "test"
}
JS 侧的方法调用
wx.miniapp.loadNativePlugin({
pluginId: 'YOUR_PLUGIN_ID',
success: (plugin) => {
console.log('load plugin success')
const ret = plugin.mySyncFunc({ a: 'hello', b: [1,2] })
console.log('mySyncFunc ret:', ret)
},
fail: (e) => {
console.log('load plugin fail', e)
}
})
注册异步方法
- 使用
AsyncJsApi
注解
参数 | 类型 | 必填 | 说明 |
---|---|---|---|
methodName | String | 是 | 在 js 层可用获取的 pluginInstance.methodName 进行方法调用 |
- 异步方法的入参与出参
参数 | 类型 | 必填 | 说明 |
---|---|---|---|
入参1 | JSONObject? | 是 | 在 js 层调用 pluginInstance.methodName 时传入的第 1 个 Object 类型参数 |
入参2 | 函数 | 是 | 该回调方法支持一个可序列化类型的参数。通过该方法回调传回给 js 层 |
入参3 | Activity | 否 | 小程序当前运行的 Activity,要正常获取该参数debugSaaAVersion 要更新到 1.0.1 及以上版本 |
- 代码示例 Android 侧的插件异步方法实现
@AsyncJsApi(methodName = "myAsyncFuncwithCallback")
fun testAsync(data: JSONObject?, callback: (data: Any) -> Unit) {
android.util.Log.i(TAG, data.toString())
callback("async testAsync")
}
如果要获取 activity:
@AsyncJsApi(methodName = "myAsyncFuncwithCallback")
fun testAsync(data: JSONObject?, callback: (data: Any) -> Unit, activity: Activity) {
android.util.Log.i(TAG, data.toString())
val componentName = activity.componentName.toString()
android.util.Log.i(TAG, componentName)
callback("async testAsync")
}
JS 侧的方法调用
wx.miniapp.loadNativePlugin({
pluginId: 'YOUR_PLUGIN_ID',
success: (plugin) => {
console.log('load plugin success')
plugin.myAsyncFuncwithCallback({ a: 'hello', b: [1,2] }, (ret) => {
console.log('myAsyncFuncwithCallback ret:', ret)
})
},
fail: (e) => {
console.log('load plugin fail', e)
}
})
插件事件监听
- 在 Native 侧通过调用继承于 NativePluginBase 的方法
sendMiniPluginEvent
可向 JS 侧发送事件。
参数 | 类型 | 必填 | 说明 |
---|---|---|---|
param | HashMap | 是 | 在 js 侧的监听方法获得的入参 |
debugSaaAVersion
要更新到1.0.2
及以上版本- 继承
NativePluginBase
- 调用
sendMiniPluginEvent
,入参类型是HashMap
import com.tencent.luggage.wxa.SaaA.plugin.NativePluginBase
class TestNativePlugin: NativePluginBase(), NativePluginInterface {
private val TAG = "TestNativePlugin"
override fun getPluginID(): String {
android.util.Log.e(TAG, "getPluginID")
return BuildConfig.PLUGIN_ID
}
@AsyncJsApi(methodName = "myAsyncFuncwithCallback")
fun testAsync(data: JSONObject?, callback: (data: Any) -> Unit) {
android.util.Log.i(TAG, data.toString())
// 有需要的时候向 js 发送消息
val values1 = HashMap<String, Any>()
values1["status"] = "testAsync start"
this.sendMiniPluginEvent(values1)
callback("async testAsync")
// 有需要的时候向 js 发送消息
val values2 = HashMap<String, Any>()
values2["status"] = "testAsync end"
this.sendMiniPluginEvent(values2)
}
}
- 在 JS 侧可使用插件实例的
onMiniPluginEvent
方法注册监听
参数 | 类型 | 必填 | 说明 |
---|---|---|---|
callback | Function | 是 | Native 侧向 JS 侧发送事件时触发的回调,支持注册多个回调 |
- 在 JS 侧可使用插件实例的
offMiniPluginEvent
取消监听。
参数 | 类型 | 必填 | 说明 |
---|---|---|---|
callback | Function | 否 | 取消监听;当未指定需要取消的回调时,取消所有监听回调 |
JS 侧的方法调用
const listener1 = (param) => {
console.log('onMiniPluginEvent listener1', param)
}
const listener2 = (param) => {
console.log('onMiniPluginEvent listener2', param)
}
wx.miniapp.loadNativePlugin({
pluginId: 'YOUR_PLUGIN_ID',
success: (plugin) => {
plugin.onMiniPluginEvent(listener1)
plugin.onMiniPluginEvent(listener2)
}
})
在主进程执行代码
debugSaaAVersion
要更新到1.0.6
及以上版本
部分 sdk 要求必须在主进程初始化代码,封装了NativePluginMainProcessTask
用于方便在主进程执行,详细参数见下面代码例子
- 首先需要继承 NativePluginMainProcessTask
请注意,下面例子里的
valToSync1
和valToSync2
只是用于跨进程传输的数据字段,如果不需要传输,可以直接删除
import com.tencent.luggage.wxa.SaaA.plugin.NativePluginMainProcessTask
import kotlinx.android.parcel.Parcelize
@Parcelize
class TestTask(private var valToSync1: String, private var valToSync2: String) :
NativePluginMainProcessTask() {
private var clientCallback: ((Any) -> Unit)? = null
fun setClientCallback(callback: (data: Any) -> Unit) {
this.clientCallback = callback
}
/**
* 运行在主进程的逻辑,不建议在主进程进行耗时太长的操作
*/
override fun runInMainProcess() {
android.util.Log.e("MainProcess", "runInMainProcess, valToSync1:${valToSync1}, valToSync2:${valToSync2}")
// 如果需要把主进程的数据回调到小程序进程,就赋值后调用 callback 函数
valToSync1 = "runInMainProcess"
this.callback() // callback函数会同步主进程的task数据,并在子进程调用runInClientProcess
}
/**
* 运行在小程序进程的逻辑
*/
override fun runInClientProcess() {
android.util.Log.e("ClientProcess", "valToSync1: ${valToSync1}, valToSync2:${valToSync2}")
this.clientCallback?.let { callback ->
callback(valToSync1)
}
}
override fun parseFromParcel(mainProcessData: Parcel?) {
// 如果需要获得主进程数据,需要重写parseFromParcel,手动解析Parcel
this.valToSync1 = mainProcessData?.readString() ?: ""
this.valToSync2 = mainProcessData?.readString() ?: ""
}
}
- 需要的时候真正执行runInMainProcess
@AsyncJsApi(methodName = "registerPush")
fun registerPush(data: JSONObject?, callback: (data: Any) -> Unit, activity: Activity) {
val testTask = TestTask("test1", "test2")
testTask.setClientCallback(callback)
testTask.execAsync() // 真正执行runInMainProcess
}
requestPermission
debugSaaAVersion
要更新到1.0.6
及以上版本
为了减少在插件侧申请权限时的麻烦,包装了一个requestPermission
方法
@AsyncJsApi(methodName = "registerPush")
fun registerPush(data: JSONObject?, callback: (data: Any) -> Unit, activity: Activity) {
android.util.Log.i(TAG, data.toString())
this.requestPermission(
activity,
arrayOf(Manifest.permission.POST_NOTIFICATIONS)
) { permissions, grantResults ->
if (grantResults != null && grantResults.size > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED
) {
android.util.Log.i(TAG, "PERMISSION_GRANTED, do invoke again");
callback("PERMISSION_GRANTED")
} else {
android.util.Log.e(TAG, "reloadQRCode fail, SYS_PERM_DENIED");
callback("fail!")
}
}
}
startactivityForResult
新增封装startactivityForResult
用于在小程序Activity启动另一个Activity并在完成特定操作后返回结果
debugSaaAVersion
要更新到1.0.8
及以上版本
@AsyncJsApi(methodName = "openActivity")
fun openActivity(data: JSONObject?, callback: (data: Any) -> Unit, activity: Activity) {
android.util.Log.i(TAG, "openActivity")
// 封装后的startActivityForResult
this.startActivityForResult(intent) { resultCode, intent ->
android.util.Log.i(TAG, "resultCode ${resultCode}")
if (resultCode == RESULT_OK) {
val data: Intent? = intent
// 在这里处理结果
if (data != null) {
val resultString = data.getStringExtra("result_key")
android.util.Log.i(TAG,"Result: $resultString")
callback("openActivity ret: ${resultString}")
} else {
callback("openActivity ret: no data")
}
} else {
android.util.Log.e(TAG, "resultCode ${resultCode}")
callback("openActivity ret: nothing")
}
}
}
五、插件构建
构建命令
安卓插件必需构建上传后,才能真正使用
在工具点击构建 -> 构建 Android 插件产物
,会自动执行构建命令./gradlew :plugin:build
,构建完成后会生成一个 localAar 文件夹,最终把 localAar 文件夹内容 copy 到目录 build/android
下,用于后续上传
- 版本号读取的是
miniapp.plugin.json
里的pluginVersion
字段
第三方依赖 sdk处理
- maven 源
如果用户有第三方依赖 sdk(maven 源),可以配置 pom 文件 格式参考:
project.ext.pomDeps = [
"com.tencent.tpns:tpns" : "1.4.0.1-release",
]
tip: 支持 Maven Central,以及下列仓库
- https://repo1.maven.org/maven2/
- https://dl.google.com/dl/android/maven2
- https://developer.huawei.com/repo/
libs 或者 不在上述 maven 源列表内的
请参考使用(fat-aar-android)[https://github.com/kezong/fat-aar-android]把依赖 embed 进 aar 包
apply gradle plugin
接安卓的厂商推送时可能会需要厂商的插件,例如华为厂商可能需要apply plugin: 'com.huawei.agconnect'
,远程构建时可以在project.miniapp.json
通过pluginConfigPath
配置 该路径的配置文件需要是一个合法 json 文件(该文件在云构建时使用),参考格式如下
{
"project": {
// 依赖的配置,可选,构建 app 时会合并到项目根目录build.gradle中(使用时不要复制注释)
"dependencies": [
"com.huawei.agconnect:agcp:1.6.0.300"
]
},
"app": {
// 需要使用的gradle plugin,可选,构建 app 时会生效 (使用时不要复制注释)
"plugins": [
"com.huawei.agconnect"
]
}
}
添加后,会在项目根目录的build.gradle的顶部添加gradle插件的依赖
buildscript {
dependencies {
classpath 'com.huawei.agconnect:agcp:1.6.0.300'
}
}
在 app 目录 build.gradle 文件底部添加 apply plugin
apply plugin: 'com.huawei.agconnect'
如果需要配套上传其他文件,可以参考 Android 配置原生资源
六、插件上传
安卓原生插件必须上传后才能在云构建的项目中使用
- 为了云构建使用插件能正常运行,请在上传前先使用
构建产物
测试一下
- 确保
localAar
文件夹里已经有构建产物 miniapp.plugin.json
文件里新增一个debugWithAar
字段,设置为 true- Android Studio 中点击「运行」,在
build
日志里能看见Use Aar
的输出 - 自测,确保使用构建产物下,逻辑也符合预期
请注意:
- 上传时会上传
build/android
和android/plugin
两个文件夹 - 上传的时候版本号必须和构建版本号(
miniapp.plugin.json
里的pluginVersion
)一致,否则使用会有问题
七、云构建使用插件
在你多端应用的项目里配置需要使用使用的插件 ID
- 在使用插件的多端应用的项目里,Android Sdk 版本需要
>= 1.1.0
- 如果需要
activity
或者事件通知
,Android Sdk 版本请选择1.1.9
及以上
debugSaaAVersion 版本更新
Android Sdk 所有版本请查看Android SDK 更新日志
1.0.6
对应最小Android Sdk Version:1.3.13
A
新增 允许在主进程执行代码A
新增 requestPermission
1.0.7
对应最小Android Sdk Version:1.3.27
A
新增 支持application 生命周期
1.0.8
对应最小Android Sdk Version:1.3.28
A
新增 支持startactivityForResult
常见问题
Q:调试的时候可以,上传之后使用云构建就失败了
- 请先检查是否改动过
app
目录(云构建您无法直接修改 app 目录下逻辑,只能读到插件构建产物) - 请务必先保证使用构建产物自测过(
miniapp.plugin.json
文件debugWithAar
为true
) - 检查下混淆配置有没有问题,可以构建一个 release 版本的 apk 自测下
- 先 clean project(
./gradlew clean
) ./gradlew :app:assembleRelease
然后看build/outpus/apk/arm64/release
下的 apk- 如果需要新增混淆配置,可以在
plugin/consumer-rules.pro
文件内处理(请勿删除该文件原有配置)
- 先 clean project(
- adb logcat 或者 Android Stuido 可以看错误信息,可以看下抛出的错误信息是否有用
Q: Execution failed for task ':plugin:generatePom'
请参考文档检查你的 pom 配置是否配置正确
Q:为什么工程改变源码无效也无法断点?
- 请确认(
miniapp.plugin.json
文件debugWithAar
是不是被设成了true
) - 请确认断点进程是不是选择的是
:wxa_container0
Q:为什么小程序代码修改后在插件工程里看不到更新?
- 请查看构建日志,更新资源是否成功,如不成功,请参考文档确认工具端口是否打开
Q:上传时报错提醒上传的版本与构建版本不一致
- 请确认
build/android
文件夹下是否有符合命名规范的文件 - 确保上传的时候版本号和构建版本号一致,也就是说需要存在以下文件:
build/android/{miniAppPluginId}/${version}/${miniAppPluginId}-${version}.aar