uni-app 调用 Android 原生能力:JS 与 Java 双向通信实战

发布时间:2026/10/26 11:19:12
uni-app 调用 Android 原生能力:JS 与 Java 双向通信实战

简介:这份资源面向使用uni-app进行跨平台开发、需要与Android原生深度交互的前端与移动端开发者,聚焦uni-app调用原生插件实现通信及控制WiFi开关这一常见但易踩坑的场景。压缩包共38个文件,约11.71MB,以js脚本、vue组件、json配置、png图片为主,另含aar原生插件包、apk安装包及html、scss、css等资源,构成一个可直接运行的uni_demo示例工程。示例中演示了通过uni.$invokeMethod调用原生插件、借助广播完成双向通信、使用WifiManager控制WiFi开关,并涉及Android 6.0以上动态申请ACCESS_WIFI_STATE与CHANGE_WIFI_STATE权限的处理思路,同时提示了iOS端需单独编写原生实现的跨平台差异。已有2488人学习,读者可据此理解原生插件封装与调用链路,结合HBuilderX真机调试与Android Studio断点排查,快速搭建自己的原生交互模块。

1. 从一次真机联调说起:uni-app 怎么和 Android 原生打通

去年做一个工业巡检 App,页面用 uni-app 写,扫码、表单、离线缓存都跑得挺顺,直到产品提了个需求:设备连不上车间内网时,要能从 App 里直接打开系统 WiFi 设置页,让运维人员手动切网络。纯前端 API 翻了一圈没找到能直接拉起 WiFi 面板的接口,最后只能走原生插件这条路。这个项目就是围绕这个场景做的:uni-app 调用 Android 原生能力,实现 JS 与 Java 双向通信,并封装一个打开 WiFi 设置页的原生模块。适合已经会用 uni-app 写页面、但被系统级能力卡住的开发者,也适合想把公司老 Android SDK 接进跨端项目的同学。下面按「能干什么 → 怎么跑 → 怎么改」拆开讲。

2. 项目结构与通信架构:JS 和 Java 到底怎么对话

2.1 目录分层与各层职责

这类 uni-app 混合开发项目,目录一般分成三块:uni-app 工程本体、原生插件模块、以及宿主 Android 壳工程。常见做法是把原生能力做成一个独立的 Android Library 模块,再以本地插件的形式挂到 uni-app 工程里,这样页面代码和原生代码互不污染,后期单独升级原生模块也不影响前端。

project-root/ ├── pages/ # uni-app 页面,业务逻辑 │ └── index/index.vue # 调用原生能力的入口页 ├── nativeplugins/ # 本地原生插件目录 │ └── WifiBridge/ │ ├── package.json # 插件声明:id、版本、方法列表 │ └── android/ │ ├── build.gradle │ └── src/main/java/com/example/wifibridge/ │ ├── WifiBridgeModule.java # 暴露给 JS 的模块 │ └── WifiBridgePackage.java # 模块注册入口 ├── manifest.json # 声明原生插件、权限 └── App.vue

package.json是插件的身份证,uni-app 打包时靠它识别插件 id 和方法名;WifiBridgeModule.java是真正干活的地方;WifiBridgePackage.java负责把模块注册进运行时。三层各管一件事,改功能基本只动 Module,改注册只动 Package,前端只认uni.requireNativePlugin拿到的对象。

2.2 通信链路:从 JS 调用到 Java 回调

uni-app 调原生走的是uni.requireNativePlugin拿到模块代理,再调方法。底层是 WebView 与原生之间的桥接,JS 侧发起调用时把方法名和参数序列化,原生侧反序列化后反射到对应方法,返回值或异步回调再原路送回。理解这条链路,排查问题时才知道该看哪一端。

// pages/index/index.vue const wifiBridge = uni.requireNativePlugin('WifiBridge') export default { methods: { openWifiSetting() { // 第一个参数是业务参数对象,第二个是回调 wifiBridge.openWifiPanel( { from: 'inspection-page' }, (res) => { // res.code: 0 成功,非 0 失败;res.msg 为描述 if (res.code === 0) { uni.showToast({ title: '已打开设置页', icon: 'none' }) } else { uni.showToast({ title: res.msg || '打开失败', icon: 'none' }) } } ) } } }

这段代码里有两个关键点。uni.requireNativePlugin('WifiBridge')的字符串必须和插件package.json里的id完全一致,大小写都不能错,否则拿到的是undefined,调用时直接报is not a function。第二个参数回调是异步的,原生侧执行完才触发,所以不要指望它同步返回结果,页面逻辑要按异步处理。参数对象里我习惯带一个from字段,方便原生侧打日志时知道是哪个页面发起的,联调阶段特别有用。

