
简介这份资源面向使用uni-app进行跨平台开发、需要与Android原生深度交互的开发者聚焦于通过原生插件实现JS与Android通信以及控制WiFi开关这一常见但门槛较高的场景。压缩包共38个文件约11.71MB以js脚本、vue组件、json配置为主辅以png图标、html页面、scss样式并包含aar原生插件包与apk安装包便于直接运行验证。内容围绕uni.$invokeMethod调用原生方法、BroadcastReceiver广播通信、WifiManager控制WiFi、Android 6.0以上动态权限申请等关键环节展开示例工程中可看到按钮触发调用、原生插件接收并执行开关WiFi的完整链路。已有2488人学习适合想打通uni-app与Android原生能力、理解插件封装与跨平台差异的读者参考借鉴。1. 从一次真机联调说起uni-app 调 Android 原生到底能干什么去年帮一个做工业巡检的团队收拾一个烂摊子uni-app 写的界面在模拟器上跑得好好的一上真机就抓瞎——要读设备序列号、要判断当前 WiFi 是不是连到了指定内网、还要在检测到离线时把系统 WiFi 设置页拉起来让操作员手动重连。前端同学第一反应是找插件市场翻了一圈发现要么年久失修要么只封装了一半能力。最后落地的方案其实很朴素写一个 Android 原生模块通过 uni-app 的扩展机制把 Java 方法暴露给 JS通信走统一桥接WiFi 相关操作直接调系统服务。这套东西不复杂但坑集中在「桥怎么搭、参数怎么传、权限怎么给、真机怎么验」四件事上。这篇就把这条链路从头拆一遍适合已经会用 uni-app 写页面、但一碰到原生能力就卡住的同学也适合想判断「这个方案值不值得接进现有工程」的熟手。2. 桥接原理与工程骨架为什么必须走原生扩展这条路uni-app 跑在 WebView 或原生渲染层里JS 本身没有能力直接碰 Android 的WifiManager、ConnectivityManager这些系统服务。想让 JS 调到原生绕不开官方的扩展机制把 Java 代码编译进 APK再通过统一的模块注册方式暴露给 JS 调用。理解这一点后面所有配置才有落脚点。2.1 三种调用通道的取舍常见做法有三条路选错了后面全是返工。第一条是plus.android.importClass直接在 JS 里反射调用原生类。优点是零配置、改完即生效适合临时验证某个 API 通不通缺点是性能差、类型转换全靠手写、混淆后容易崩生产环境不建议大面积用。第二条是原生插件native plugin把 Java 打成独立模块通过uni.requireNativePlugin引入。这是工程化程度最高的方式适合能力要复用、要跨项目移植的场景代价是要维护一份插件工程和打包配置。第三条是自定义基座 扩展 module把 Java 类注册进 uni-app 的 module 体系JS 侧用uni.requireNativePlugin(你的模块名)拿到对象。它介于前两者之间配置量适中调试也方便是这类「通信 WiFi 操作」需求最常用的落点。我一般这么选先花十分钟用plus.android.importClass把目标 API 跑通确认系统层没问题再把这部分逻辑搬进扩展 module 做正式封装。这样能避免一上来就陷进打包配置里分不清是桥没通还是 API 用错了。2.2 工程骨架与模块注册一个能跑的最小骨架包含三块Android 原生工程放 Java 代码、uni-app 前端工程放页面和调用逻辑、以及连接两者的模块声明。原生侧的核心是继承 uni-app 提供的 module 基类把要暴露的方法写成UniJSMethod注解的公开方法。package com.example.wifibridge; 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 { // 暴露给 JS 的同步方法读取当前 WiFi 连接状态 UniJSMethod(uiThread false) public JSONObject getWifiState() { JSONObject result new JSONObject(); try { android.net.wifi.WifiManager wm (android.net.wifi.WifiManager) mUniSDKInstance.getContext().getSystemService(android.content.Context.WIFI_SERVICE); result.put(enabled, wm.isWifiEnabled()); result.put(ssid, wm.getConnectionInfo().getSSID()); result.put(code, 0); } catch (Exception e) { result.put(code, -1); result.put(msg, e.getMessage()); } return result; } // 暴露给 JS 的异步方法打开系统 WiFi 设置页 UniJSMethod(uiThread true) public void openWifiSettings(UniJSCallback callback) { try { android.content.Intent intent new android.content.Intent( android.provider.Settings.ACTION_WIFI_SETTINGS); intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK); mUniSDKInstance.getContext().startActivity(intent); if (callback ! null) { JSONObject ok new JSONObject(); ok.put(code, 0); callback.invoke(ok); } } catch (Exception e) { if (callback ! null) { JSONObject err new JSONObject(); err.put(code, -1); err.put(msg, e.getMessage()); callback.invoke(err); } } } }这段代码有两个关键点要说清。uiThread false表示方法在子线程执行适合读状态这类不涉及 UI 的操作避免阻塞主线程uiThread true表示在主线程执行凡是startActivity这种必须主线程调用的一定要标 true否则会抛线程异常。UniJSCallback是异步回调载体JS 侧传一个函数进来原生处理完invoke回去这是通信的核心机制。mUniSDKInstance.getContext()拿到的是当前页面上下文用它取系统服务、起 Activity 都靠它。模块注册需要在原生工程的清单里声明模块名前端侧才能按名字拿到。这一步各版本配置略有差异常见做法是在模块配置里登记WifiBridgeModule对应的名称前端用同名引入。2.3 前端侧调用与参数约定前端拿到模块对象后调用方式和普通 JS 方法没区别区别在于返回值形态和回调时机。// 引入原生模块名字要和原生侧注册的一致 const wifiBridge uni.requireNativePlugin(WifiBridge) export default { methods: { // 同步读取状态原生返回 JSONObject前端直接拿到对象 checkWifi() { const res wifiBridge.getWifiState() if (res.code ! 0) { uni.showToast({ title: 读取失败 res.msg, icon: none }) return } this.wifiEnabled res.enabled this.currentSsid res.ssid }, // 异步打开设置页第二个参数是回调函数 gotoWifiSettings() { wifiBridge.openWifiSettings((res) { if (res.code 0) { uni.showToast({ title: 已打开设置页, icon: none }) } else { uni.showToast({ title: 打开失败 res.msg, icon: none }) } }) } } }参数约定建议统一成「入参用对象、出参带 code」。入参用对象是为了以后加字段不破坏调用方出参带code是为了让前端能区分成功和异常而不是靠undefined判断。上面getWifiState返回的是JSONObject前端拿到的就是普通对象字段名要和原生put的 key 严格一致大小写错了就是undefined这是最常见的低级翻车点。3. WiFi 能力落地从状态读取到系统设置跳转桥通了只是第一步真正干活的是 WiFi 这部分。Android 对 WiFi 的管控这些年越来越严很多老教程里的写法在新系统上直接失效得按当前系统的规则来。3.1 权限声明与运行时申请WiFi 相关权限分两类一类是普通权限清单里声明即可另一类是危险权限必须运行时申请。读 WiFi 状态、拿 SSID 涉及位置信息因为 SSID 可以被用来推断地理位置系统把它归到了定位权限下。!-- AndroidManifest.xml 中声明 -- uses-permission android:nameandroid.permission.ACCESS_WIFI_STATE / uses-permission android:nameandroid.permission.CHANGE_WIFI_STATE / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / !-- 读取 SSID 需要定位权限且要区分新旧系统 -- uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION /清单声明只是「允许申请」真正拿到还得在运行时弹窗。原生侧封装一个权限检查方法前端在调用 WiFi 能力前先过一遍。UniJSMethod(uiThread true) public void requestWifiPermission(UniJSCallback callback) { // 检查定位权限是否已授予 boolean granted mUniSDKInstance.getContext().checkSelfPermission( android.Manifest.permission.ACCESS_FINE_LOCATION) android.content.pm.PackageManager.PERMISSION_GRANTED; JSONObject res new JSONObject(); res.put(granted, granted); callback.invoke(res); }参数说明ACCESS_FINE_LOCATION是精确位置读 SSID 通常需要它如果只判断 WiFi 开关状态ACCESS_WIFI_STATE就够。注意不同系统版本对 SSID 读取的限制不一样新系统上即使给了定位权限拿到的 SSID 也可能是占位符这时候要引导用户去系统设置里确认而不是死磕代码。3.2 读取连接信息与判断内网工业场景里经常要判断「当前是不是连到了指定内网」光看 SSID 不够还得看 IP 段。原生侧把这两样一起返回前端做判断。UniJSMethod(uiThread false) public JSONObject getNetworkDetail() { JSONObject res new JSONObject(); try { android.net.wifi.WifiManager wm (android.net.wifi.WifiManager) mUniSDKInstance.getContext().getSystemService(android.content.Context.WIFI_SERVICE); android.net.wifi.WifiInfo info wm.getConnectionInfo(); res.put(ssid, info.getSSID()); // 拿本机 IP判断是否落在内网段 int ip info.getIpAddress(); String ipStr String.format(%d.%d.%d.%d, ip 0xff, (ip 8) 0xff, (ip 16) 0xff, (ip 24) 0xff); res.put(ip, ipStr); res.put(code, 0); } catch (Exception e) { res.put(code, -1); res.put(msg, e.getMessage()); } return res; }getIpAddress()返回的是小端序整数直接toString会得到一串看不懂的数字必须按位与还原成点分十进制这是很多人第一次写会踩的坑。前端拿到ip后按前缀判断比如192.168.开头视为内网具体网段按现场实际情况配。3.3 打开系统 WiFi 设置页的正确姿势「打开 WiFi 设置页」听起来简单但直接startActivity到ACTION_WIFI_SETTINGS有两个细节一是必须加FLAG_ACTIVITY_NEW_TASK因为从非 Activity 上下文启动需要新任务栈二是部分定制系统会拦截这个 Intent得做兜底。UniJSMethod(uiThread true) public void openWifiSettings(UniJSCallback callback) { JSONObject res new JSONObject(); try { android.content.Intent intent new android.content.Intent( android.provider.Settings.ACTION_WIFI_SETTINGS); intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK); // 先判断有没有能处理这个 Intent 的页面避免崩溃 if (intent.resolveActivity(mUniSDKInstance.getContext().getPackageManager()) ! null) { mUniSDKInstance.getContext().startActivity(intent); res.put(code, 0); } else { res.put(code, -2); res.put(msg, 系统无 WiFi 设置页); } } catch (Exception e) { res.put(code, -1); res.put(msg, e.getMessage()); } callback.invoke(res); }resolveActivity这一步是后悔药定制 ROM 上确实存在没有标准设置页的情况不判断直接起就是闪退。返回码设计上0成功、-1异常、-2无可用页面前端按码分流提示比统一弹「操作失败」有用得多。4. 避坑与排查真机上最容易翻车的五个点这套方案在模拟器上基本不会出问题问题全在真机和打包环节。下面五条是按出现频率排的每条都按「现象 → 原因 → 解决」写。4.1 模块引入返回 undefined现象前端uni.requireNativePlugin(WifiBridge)拿到的是undefined调用直接报错。原因模块名和原生侧注册名不一致或者原生代码根本没打进当前运行的包里。自定义基座没重新打、用的是标准基座都会导致原生模块不存在。解决先确认模块名逐字符一致包括大小写再确认当前跑的是自定义基座而不是标准基座最后检查原生工程是否真的参与了编译看 APK 里有没有对应的类。4.2 读到的 SSID 是 unknown 或带引号现象getConnectionInfo().getSSID()返回unknown或者返回MyWiFi带一对引号。原因带引号是系统返回格式如此需要自己剥返回unknown多半是定位权限没给或者系统版本对 SSID 读取做了限制。解决字符串做一次replace(\, )处理权限问题按 3.1 的流程补运行时申请新系统上如果仍拿不到改用getNetworkCapabilities配合getTransportInfo判断连接类型不要死磕 SSID。4.3 打开设置页闪退现象点按钮后应用直接退出日志里有ActivityNotFoundException或AndroidRuntimeException。原因漏了FLAG_ACTIVITY_NEW_TASK或者当前上下文不是 Activity 上下文或者目标 Intent 在该 ROM 上不存在。解决补 flag用mUniSDKInstance.getContext()而不是自己 new 一个 Context加resolveActivity判断做兜底按 3.3 的写法来。4.4 回调不触发现象原生方法执行了日志也打了但 JS 侧的回调函数一直不执行。原因UniJSCallback只能在方法执行期间有效如果把它存成成员变量延迟调用回调会失效另外uiThread标错也可能导致回调在错误线程触发。解决回调必须在方法内同步或异步地及时invoke不要跨方法持有涉及 UI 的回调确保uiThread true调试时在invoke前后各打一条日志确认到底卡在哪。4.5 混淆后原生方法找不到现象debug 包正常release 包一调原生就崩或返回空。原因开启混淆后UniJSMethod注解的方法名被混淆桥接层按名字找不到对应方法。解决在混淆规则里保留模块类和被注解的方法常见做法是加-keep class com.example.wifibridge.** { *; }并把注解本身也 keep 住。这条血泪经验值得写进工程模板别等发版前才发现。5. 进阶把桥接封装成可复用能力并做真机验证单点调通之后真正决定这套东西能不能长期用的是封装和验证。我一般会把原生侧收成一个门面类所有对外方法统一走「入参对象 出参 code」的约定前端再包一层 Promise页面里就不必关心回调形态了。// 前端统一封装把回调式原生方法包成 Promise function callNative(method, params {}) { return new Promise((resolve, reject) { const bridge uni.requireNativePlugin(WifiBridge) if (!bridge || typeof bridge[method] ! function) { reject(new Error(原生方法不存在 method)) return } bridge[method](params, (res) { if (res res.code 0) { resolve(res) } else { reject(new Error((res res.msg) || 原生调用失败)) } }) }) } // 页面里这样用异常统一 catch async function ensureWifi() { try { const detail await callNative(getNetworkDetail) if (!detail.ip.startsWith(192.168.)) { await callNative(openWifiSettings) } } catch (e) { uni.showToast({ title: e.message, icon: none }) } }这层封装的价值在于把「桥可能不存在」「返回码可能异常」这两个不确定性收敛到一个地方页面逻辑只关心业务。参数上注意params即使为空也要传原生侧按对象取字段传undefined容易在部分版本上触发空指针。验证环节建议固定一套清单每次改原生代码都走一遍别靠记忆。下面这张表是我自己用的最小验证集覆盖了通信和 WiFi 两条链路的关键分支。验证项操作预期结果失败先看模块可达调用任意方法不返回 undefined模块名、基座类型同步返回调 getWifiState拿到 code0 和字段字段名大小写异步回调调 openWifiSettings回调收到 code0uiThread、回调是否及时 invoke权限分支拒绝定位权限后读 SSID返回 unknown 而非崩溃运行时权限流程异常兜底在无设置页环境调用返回 code-2resolveActivity 判断release 包打混淆包重跑以上行为与 debug 一致混淆 keep 规则这张表里最后一行最容易被跳过我见过不止一个项目在发版当天才发现混淆把桥打断了。从那以后我每次动原生代码都会强制走一遍 release 包的真机验证哪怕只改了一行。希望这套拆法能帮到你少在真机上熬几个晚上。本文还有配套的精品资源点击获取