ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

uni-app混合开发实战:Android原生aar插件集成与JS调用指南

uni-app混合开发实战:Android原生aar插件集成与JS调用指南 1. 项目背景与核心价值为什么要在uni-app中调用Java代码作为一名在移动端开发领域摸爬滚打了十多年的老手我见过太多跨平台框架的起起落落。uni-app凭借其“一套代码多端发布”的理念确实为快速开发带来了巨大便利。但做久了就会发现当项目需要深度定制、追求极致性能或者必须使用某些平台独有的硬件能力比如高精度的蓝牙扫描、复杂的音视频编解码、特定的NFC读写协议时纯JavaScript/TypeScript的uni-app就显得有些力不从心了。这时候混合开发就成了必由之路。所谓“混合开发”就是在uni-app这个跨平台框架的“壳”里嵌入由原生语言Android用Java/KotliniOS用Objective-C/Swift编写的“芯”。这个“芯”通常被打包成原生插件在Android平台上最常见的形态就是.aar文件。所以“uni-app调用java代码”这个标题本质上探讨的就是如何将原生能力封装成插件并让uni-app的JavaScript业务层能够无缝调用从而实现“跨平台UI原生高性能/特定能力”的最佳组合。这不仅仅是技术上的缝合更是一种架构上的权衡。它的核心价值在于你无需为了一个复杂功能而放弃uni-app带来的开发效率和多端一致性同时又能在关键路径上获得原生代码的执行效率和硬件直通能力。比如我之前做过一个智能硬件的配网项目配网协议极其复杂且对时序要求苛刻用JS实现不仅困难而且不稳定。最终方案就是用Java写好核心配网逻辑并打包成aar插件由uni-app的界面来触发和展示结果项目得以快速上线且运行稳定。2. 理解aar插件Android原生能力的封装单元在开始动手之前我们必须先搞清楚我们要集成的对象——.aar文件到底是什么。很多刚开始接触混合开发的朋友容易把它和.jar混淆。你可以把.aar理解为Android版的“加强版.jar”。一个标准的.jar文件通常只包含编译后的Java字节码.class文件。而.aarAndroid Archive除了包含字节码还囊括了Android项目特有的资源比如/res/目录下的布局文件layout、图片drawable、字符串values等。/assets/目录下的原始资源文件。AndroidManifest.xml片段可以声明插件自身的组件如Activity、Service和权限。本地库文件.so文件即C/C编译的库。混淆规则文件proguard-rules.pro。这种封装方式使得.aar成为一个自包含的功能模块集成起来比.jar更方便因为它连UI资源都打包好了。在uni-app混合开发中我们最终的目标就是生成一个或多个这样的.aar文件每个文件对应一个或多个需要暴露给JS调用的原生功能。那么一个能给uni-app用的aar插件内部结构是怎样的呢它绝不是一个随便写个Java类打成的包。它需要遵循一套与uni-app框架约定的通信协议。核心是一个继承自UniModule的Java类。这个类中的方法通过特定的注解就可以被JS识别和调用。举个例子假设我们要做一个原生Toast插件// 原生插件Java代码示例ToastModule.java import io.dcloud.feature.uniapp.annotation.UniJSMethod; import io.dcloud.feature.uniapp.bridge.UniJSCallback; import io.dcloud.feature.uniapp.common.UniModule; import android.widget.Toast; import android.content.Context; public class ToastModule extends UniModule { // 必须继承UniModule // UniJSMethod注解表明此方法可被JS同步调用 UniJSMethod(uiThread true) // uiThread true 表示该方法会在UI线程执行 public void showShortToast(String message) { if (mUniSDKInstance ! null mUniSDKInstance.getContext() ! null) { Toast.makeText(mUniSDKInstance.getContext(), message, Toast.LENGTH_SHORT).show(); } } // 支持回调函数 UniJSMethod(uiThread false) // 在非UI线程执行 public void doSomethingHeavy(String param, UniJSCallback callback) { // 模拟耗时操作 String result 处理结果: param; // 通过callback将结果回传给JS if (callback ! null) { callback.invoke(result); // invoke方法传递结果给JS的成功回调 // 如果需要错误回调可以使用 callback.invokeAndKeepAlive 配合特定格式 } } }这个简单的例子揭示了几个关键点1) 继承UniModule2) 使用UniJSMethod注解暴露方法3) 可以通过UniJSCallback与JS进行异步通信。这就是原生插件与uni-app JS引擎对话的基础语言。3. 实操全链路从零构建并集成一个uni-app原生插件aar理论清楚了我们进入最关键的实战环节。我会以一个“获取手机电池信息”的插件为例带你走通从编写、打包到集成的完整流程。这个过程比单纯引用一个第三方aar要复杂因为涉及uni-app原生插件SDK的集成和配置。3.1 环境准备与工程创建首先你需要一个Android开发环境Android Studio和uni-app开发环境HBuilderX。重点在于uni-app原生插件SDK的获取。你需要从uni-app官方插件市场或GitHub仓库下载对应版本的uniplugin-component和uniplugin-library的aar包。这里有一个大坑务必确保原生插件SDK的版本与你项目使用的HBuilderX版本或cli项目的基础库版本兼容。版本不匹配会导致类找不到、方法签名错误等诡异问题。接下来在Android Studio中创建一个新项目选择No Activity模板即可。我们不是要开发一个完整的App而是一个Android Library模块来制作插件。创建ModuleFile - New - New Module...选择Android Library给它起个名字比如uni-plugin-battery。确保Minimum SDK版本不低于你uni-app项目的要求。引入依赖打开这个Library模块的build.gradle文件在dependencies块中添加对uni-app原生插件SDK的依赖。dependencies { // 示例具体版本和文件名请根据你下载的SDK调整 implementation fileTree(dir: libs, include: [uniapp-v8-release.aar]) // 核心SDK implementation com.alibaba:fastjson:1.1.46.android // uni-app依赖的JSON库 implementation androidx.appcompat:appcompat:1.2.0 // 通常需要 // 注意避免与其他依赖的版本冲突特别是support库和androidx }将下载的uniapp-v8-release.aar等文件放入该Library模块的libs目录下。3.2 编写核心插件代码在刚创建的uni-plugin-battery模块的src/main/java目录下创建你的包名和Java类。创建UniModule类如前所述创建类似BatteryModule.java的类。package com.yourcompany.uniplugin.battery; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.BatteryManager; import io.dcloud.feature.uniapp.annotation.UniJSMethod; import io.dcloud.feature.uniapp.bridge.UniJSCallback; import io.dcloud.feature.uniapp.common.UniModule; import org.json.JSONObject; public class BatteryModule extends UniModule { UniJSMethod(uiThread false) public void getBatteryInfo(UniJSCallback callback) { if (mUniSDKInstance null || mUniSDKInstance.getContext() null) { if (callback ! null) { callback.invoke(createErrorResult(上下文环境不可用)); } return; } try { Context context mUniSDKInstance.getContext(); IntentFilter ifilter new IntentFilter(Intent.ACTION_BATTERY_CHANGED); Intent batteryStatus context.registerReceiver(null, ifilter); int level batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); int scale batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1); int status batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); int health batteryStatus.getIntExtra(BatteryManager.EXTRA_HEALTH, -1); int plugged batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); float batteryPct (level / (float)scale) * 100; JSONObject result new JSONObject(); result.put(level, level); result.put(scale, scale); result.put(percentage, (double)Math.round(batteryPct * 10) / 10); // 保留一位小数 result.put(isCharging, status BatteryManager.BATTERY_STATUS_CHARGING || status BatteryManager.BATTERY_STATUS_FULL); result.put(pluggedAC, plugged BatteryManager.BATTERY_PLUGGED_AC); result.put(pluggedUSB, plugged BatteryManager.BATTERY_PLUGGED_USB); result.put(health, health); if (callback ! null) { callback.invoke(result); } } catch (Exception e) { e.printStackTrace(); if (callback ! null) { callback.invoke(createErrorResult(e.getMessage())); } } } private JSONObject createErrorResult(String msg) { JSONObject error new JSONObject(); try { error.put(errCode, -1); error.put(errMsg, msg); } catch (Exception e) { } return error; } }这段代码做了几件事通过系统广播获取电池信息将数据组装成JSONObject然后通过callback.invoke()回传给JS。注意错误处理一定要通过相同的回调通道将错误信息传回去这是良好的插件设计习惯。注册插件这是让uni-app运行时能找到你插件类的一步。在Library模块的src/main目录下创建assets文件夹如果没有的话然后在其中创建dcloud_uniplugins.json文件。{ nativePlugins: [ { hooksClass: , // 生命周期钩子类非必需 plugins: [ { type: module, name: BatteryModule, // 这个name很重要JS端通过它调用 class: com.yourcompany.uniplugin.battery.BatteryModule // 完整类路径 } ] } ] }name字段是你自定义的插件标识符JS端就靠它来调用。3.3 打包生成aar文件代码写好后在Android Studio右侧的Gradle面板中找到你的Library模块例如uni-plugin-battery展开Tasks-build双击运行assemble或assembleRelease任务。构建成功后你可以在模块目录的build/outputs/aar/下找到生成的uni-plugin-battery-release.aar文件。这个就是我们最终要集成的产物。关键经验在打包前务必检查build.gradle中是否启用了代码混淆minifyEnabled。如果插件代码需要被外部调用通常你需要配置混淆规则proguard-rules.pro确保你的UniModule子类及其UniJSMethod方法不被混淆。可以添加类似规则-keep public class * extends io.dcloud.feature.uniapp.common.UniModule {*;} -keepclassmembers class * { io.dcloud.feature.uniapp.annotation.UniJSMethod methods; }3.4 在uni-app项目中集成aar插件现在切换到你的uni-app项目。假设你使用HBuilderX。放置aar文件在uni-app项目根目录下创建或找到nativeplugins文件夹这是uni-app约定的目录。然后在该目录下创建一个以你插件命名的子文件夹例如battery-native-plugin。其内部结构应遵循约定nativeplugins/ └── battery-native-plugin/ (插件文件夹名称自定义但需与后续配置一致) ├── android (Android平台插件内容) │ └── batteryModule.aar (将我们打包的aar文件重命名并放置于此名字可自定义) └── package.json (插件的配置文件至关重要)将之前生成的uni-plugin-battery-release.aar复制到android目录下可以重命名为一个更简洁的名字如batteryModule.aar。配置package.json这是告诉uni-app如何加载你插件的“说明书”。{ name: battery-native-plugin, id: battery-native-plugin, version: 1.0.0, description: 获取电池信息原生插件, _dp_type: nativeplugin, _dp_nativeplugin: { android: { plugins: [ { type: module, name: BatteryModule, // 必须与dcloud_uniplugins.json中的name一致 class: com.yourcompany.uniplugin.battery.BatteryModule // 必须与Java类完全一致 } ], integrateType: aar, minSdkVersion: 21, // 与你插件模块的minSdkVersion保持一致 useAndroidX: true, permissions: [ // 声明插件需要的权限本例中获取电池状态不需要额外权限 ] } } }这里的name和class必须与原生模块中dcloud_uniplugins.json里配置的完全一致这是连接JS和Java的桥梁错一个字母都无法调用成功。在HBuilderX中注册插件在HBuilderX中打开你的uni-app项目找到manifest.json文件切换到“App原生插件配置”视图。点击“选择本地插件”理论上应该能扫描到nativeplugins目录下的battery-native-plugin插件勾选它并保存。这一步的本质是让HBuilderX在后续打包时将这个原生插件模块合并到最终的Android安装包中。4. JS调用与联调打通最后一公里插件集成好了如何在uni-app的Vue页面中调用呢这里需要使用uni.requireNativePlugin方法。在你的Vue组件中template view classcontent button clickgetBattery获取电池信息/button text电量{{batteryInfo.percentage}}%/text text状态{{batteryInfo.isCharging ? 充电中 : 未充电}}/text /view /template script export default { data() { return { batteryInfo: {} }; }, methods: { getBattery() { // 关键调用代码 const batteryModule uni.requireNativePlugin(BatteryModule); // 参数是package.json中配置的name if (batteryModule) { batteryModule.getBatteryInfo((result) { console.log(收到原生回调, JSON.stringify(result)); // 注意回调参数result直接就是JS对象对应Java端的JSONObject if (result.errCode ! undefined) { uni.showToast({ title: 获取失败${result.errMsg}, icon: none }); } else { this.batteryInfo result; uni.showToast({ title: 获取成功, icon: success }); } }); } else { uni.showToast({ title: 模块加载失败, icon: none }); } } } }; /scriptuni.requireNativePlugin是uni-app框架提供的桥梁方法它根据传入的name找到并初始化对应的原生模块对象。之后你就可以像调用普通JS对象方法一样调用原生方法了参数和回调会自动完成类型转换如JS对象转JSONObject数字、字符串等基本类型也会自动转换。联调心得这是最容易卡住的地方。如果调用失败请按以下顺序排查检查控制台运行到手机或模拟器后查看HBuilderX的控制台或手机端的日志adb logcat看是否有ClassNotFoundException或MethodNotFoundException等错误。这通常意味着name或class配置错误或者aar包没有正确打入APK。确认打包确保在HBuilderX进行“原生App-云打包”或“原生App-本地打包”时确实勾选了你配置的插件。云打包后可以下载安装包用压缩软件打开检查assets/apps/你的应用id/www/nativeplugins目录下是否存在你的插件配置文件以及lib目录下是否有对应的so库如果有的话。简化测试初次尝试建议从一个最简单的showToast插件开始确保整个链路写Java - 打包aar - 配置 - JS调用是通的再逐步增加复杂功能。5. 进阶考量与深度避坑指南当你成功运行第一个插件后可能会遇到更复杂的需求和更深的水坑。下面分享几个进阶场景下的处理经验和避坑点。5.1 插件间依赖与资源冲突你的插件可能需要依赖第三方库例如网络请求库OkHttp、图片加载库Glide等。处理方式是在你的Library模块的build.gradle中正常添加implementation依赖。但是要特别注意依赖冲突。冲突表现打包失败报错Program type already present或Duplicate class found。解决方案统一版本确保你的插件依赖的库版本与uni-app基础SDK或项目其他插件依赖的版本尽可能一致。可以在build.gradle中使用exclude或force指令来强制指定某个版本。使用api还是implementation如果你的插件需要将某些类暴露给最终的App模块这种情况较少使用api依赖否则使用implementation将依赖内部化避免传递性依赖冲突。资源ID冲突如果你的插件包含/res资源要确保资源ID不会与主App或其他插件冲突。最好的实践是为你的插件资源名称添加前缀。这可以在插件的build.gradle中配置android { ... resourcePrefix battery_plugin_ // 为所有资源名加前缀 }这样你的布局文件就需要命名为battery_plugin_activity_main.xml字符串资源引用为string/battery_plugin_hello能有效避免合并APK时的资源冲突。5.2 同步与异步方法及线程处理UniJSMethod注解有两个重要属性uiThread: 默认为false。如果方法需要操作UI如显示Toast、更新插件自身的视图必须设为true否则会抛出CalledFromWrongThreadException。runOnJSThread: 这个属性控制方法在JS线程还是原生线程执行。大部分耗时操作如文件IO、网络请求应该在原生线程uiThreadfalse执行避免阻塞JS线程导致页面卡顿。对于异步操作务必使用UniJSCallback将结果传回。回调只能调用一次invoke多次调用可能导致JS端异常。如果需要持续通信如返回进度可以考虑使用UniJSCallback的invokeAndKeepAlive并结合事件机制但这更复杂。5.3 插件调试技巧调试原生插件不像调试JS那样方便但有几个方法可以提升效率日志输出在Java代码中使用Log.d(“BatteryPlugin”, “msg”)输出日志。在HBuilderX控制台选择“原生日志”查看或者使用adb logcat -s BatteryPlugin命令过滤查看。远程调试真机将你的Library模块和写好的测试Demo一个简单的Android App放在同一个Android Studio工程里。在Demo App中直接依赖这个Library模块进行开发和调试验证功能无误后再打包aar集成到uni-app。这能极大提升原生代码的调试效率。检查生成的APK对于云打包下载APK后用工具如apktool或直接解压检查lib目录下是否有你的插件so文件assets目录下是否有正确的dcloud_uniplugins.json这能帮你确认插件是否真的被打包进去。5.4 性能与内存管理虽然原生代码性能高但不当使用也会带来问题避免内存泄漏如果插件中注册了广播接收器registerReceiver、开启了线程或持有了Context的引用一定要在合适的时机如UniModule的onDestroy方法进行反注册和释放。mUniSDKInstance提供的Context通常是Activity上下文长时间持有可能导致Activity无法被回收。数据传输体积JS和原生之间频繁大量地传递数据比如巨大的JSON或Base64图片字符串会有性能开销。对于大文件传输考虑通过原生插件将文件写入特定目录然后只把文件路径传给JS。6. 从模块到组件扩展原生UI控件除了UniModule功能模块uni-app原生插件还支持UniComponent原生组件。这允许你创建用原生代码渲染的、能在template中像普通组件一样使用的标签比如一个高性能的图表、一个特殊的相机视图。创建UniComponent更复杂一些你需要创建一个类继承UniComponent。重写initComponentHostView方法创建并返回一个原生的AndroidView如TextView,SurfaceView, 或自定义View。使用UniComponentProp注解来定义可供JS绑定的属性。在dcloud_uniplugins.json中注册类型为component的插件。在package.json中同样声明。在Vue中通过my-native-view这样的自定义标签使用并通过属性绑定设置参数。原生UI组件开发涉及视图生命周期管理、属性同步、事件发送从原生到JS等更多细节是更高级的集成场景。但核心通信原理与UniModule一脉相承。混合开发是uni-app进阶的必经之路它打破了跨平台框架的能力边界。整个过程就像在精心搭建一座桥Java/Kotlin端是坚实的桥墩UniModule/UniComponent和UniJSMethod/UniComponentProp是桥身结构而uni.requireNativePlugin和package.json的配置则是连接两岸的榫卯。把每一步的细节扣到位这座桥就能稳稳当当地承载起你的业务需求。我自己的经验是第一个插件集成可能会花上一两天踩坑但一旦跑通后续的插件开发就会变得非常顺畅很多配置和代码都可以复用。关键在于理解整个通信机制和配置文件的对应关系剩下的就是按部就班的Android开发了。
返回列表