2.3 原生模块的注册与方法暴露

原生侧要做的第一件事是继承 uni-app 提供的模块基类,把方法用注解暴露出去。下面这段是模块主体,打开 WiFi 设置页的核心逻辑就在这里。

package com.example.wifibridge; import android.content.Context; import android.content.Intent; import android.provider.Settings; import com.alibaba.fastjson.JSONObject; import io.dcloud.feature.uniapp.annotation.UniJSMethod; import io.dcloud.feature.uniapp.bridge.UniJSCallback; import io.dcloud.feature.uniapp.common.UniModule; public class WifiBridgeModule extends UniModule { @UniJSMethod(uiThread = true) public void openWifiPanel(JSONObject options, UniJSCallback callback) { JSONObject result = new JSONObject(); try { Context ctx = mUniSDKInstance.getContext(); // 跳转系统 WiFi 设置页 Intent intent = new Intent(Settings.ACTION_WIFI_SETTINGS); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); ctx.startActivity(intent); result.put("code", 0); result.put("msg", "success"); } catch (Exception e) { result.put("code", -1); result.put("msg", e.getMessage()); } // 回调只触发一次,成功失败都要回 if (callback != null) { callback.invoke(result); } } }

逐段看。@UniJSMethod(uiThread = true)表示这个方法在 UI 线程执行,因为startActivity涉及界面跳转,必须主线程;如果做的是纯计算或 IO,可以设成false避免卡 UI。mUniSDKInstance.getContext()拿到当前页面上下文,这是 uni-app 运行时注入的,不要自己 new Activity。Settings.ACTION_WIFI_SETTINGS是系统常量,跳的是 WiFi 列表页;想跳更细的设置可以用ACTION_WIFI_IP_SETTINGS。FLAG_ACTIVITY_NEW_TASK在上下文不是 Activity 时是必须的,少了会抛AndroidRuntimeException。回调对象UniJSCallback只能invoke一次,重复调用在部分版本上会出问题,所以我把结果统一收在 try/catch 之后回一次。

模块写好后还要注册,否则 JS 侧拿不到。

package com.example.wifibridge; import io.dcloud.feature.uniapp.common.UniModule; import io.dcloud.feature.uniapp.AbsSDKInstance; import io.dcloud.feature.uniapp.bridge.UniJSCallback; import com.alibaba.fastjson.JSONObject; public class WifiBridgePackage { // 模块名必须与 package.json 中 id 一致 public static final String MODULE_NAME = "WifiBridge"; public UniModule createModule(AbsSDKInstance instance) { return new WifiBridgeModule(); } }

注册类的核心就是模块名和package.json的id对齐,这是整条链路最容易翻车的地方,后面避坑章节会细说。createModule每次调用返回新实例,模块内部不要存跨页面的状态,需要共享的数据放静态变量或单独的单例里。

3. 跑起来:环境、配置与真机验证

3.1 环境准备与工程导入

先把工具链对齐,版本错位是新手最常见的卡点。我一般按这个顺序装:Android Studio 用于编译原生模块和看日志,HBuilderX 用于跑 uni-app 工程,JDK 用 8 或 11(跟 Android Gradle Plugin 版本匹配),手机开开发者模式并允许 USB 调试。原生插件必须真机验证,模拟器上部分系统设置页行为不一致,别在模拟器上纠结。

导入步骤:HBuilderX 打开 uni-app 工程,把nativeplugins/WifiBridge整个目录放进去;manifest.json里找到「App 原生插件配置」,选择本地插件并勾选WifiBridge;然后制作自定义调试基座。注意,用了原生插件就不能用标准基座跑,必须打自定义基座,这一步跳过了后面所有调用都会失败。

# 命令行方式打自定义基座(HBuilderX 也可图形化操作) # 1. 确认插件已勾选 # 2. 运行 -> 运行到手机或模拟器 -> 制作自定义调试基座 # 3. 选择 Android,等待打包完成 # 4. 运行 -> 运行到手机或模拟器 -> 运行基座选择「自定义调试基座」

命令本身不复杂,关键是顺序:先做基座,再选基座运行。很多人改完原生代码直接点运行,结果跑的还是旧基座,改动不生效,白折腾半天。

3.2 权限与 manifest 配置

打开 WiFi 设置页本身不需要额外权限,但如果后续要读 WiFi 状态、扫描列表,就得在AndroidManifest.xml里声明。Android 6.0 以后定位权限还和 WiFi 扫描绑定,这块坑不少。

<!-- 原生模块的 AndroidManifest.xml --> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

ACCESS_WIFI_STATE读状态,CHANGE_WIFI_STATE改状态,两个定位权限是扫描 WiFi 列表的前置条件。只跳设置页的话这些都可以不加,但项目里既然叫 WifiBridge,后面大概率要扩展,提前声明省得回头改。运行时权限还要在 JS 侧用plus.android.requestPermissions申请,光声明不申请,Android 6 以上照样拿不到数据。

3.3 真机验证与日志观察

跑通的标准很简单:点按钮,手机跳到系统 WiFi 设置页,回到 App 能看到 toast。验证时同时开两个日志窗口,HBuilderX 的控制台看 JS 侧输出,Android Studio 的 Logcat 看原生侧。Logcat 过滤WifiBridge或包名,能快速定位是没进方法还是方法里抛了异常。

// 联调时在调用前后加日志,确认走到了哪一步 console.log('[WifiBridge] before call') const bridge = uni.requireNativePlugin('WifiBridge') console.log('[WifiBridge] plugin =', !!bridge) bridge.openWifiPanel({ from: 'debug' }, (res) => { console.log('[WifiBridge] callback =', JSON.stringify(res)) })

如果plugin打印出来是false,问题在插件没打进基座或 id 不匹配;如果打印了对象但回调不来,问题在原生方法没执行或回调没触发,去 Logcat 看有没有异常堆栈。这种二分定位比盲猜快得多。

4. 核心代码拆解:双向通信与 WiFi 能力封装

4.1 JS 调原生的三种传参形态

实际项目里调用不止「无参 + 回调」一种。常见有三类:纯回调、带参回调、以及需要原生主动通知 JS 的事件。前两种上面见过了,第三种要靠fireGlobalEventCallback或全局事件,适合 WiFi 状态变化这类持续监听场景。

// 形态一:带参 + 回调(最常用) wifiBridge.openWifiPanel({ from: 'home' }, (res) => { /* ... */ }) // 形态二:需要返回值同步拿结果(少量场景) const info = wifiBridge.getWifiStateSync() console.log('wifi enabled =', info.enabled) // 形态三:原生主动通知 JS plus.globalEvent.addEventListener('wifiStateChanged', (e) => { console.log('state changed to', e.state) })

形态二要求原生方法直接return,但注意它仍在桥接线程模型里,别在里面做耗时操作。形态三依赖原生侧调用mUniSDKInstance.fireGlobalEventCallback("wifiStateChanged", params),适合做状态监听。选哪种取决于业务:一次性动作走回调,持续状态走全局事件,别混用。

4.2 原生侧读取 WiFi 状态

光打开设置页还不够,项目里通常要判断当前 WiFi 开没开,决定是提示用户还是直接跳转。原生侧读状态用WifiManager。

@UniJSMethod(uiThread = false) public void getWifiState(JSONObject options, UniJSCallback callback) { JSONObject result = new JSONObject(); try { Context ctx = mUniSDKInstance.getContext(); WifiManager wm = (WifiManager) ctx.getApplicationContext() .getSystemService(Context.WIFI_SERVICE); boolean enabled = wm != null && wm.isWifiEnabled(); result.put("code", 0); result.put("enabled", enabled); } catch (Exception e) { result.put("code", -1); result.put("msg", e.getMessage()); } if (callback != null) callback.invoke(result); }

这里uiThread = false,因为读状态是轻量但非 UI 操作,放子线程更稳。getApplicationContext()而不是页面 context,避免持有 Activity 引用导致内存泄漏。isWifiEnabled()在 Android 10 以后对普通应用仍可用,但部分厂商 ROM 有定制,返回值要以真机为准。返回结构统一成code + 数据,前端好判断。

4.3 回调线程与数据序列化

UniJSCallback.invoke内部会把 Java 对象序列化成 JS 能认的结构,JSONObject是最稳的载体,别直接传自定义实体类,字段名和类型容易对不上。回调线程取决于方法注解:uiThread = true时回调在 UI 线程,前端拿到后可以直接操作界面;false时在子线程,前端如果要做 UI 更新得自己切回主线程。我一般统一在原生侧把结果整理好再回,前端不做二次加工。

// 统一封装回调,减少重复代码 private void reply(UniJSCallback cb, int code, String msg, JSONObject data) { JSONObject r = new JSONObject(); r.put("code", code); r.put("msg", msg); if (data != null) r.putAll(data); if (cb != null) cb.invoke(r); }

这个小工具方法在模块方法多的时候很省事,所有返回走同一个出口,字段不会漏。参数说明:code用 0 表示成功、负数表示各类失败,msg给人看,data放业务字段。前端只认这三个约定,扩展新方法时不用改解析逻辑。

5. 避坑与排查:五个真机上踩过的坑

5.1 插件 id 大小写不一致导致拿不到对象

现象:uni.requireNativePlugin('WifiBridge')返回undefined,调用报is not a function。原因:package.json里id写成了wifibridge或WifiBridgePlugin,和 JS 里传的字符串对不上。解决:三处必须完全一致——插件package.json的id、WifiBridgePackage里的模块名常量、JS 调用字符串。改完重新打自定义基座,光热重载不生效。

5.2 用了标准基座导致原生方法找不到

现象:代码没问题,但一调用就提示模块不存在。原因:标准基座里没有你的原生插件,只有自定义基座才包含。解决:制作自定义调试基座并在运行时选中它。判断方法很简单,看运行日志里基座名称,带「自定义」字样才对。这个坑新手几乎必踩一次。

5.3 上下文用错引发 NEW_TASK 异常

现象:跳设置页时崩溃,日志里有Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag。原因:getContext()拿到的不是 Activity,或者漏了 flag。解决:加Intent.FLAG_ACTIVITY_NEW_TASK,或者改用mUniSDKInstance.getContext()强转 Activity 后再启动。我一般直接加 flag,兼容性最好。

5.4 回调重复触发导致前端逻辑错乱

现象:前端回调执行了两次,toast 弹两遍,或者状态被覆盖。原因:原生方法里成功分支和 catch 分支都调了callback.invoke,或者异步任务里多次回。解决:回调只在一个出口触发,用前面那个reply方法统一收口。UniJSCallback设计上是一次性的,重复调用行为不确定,别赌。

5.5 权限声明了但运行时没申请

现象:读 WiFi 列表返回空,或者扫描直接失败。原因:AndroidManifest.xml声明了权限,但 Android 6 以上还要运行时申请,尤其定位权限。解决:JS 侧调plus.android.requestPermissions申请,原生侧在方法里先检查checkSelfPermission,没授权就回错误码让前端引导。别假设声明了就一定有。

6. 二次开发:把 WifiBridge 扩成通用原生能力层

这个项目现在的形态只解决了一个点,真正有价值的是它搭好的通信骨架。我的改造思路是把它从「打开 WiFi 页」升级成「系统能力网关」:所有需要原生兜底的功能都往这个模块里加方法,前端只维护一份调用封装。具体做法是新建一个bridge.js统一出口,页面不直接碰requireNativePlugin,这样以后换插件实现或加降级逻辑,只改一个文件。

// utils/bridge.js —— 前端统一调用层 const native = uni.requireNativePlugin('WifiBridge') function call(method, params = {}) { return new Promise((resolve, reject) => { if (!native || typeof native[method] !== 'function') { return reject(new Error(`native method ${method} unavailable`)) } native[method](params, (res) => { if (res && res.code === 0) resolve(res) else reject(new Error((res && res.msg) || 'native call failed')) }) }) } export const openWifiPanel = () => call('openWifiPanel', { from: 'bridge' }) export const getWifiState = () => call('getWifiState')

这层封装把回调式 API 转成 Promise,页面里await就行,错误统一走 catch。参数说明:method是原生方法名,必须和@UniJSMethod标注的方法一致;params是业务参数对象。加新能力时,原生侧写方法、package.json补声明、这里加一个导出函数,三步走完。

验证改造是否成功,我习惯用一张对照表逐项过:

检查项通过标准失败先看哪
插件加载!!native为 truepackage.json id、基座
方法存在typeof native[method]为 function注解、注册类
回调返回res.code 有值Logcat 异常、回调出口
权限敏感操作有授权manifest、运行时申请

还有个小技巧:原生侧所有方法入口统一打一行Log.d("WifiBridge", method + " called"),联调时一眼看出请求有没有到原生。等稳定了再按需删。另外,模块里别存 Activity 引用,用getApplicationContext(),否则页面销毁后容易泄漏,这种问题线上很难查。

血泪经验是:每次改完原生代码,我一定先删掉手机上旧的自定义基座再重新安装,而不是直接覆盖运行。有次缓存导致新方法死活不生效,查了两个小时才发现跑的是旧包。从那以后我每次动原生模块都强制走一遍「重打基座 → 卸载旧包 → 安装 → 验证」的流程,再没在这上面翻过车。希望帮到你。

本文还有配套的精品资源,点击获取