☰
uni-app调用Android原生:从plus.android到打开WiFi开关的完整实践
2026/10/21 1:21:34 网站建设 项目流程

简介:一份面向uni-app开发者的Android原生插件调用示例包,演示如何通过uni.$invokeMethod实现JS与原生代码通信,并利用WifiManager控制WiFi开关。包内包含完整的uni_demo工程,既有vue、js、json等前端资源,又有aar、apk等Android原生构建产物,合计38个文件、约11.71MB。已有2487人学习下载,特别适合需要对接硬件能力或扩展原生功能的中高级前端开发者。示例完整覆盖了原生插件封装、BroadcastReceiver广播监听、运行时动态权限申请、跨平台差异处理等关键环节,目录结构清晰,便于对照源码逐段理解调用链路。通过该示例,可以快速掌握uni-app调用Android原生的标准流程,并以此为模板扩展到其他硬件控制或系统能力调用,提升多平台开发效率。

1. 从“JS调不动WiFi开关”说起:uni-app 与 Android 原生的通信桥梁

某开发者接手一个物业巡检App,用uni-app写了大半,突然接到需求:首页加一个“打开WiFi”按钮,最好直接打开WiFi开关。翻遍uni-app文档,发现JS层没有现成API能控制WiFi。一顿折腾后,他用plus.android这层桥接,把Java原生代码和uni-app的JS代码打通了,最终实现了「uni-app调用Android原生可以实现通信及打开WiFi」这套可复用的最小工程。这个项目核心不是WiFi功能本身,而是演示了JS与Android原生之间的双向通信怎么搭:JS侧发起调用、传参数、接收返回值,原生侧执行WifiManager操作并返回状态。它适合所有在uni-app里被系统能力卡住的开发者,同时也给后续做原生插件留了一条清晰的迁移路径。

2. 技术选型:native.js、原生插件、离线SDK,直调是起步最快的一条路

2.1 三条路怎么选

在uni-app里调Android原生,我见过大多数人第一次都会搜到三类方案。第一类是「原生插件」,按照官方文档写Java或Kotlin代码,再通过uni_modules目录接入。它的优点是封装完整、性能好、能被其他项目复用,缺点也很直接:要理解插件生命周期、自定义基座、云打包那一套,第一个Hello World可能就要折腾大半天。尤其是云打包还要排队,改一次权限重新打一次包,节奏很慢。

第二类是「离线SDK混编」,把uni-app的编译产物放进一个Android Studio工程,在原生层直接操作WebView的JS桥。这种适合已经有原生团队、要深度改造的场景,但对纯前端开发者基本算劝退。

第三类就是本项目用的「native.js直调」。uni-app在App端运行时暴露了一个plus.android对象,可以直接importClass导入Java类,invoke调用Java方法,runtimeMainActivity拿到当前原生Activity实例。也就是说,不写一行Java代码,也能调用系统原生API。这个项目的目标是打开WiFi,只是WifiManager一个类的事,没有后台服务也没有复杂UI,所以选这条成本最低的路,合情合理。

2.2 plus.android 的桥接本质

很多人第一次用plus.android都栽在“我怎么调Java静态方法”上。这套桥的底层原理不复杂:uni-app的JS运行在WebView的JavaScript引擎里,引擎通过一个JSBridge与Android的Java世界双向通信。plus.android就是uni-app暴露出来的、让你在JS侧操作这个Bridge的入口。Java对象会被包装成JS可调用的Proxy对象,你调用方法时,Bridge通过反射找到对应Java类的方法并执行,再把结果返回给JS。

常用的四个东西要背下来:

  • plus.android.runtimeMainActivity():拿到App当前的主Activity实例,几乎所有系统服务都要通过它获取。
  • plus.android.importClass("完整类名"):把一个Java类导入成JS对象,之后就可以new它,或者调用它的静态字段和方法。
  • plus.android.invoke(obj, "方法名", 参数...):等价于在Java里调用obj.methodName(参数...)。
  • plus.android.implements("接口名", {...}):在JS侧实现一个Java接口,常用于回调。

这里有个关键前提:plus.android必须在plusready事件触发后才能使用。如果你在页面onLoad里直接调,大概率拿到undefined。项目里的做法是先在App.vue的onLaunch里监听plusready,再做一层Promise封装,后面所有调用都在ready之后发起。这套封装放在utils/native-bridge.js里,后面所有章节都会用到。

2.3 最小可运行示例:先打开WiFi设置页

动手第一步不是直接开WiFi,而是先打通“跳转系统设置页”这条链路,因为它在所有Android版本上都稳定,适合验证桥是否通。下面这段是从项目里抽出来的最小示例:

function openWifiSettingsPage() { // 获取主Activity,相当于Java层的this var main = plus.android.runtimeMainActivity(); // 导入Intent类和Settings类 var Intent = plus.android.importClass("android.content.Intent"); var Settings = plus.android.importClass("android.provider.Settings"); // 创建跳转WiFi设置页的Intent var intent = new Intent(Settings.ACTION_WIFI_SETTINGS); // 在原生命周期上下文中启动这个Activity main.startActivity(intent); }

逻辑说明:前三步分别是拿Activity、导入类、创建Intent,最后调用startActivity完成跳转。这里没有硬编码action字符串,而是直接引用了Settings.ACTION_WIFI_SETTINGS这个静态字段,比写死字符串更不容易出错。

参数说明:new Intent(Intent action)里的action表示要执行的动作,ACTION_WIFI_SETTINGS对应的是系统WiFi设置界面;startActivity需要传入一个Activity作为调用方,所以前面必须先拿runtimeMainActivity。如果你在onLaunch里还没等plusready就调startActivity,Activity是拿不到的,这就是常见的黑匣子报错来源。

这段代码在真机上运行,能直接跳到WiFi列表页。但它只是“打开设置页”,还不是“打开WiFi开关”。真正的开关控制,需要进入WifiManager的调用,这就是下一章要讲的工程与权限部分。

2.4 直调方案的上限与迁移边界

直调虽然快,但有三道坎。第一,代码安全性弱,所有Java类都暴露在JS层,出问题难排查。第二,复杂UI组件很难在JS侧用importClass拼出来,比如系统级Dialog、自定义View,这种情况更适合真正写原生插件。第三,一旦涉及长时间后台任务,比如持续监听WiFi信号强度,JS层生命周期不可控,还是得靠原生Service。

所以这个项目给自己划了一条边界:只要新增需求超过“简单方法调用”,比如要做原生界面、要跑后台服务,就立刻迁到uni原生插件。迁移时不需要重写业务代码,只需要把utils/wifi.js里的实现搬到Java类里,然后native-bridge.js的callNative入口保持不变。这样前端页面几乎是零改动,这是个人比较推荐的做法。

3. 工程与权限:uni-app 项目里的原生代码、双份清单、一个请求的回路

3.1 目录结构:原生代码放哪

这个项目没有采用Android Studio工程,而是以native.js直调为主,所以“原生代码”在仓库里体现为JS工具模块加一份权限配置。目录长这样:

wifi-demo/ ├── pages/ │ └── index/ │ └── index.vue # 页面:按钮 + 状态展示 ├── utils/ │ ├── native-bridge.js # plus.android 封装层 │ └── wifi.js # WiFi 相关能力 ├── static/ │ └── logo.png ├── manifest.json # uni-app 应用配置 ├── main.js └── App.vue

逻辑说明:native-bridge.js负责判断plusready、统一调用plus.android;wifi.js只处理业务,不管底层导入细节。这样后续如果要把能力迁移成真正的原生插件,只需要替换native-bridge.js内部实现,页面和wifi.js都不用大改。

如果你需要写真正的Java扩展,常见做法是另外开一个Android Studio工程,把uni-app的离线SDK依赖进去,Java代码放在app/src/main/java/包名/下面,编译出AAR后,再通过uni_modules的nativeplugins目录接入。这个项目暂时没走那一步,所以目录里没有java文件。

3.2 双份清单:manifest.json 与 AndroidManifest.xml 的权限配合

uni-app在云打包时会把manifest.json里的权限声明转换到AndroidManifest.xml中;如果你离线打包,则要直接改AndroidManifest.xml。这份资源里同时涉及两处,避免团队里有人只改一边导致真机调不通。

manifest.json的app-plus -> permissions需要配置:

{ "app-plus": { "permissions": [ "android.permission.ACCESS_WIFI_STATE", "android.permission.CHANGE_WIFI_STATE", "android.permission.ACCESS_NETWORK_STATE" ] } }

如果走离线打包,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_NETWORK_STATE" /> <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" android:usesPermissionFlags="neverForLocation" />

逻辑说明:第一份清单给uni-app云打包用,第二份给离线工程用。ACCESS_WIFI_STATE是查询WiFi状态,CHANGE_WIFI_STATE是修改WiFi开关,这两个缺一个就会在调用getWifiState或setWifiEnabled时被SecurityException打断。NEARBY_WIFI_DEVICES是Android 13开始用来替代部分WiFi权限的,如果只是开开关不看列表,可以不加,项目里加上是为了兼容后面的扩展。

参数说明:usesPermissionFlags="neverForLocation"表示这个权限声明不会用于推断位置,属于Android 13权限声明里的推荐姿势;如果你后续要扫描周边WiFi列表,就要重新评估是否需要定位权限,不能照抄这行。

3.3 通信协议:一次调用、回调结果、主动推送

通信不只是一行invoke,项目里把通信分成了三种模式,方便后面扩展。

第一种是“一次性调用”:JS传参数给原生,原生同步返回结果。适合getWifiState这种查询类操作。

第二种是“回调结果”:原生方法执行耗时操作,比如等WiFi状态稳定,JS侧传入一个回调函数,原生执行完再调用。native.js里可以用plus.android.implements创建一个Java接口的实现对象当作回调参数传进去。

第三种是“主动推送”:原生在后台发现状态变化,主动发事件给JS。这个在纯native.js里比较绕,更推荐直接用uni.$emit和uni.$on在JS层做轮询结果的通知。项目里默认做法是:原生方法返回结果后,JS侧统一状态并uni.$emit("wifiStateChanged"),页面监听这个事件刷新UI。这样通信路径清晰:JS发起、原生执行、结果回传、页面感知。

// 在 wifi.js 中统一返回结构 function formatResult(code, data, message) { return { code, data, message }; }

逻辑说明:所有通信结果都统一成{code,data,message},避免有人在页面里拿到undefined不知所措。code为0表示成功,非0表示失败或降级处理。参数说明:data里放具体业务数据,比如WiFi状态值、开关结果;message可以写给人看的文案,也可以只留错误信息。这套结构在下一章完整链路里反复用到。

4. 核心模块拆解:从 JS 发出请求到原生返回结果的完整链路

4.1 打开WiFi开关:WifiManager 的封装与高版本降级

真正打开WiFi开关,本质是调用系统服务WifiManager的setWifiEnabled方法。在Android 10之前的版本,这个方法对应用是开放的。项目里的核心实现如下:

// utils/wifi.js function setWifiEnabled(enable) { // 拿到主Activity var main = plus.android.runtimeMainActivity(); // 导入Context,用于拿系统服务 var Context = plus.android.importClass("android.content.Context"); // 导入WifiManager var WifiManager = plus.android.importClass("android.net.wifi.WifiManager"); // 通过Activity.getSystemService拿到系统WiFi服务 var wifiManager = main.getSystemService(Context.WIFI_SERVICE); // 执行开关 var result = wifiManager.setWifiEnabled(enable); return result; }

逻辑说明:这一步对应了前面章节说的importClass和invoke链路。setWifiEnabled(enable)的enable是布尔值,true表示打开WiFi,false表示关闭。返回值也是布尔值,反映系统是否接受了这次请求,注意它不代表WiFi已经真正打开,因为打开是一个异步过程。

参数说明:getSystemService(Context.WIFI_SERVICE)中,WIFI_SERVICE是Context类的静态常量;setWifiEnabled在Android 10及以上会被系统拒绝并抛出SecurityException,因为这属于系统控制权限。所以真实项目里不能直接把它裸奔,得加一层版本判断和异常捕获。

function toggleWifi(enable) { var main = plus.android.runtimeMainActivity(); var Context = plus.android.importClass("android.content.Context"); var WifiManager = plus.android.importClass("android.net.wifi.WifiManager"); var wifiManager = main.getSystemService(Context.WIFI_SERVICE); try { var result = wifiManager.setWifiEnabled(enable); return { code: 0, data: result, message: "操作成功" }; } catch (e) { // 高版本系统不允许直接改开关,降级跳转设置页 var Intent = plus.android.importClass("android.content.Intent"); var Settings = plus.android.importClass("android.provider.Settings"); var intent = new Intent(Settings.ACTION_WIFI_SETTINGS); main.startActivity(intent); return { code: 1, data: false, message: "当前系统版本不允许直接开关,已跳转设置页" }; } }

逻辑说明:try块里执行直接开关,一旦SecurityException被捕获,就跳转系统设置页,把选择权交还给用户。code=1表示“降级处理”,不是失败,页面可以显示提示而不是红色报错。这里最值钱的不是那行setWifiEnabled,而是这个降级分支。

参数说明:catch (e)中的e是Java异常对象,你可以在里面加e.printStackTrace()打日志。实际调试时如果自定义基座没有日志,可以用返回值把message带回页面。这个设计在血泪经验里很管用。

4.2 从原生取回状态:getWifiState 与轮询

开关发出后,用户真正关心的是“WiFi到底开了没有”。WifiManager.getWifiState返回一个整数常量:0表示WIFI_STATE_DISABLED,1表示WIFI_STATE_DISABLING,2表示WIFI_STATE_ENABLED,3表示WIFI_STATE_ENABLING。项目里封装了查询方法:

function getWifiState() { var main = plus.android.runtimeMainActivity(); var Context = plus.android.importClass("android.content.Context"); var WifiManager = plus.android.importClass("android.net.wifi.WifiManager"); var wifiManager = main.getSystemService(Context.WIFI_SERVICE); var state = wifiManager.getWifiState(); var stateMap = { 0: "已关闭", 1: "正在关闭", 2: "已开启", 3: "正在开启" }; return { code: 0, data: { state: state, desc: stateMap[state] } }; }

逻辑说明:查询方法没有安全限制,任何版本都能调用。返回的data里同时放数字和文案,页面直接拿desc渲染,不需要自己翻译状态码。参数说明:stateMap里的映射关系对应Android系统的WifiManager常量值,不要随意改顺序,否则文案会对不上。

由于WiFi开关是异步的,从发出指令到状态变成2可能需要几百毫秒到几秒。项目里用了一个简单的轮询,每500毫秒查一次,最多查6次:

function waitWifiEnabled(timeout) { timeout = timeout || 3000; var interval = 500; var times = Math.floor(timeout / interval); return new Promise(function (resolve, reject) { var count = 0; var timer = setInterval(function () { count++; var result = getWifiState(); if (result.data.state === 2) { clearInterval(timer); resolve({ code: 0, data: { state: 2, desc: "WiFi已开启" } }); } else if (count >= times) { clearInterval(timer); resolve({ code: -1, data: result.data, message: "等待超时,请检查系统是否弹窗拦截" }); } }, interval); }); }

逻辑说明:这是一个标准的前端异步轮询模式。500毫秒一次,3秒超时。如果WiFi状态变成2,立即resolve;如果超时,返回当前的state,方便排查是权限问题还是系统弹窗卡住了。参数说明:timeout可以外部传入,默认3000毫秒;interval固定500毫秒,如果你在低端机上跑,建议调到800毫秒,减少空转。

4.3 JS 侧完整封装:把原生调用包成 Promise

前面都是零散方法,页面里直接用会很碎。项目里最终给页面暴露了两个方法:openWifi直接一键开,openWifiSettings只跳设置页。完整代码如下:

export function openWifi() { return new Promise(function (resolve, reject) { var result = toggleWifi(true); if (result.code === 0) { // 开关指令已接受,等待真正开启 waitWifiEnabled(3000).then(function (waitResult) { if (waitResult.code === 0) { resolve(waitResult); } else { // 超时但系统可能在开,降级返回 resolve(waitResult); } }); } else if (result.code === 1) { // 已跳转设置页,把决定权交给用户 resolve(result); } else { reject(result); } }); }

逻辑说明:openWifi面向页面,全程Promise化。先发开关指令,再轮询真实状态,最后把结果统一成之前定义的{code,data,message}结构。页面调用方只需要写uni.showToast判断code,不用关心底层是直接开成功还是跳设置页。

参数说明:waitWifiEnabled里的3000是最终等待时间,单位毫秒。注意这里resolve和reject的边界:只要是系统接受了操作,哪怕超时也resolve,因为WiFi可能正在开,页面能给出“正在开启”的提示;只有连开关指令都发不出去才reject。

页面index.vue里的使用简化如下:

import { openWifi } from "../../utils/wifi.js"; export default { methods: { handleOpenWifi() { openWifi().then(function (res) { uni.showToast({ title: res.message, icon: "none" }); }).catch(function (err) { uni.showToast({ title: err.message || "操作失败", icon: "none" }); }); } } };

逻辑说明:页面只管调方法和显示提示。参数说明:res.message在code为1时是“已跳转设置页”,catch分支处理的是代码层面没跑通的情况。这个分层结构让新手不会在页面里看到一堆plus.android。

5. 真机调试:运行步骤、权限开关与高频踩坑排查

5.1 从 HBuilderX 到真机的完整运行步骤

第一步,用HBuilderX打开项目,确认manifest.json里已勾选对应权限。第二步,用数据线连接Android真机,手机开启开发者模式和USB调试。第三步,在HBuilderX菜单栏点击“运行” -> “运行到手机或模拟器”,选择你的设备。第四步,如果直接运行失败,很可能是内置基座不带这些权限,需要先制作自定义调试基座。

常见做法是:点击“发行” -> “制作自定义调试基座”,在云打包页面勾选本项目的权限,打包完成后,运行菜单里选择“使用自定义基座”再运行。这一步能解决一半的“JS调了没反应”问题。另外强烈建议在页面里加一个console.log输出plus.os.name,先确认当前是App端而不是H5端,因为H5端没有plus对象。

5.2 避坑:真机调试中我遇到的5个问题

第一,现象:浏览器预览时控制台报plus is not defined。原因:plus对象只在uni-app的App端运行时存在,H5端没有。解决:所有调用都放在plusready之后,或者先做环境判断:

function isApp() { return typeof plus !== "undefined"; }

第二,现象:在onLoad里直接调plus.android.runtimeMainActivity()返回null。原因:plus还没ready。解决:用plusready事件包裹,或者把初始化放到onReady之后。项目里的native-bridge.js专门处理这个:

function ready() { return new Promise(function (resolve) { if (typeof plus !== "undefined" && plus.android.runtimeMainActivity()) { resolve(); } else { document.addEventListener("plusready", resolve, false); } }); }

第三,现象:真机上点按钮,提示SecurityException: setWifiEnabled is not allowed。原因:Android 10及以上系统禁止第三方应用直接改WiFi开关。解决:不要想着绕过系统限制,老老实实走降级跳设置页。项目里已经在toggleWifi的catch分支处理了,你看代码里那行main.startActivity就是为此准备的。

第四,现象:getSystemService返回的wifiManager是null。原因:权限没声明,或者传的Context类型不对。解决:确认AndroidManifest.xml(或manifest.json)里加了ACCESS_WIFI_STATE,并且确保传入的是runtimeMainActivity而不是一个普通页面对象。真机上这个null不会抛异常,结果就是调用方法时报Cannot read property,排查起来很玄学,所以我习惯在每个方法入口先打印wifiManager是否存在。

第五,现象:自定义基座打出来后,WiFi方法正常,但同事的Android 13手机上直接闪退。原因:Android 13新的NEARBY_WIFI_DEVICES权限影响WiFi相关调用。解决:在打包配置里补上NEARBY_WIFI_DEVICES权限,并且声明neverForLocation,避免触发定位相关审核。这属于新系统踩坑,建议项目升级时先查一遍目标版本的权限变化。

5.3 验证通信是否打通:日志与返回码

通信通没通,不要靠猜。项目里的验证套路分三步:第一步,在JS里打日志,console.log(JSON.stringify(result)),看返回的code。第二步,在系统里看原生层日志,用adb命令过滤关键字:

adb logcat | grep -i "wifi-demo"

第三步,用返回码做断言。code为0时,Toast显示操作成功;code为1时,显示已跳转设置页;code为-1时,显示等待超时。如果code稳定为0但WiFi实际没开,那就要怀疑WifiManager拿错了,回到第四点排查。这样一层层拆,大多数问题十分钟内能定位。

6. 二次开发:把“打开 WiFi”扩展成一个可复用的原生能力桥

6.1 统一调用入口

当你不想每次都在页面里importClass,可以把原生调用收敛到一个方法分发器里。我在项目里做了个native-bridge.js,核心思路是method + params,所有原生能力通过这个入口走。

export function callNative(method, params) { return ready().then(function () { switch (method) { case "openWifi": return openWifi(); case "openWifiSettings": return openWifiSettings(); case "getWifiState": return getWifiState(); default: return Promise.reject({ code: -999, message: "未注册的原生方法:" + method }); } }); }

逻辑说明:这层相当于把plus.android和业务方法全部隔离,页面只依赖callNative。参数说明:method是字符串方法名,params是预留参数对象,后续扩展蓝牙、NFC时,往switch里加分支就行。这种设计比散落一堆importClass好在:权限、版本兼容、降级逻辑都集中在同一个文件,好找。

6.2 新增一个原生方法要动几个文件

我整理了最小改动清单:

改动文件改动点举例
utils/native-bridge.js在callNative中注册方法名"getWifiState"
utils/wifi.js实现具体原生调用逻辑getWifiState()
manifest.json声明方法所需权限ACCESS_WIFI_STATE
页面调用callNative并处理返回值callNative("getWifiState")

逻辑说明:如果方法需要请求定位等运行时权限,还要额外在页面里用uni.authorize处理授权,这部分页面代码同样走上述流程。这样一次原生能力扩展,改动点在四个文件内可控,不会牵连整个页面。

6.3 回归验证与个人习惯

每次新增方法后,我的验证顺序是:先在真机跑一遍callNative返回code,再检查权限声明是否出现在自定义基座的打包日志里,最后用adb logcat看原生层是否有异常。通常后两步能拦住九成的问题。我从这个项目里养成的习惯是:新增任何“调用Android原生”的方法前,强制先查一遍目标Android版本的API限制,再写importClass代码。以前我在某次项目里直接调setWifiEnabled,结果在测试机上翻车,后来所有原生调用都先看了一遍系统版本差异,再落到代码里。希望这个习惯和这份工程能帮到你,让你在uni-app里调原生时少走几段弯路。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询