首页 / 资讯中心 / 文章详情

uni-app调用Android原生插件:实现打开WiFi与双向通信

uni-app调用Android原生插件:实现打开WiFi与双向通信 ★ FEATURED ARTICLE
简介这是一套uni-app调用Android原生插件实现双向通信与WiFi控制的示例工程面向有一定前端基础的开发者适合需要在uni-app项目里动态打开或关闭系统WiFi、与硬件模块进行消息交互的场景。资源共38个文件压缩包大小11.71MB包含JS、Vue、JSON等前端源码Android原生AAR库、APK安装包以及PNG图片资源其中AAR插件封装了WiFi控制与广播接收逻辑APK可直接安装体验前端按照pages、components、nativeplugins等模块组织配套清单和配置文件齐全方便整体浏览和对比。目前已有2487人学习下载。通过该工程可以掌握uni调用原生方法的核心链路理解安卓广播接收器在通信中的角色以及Android 6.0以上动态申请系统权限的处理方式同时还能学习到原生插件跨平台适配与真机调试的一般方法。依托uni_demo示例可在HBuilderX中直接运行体验开关WiFi的效果这种插件封装与JS调用的对照结构也可迁移到蓝牙、传感器等其他原生能力的扩展上。1. 为什么uni-app要调用Android原生不是所有事都能在JS里解决uni-app 做跨端开发确实方便一套代码同时覆盖 App、小程序、H5但有一类需求它天生给不了你直接操作系统级硬件。比如一键打开 WiFi、读取当前连接的网络、和蓝牙设备握手这些能力要么在 HTML5 的封装范围之外要么被阉割成半吊子。于是就得走原生的后门——在 Android 工程里写 Java 或 Kotlin通过 uni-app 提供的原生插件桥让 JS 侧像调用普通方法一样调过去再把结果带回来。这篇文章用“打开 WiFi”这个具体例子把整条链路拆开工程结构、Module 写法、JS 调用、版本坑和双向通信一条龙给出来。适合谁看被 uni-app 限制过手脚的前端、要接系统能力的全栈、以及打算把现有原生模块包装成插件的人。2. 通信原理与工程准备JS与Java之间不止一座桥2.1 原生插件是什么Module 与 Component 两种形态uni-app 的原生插件不是神秘的黑匣子它本质上是你写好的一段原生代码被包成一个 JS 可调用的对象。分两种形态Module模块和 Component组件。Module 是纯逻辑不涉及界面比如打开 WiFi、读写文件、执行加解密Component 是带原生界面的组件比如地图、扫码框、自定义输入框。我们要做的“打开 WiFi”显然属于 Module。Module 在 Android 侧是一个继承自UniModule的类部分旧版本叫WXModule。类里的每个公开方法通过UniJSMethod注解暴露给 JSJS 侧用uni.requireNativePlugin(插件名)拿到这个对象然后直接调用方法。插件名来自原生侧注册的 name两端必须精确一致大小写都不能错否则requireNativePlugin会返回null调用直接卡死。为什么必须通过这一层桥因为 JS 和 Java 运行在两个完全隔离的世界JS 在 V8 或 quickjs 引擎里Java 在 Android 虚拟机里两者之间不能共享对象也没法直接互相调用。uni-app 在中间起着序列化和反序列化的作用JS 传过来的对象会被转成JSONObject或JSONArray方法的返回值再转回 JS 对象。我们不需要关心底层实现只需要遵循它的注解规则就能像写普通 Java 方法一样被 JS 调到。2.2 工程目录原生代码放在哪里打包如何生效这里有个关键选择离线打包还是云打包。离线打包指你自己在 Android Studio 里打开 uni-app 的壳工程把原生插件代码直接放进去编译调试。云打包则是把前端代码和插件配置传到 uni-app 云端由云端生成安装包。我强烈建议初期用离线打包因为你能看到完整日志和堆栈云打包的报错往往是一团黑连是哪个类找不到都不说。以离线打包为例核心目录结构大约是这样app/src/main/ assets/ apps/__UNI__你的应用id/www/ # uni-app 编译后的前端资源 data/dcloud_uniplugins.json # 插件注册文件 java/你的包名/ wifi/ WiFiModule.javadcloud_uniplugins.json是原生插件的注册文件壳工程启动时会读取它把里面的插件类加载到内存。它的格式是一个 JSON 数组每个插件对象里至少包含type、name、class三个字段。下面这个例子注册了刚才的WiFiModule{ nativePlugins: [ { plugins: [ { type: module, name: WiFiModule, class: com.example.myplugin.WiFiModule } ] } ] }type是module或componentname是 JS 侧引用的名字class是原生类的全限定名。这个文件放在assets/data/下不同 uni-app 版本可能放到assets/根目录或者改名为dcloud_uniplugins.json。我一般在收到新版本 SDK 时先检查目录变化再决定是否迁移。这个细节特别容易让人踩坑我就见过同事把配置文件放错位置结果云打包时插件静默丢失前端调用永远拿不到对象。还需要说明一点插件类的生命周期是跟随 uni-app 应用实例的。一个应用里可能有多个页面如果多个页面都requireNativePlugin同一个插件拿到的是同一个原生实例还是不同实例官方没有严格保证。我在实践中通常认为同一UniSDKInstance下是复用的但跨页面时不要依赖这个行为最好自行管理状态。2.3 最小可用的 Module 骨架注册、导出、调用我们写一个最小可用骨架验证通信链路是否走通。原生侧新建WiFiModule.javapackage com.example.myplugin; import io.dcloud.feature.uniapp.common.UniModule; import io.dcloud.feature.uniapp.annotation.UniJSMethod; import io.dcloud.feature.uniapp.bridge.UniJSCallback; import org.json.JSONObject; public class WiFiModule extends UniModule { UniJSMethod(uiThread true) public void echo(JSONObject options, UniJSCallback callback) { String msg options.optString(msg, hello); JSONObject result new JSONObject(); try { result.put(echo, msg); result.put(code, 0); } catch (Exception e) { // JSONException 不用过度处理 } if (callback ! null) { callback.invoke(result); } } }JS 侧调用const wifi uni.requireNativePlugin(WiFiModule); wifi.echo({ msg: from js }, (ret) { console.log(原生返回, JSON.stringify(ret)); });这段代码的逻辑说明原生方法echo第一参数是 JS 传入的对象第二参数是回调。UniJSMethod(uiThread true)表示这个方法在 Android 主线程执行这样回调回去更新 UI 不会崩溃。options.optString(msg, hello)是安全读取JS 那边可以不带msg缺省就是hello我见过不少前端一开始传undefined导致getString直接抛异常换成optString就稳了。参数说明回调callback.invoke返回一个JSONObjectJS 侧自动转成普通 object。这里的code字段是我自定义的返回格式为了后面多个方法统一使用。实际项目中不建议每个方法返回值结构都不一样最好统一成{code, msg, data}三件套否则前端封装 Promise 时还得写一堆if (ret.someStrangeField)。骨架跑通后你就能在这个基础上加任意原生逻辑了。3. 实现“打开WiFi”原生方法API、权限与JS调用链路3.1 Android 侧写什么WifiManager、Intent 与权限声明打开 WiFi 在 Android 上不是一个稳定 API 能搞定的。早期WifiManager.setWifiEnabled(true)可以直接切换开关但从 Android 10API 29开始这个方法被官方废弃普通应用调用它不会生效甚至直接抛SecurityException。原因是系统不希望第三方应用随意打断用户当前的网络连接所以把开关权限收归系统。官方给出的替代方案有两个方向如果只是“跳过去让用户开”用Settings.ACTION_WIFI_SETTINGS打开系统设置页。如果是要“连接指定 WiFi”用WifiNetworkSpecifier发起系统级连接请求用户确认后自动连接。如果你只需要读取 WiFi 状态用WifiManager.getWifiState()或isWifiEnabled()都没问题但要注意权限声明。在 Android 12 及以下需要ACCESS_WIFI_STATE在 Android 13API 33以上除了ACCESS_WIFI_STATE还需要NEARBY_WIFI_DEVICES这个运行时权限它属于“附近设备”组弹窗文案要提前设计好。否则即使清单里写了权限运行时没有弹窗授权返回的列表依然是空。3.2 原生代码带版本判断的 openWiFi 方法下面这段代码是经过验证的版本UniJSMethod(uiThread true) public void openWiFi(JSONObject options, UniJSCallback callback) { Context context mUniSDKInstance.getContext(); WifiManager wifiManager (WifiManager) context.getSystemService(Context.WIFI_SERVICE); JSONObject result new JSONObject(); try { if (android.os.Build.VERSION.SDK_INT android.os.Build.VERSION_CODES.Q) { result.put(code, 1); result.put(msg, 系统限制请跳转设置页手动打开); result.put(action, settings); } else { boolean open options.optBoolean(open, true); boolean success wifiManager.setWifiEnabled(open); result.put(code, success ? 0 : 1); result.put(msg, success ? 操作成功 : 操作失败); result.put(data, wifiManager.isWifiEnabled()); } } catch (Exception e) { result.put(code, -1); result.put(msg, e.getMessage()); } if (callback ! null) { callback.invoke(result); } }逻辑说明方法里先取上下文再拿WifiManager实例。因为mUniSDKInstance是UniModule的成员所以可以直接用不需要自己存 Context。代码对 Android Q10及以上返回固定结果不调用废弃 API旧系统则执行setWifiEnabled。参数说明options.optBoolean(open, true)表示默认打开如果 JS 传{open: false}就尝试关闭。返回的JSONObject里统一用code表示状态0 成功1 业务限制-1 异常。action字段告诉前端下一步该做什么。注意即便旧系统上能调用setWifiEnabled在 Android 7API 24之后不少厂商 ROM 也会拦截最终表现是方法返回true但 WiFi 没变化。所以为了保证体验我一般会再加一个校验步骤调用后延迟 300 毫秒再读一次isWifiEnabled()确认状态是否真的变了。如果没变就返回code: 2提示“操作可能被系统拦截”。这一步很关键能在数据层挡住厂商差异性。另外代码里要用到Intent跳转设置页需要额外实现一个方法toWifiSettings下面给出UniJSMethod(uiThread true) public void toWifiSettings(UniJSCallback callback) { Context context mUniSDKInstance.getContext(); Intent intent new Intent(Settings.ACTION_WIFI_SETTINGS); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); if (callback ! null) { try { callback.invoke(new JSONObject().put(code, 0)); } catch (Exception e) { // no-op } } }这段代码比openWiFi简单但有一个隐藏坑context如果来自 application启动 Activity 时必须加上FLAG_ACTIVITY_NEW_TASK否则会崩。我遇到过不少同学在这行代码上翻车直接在插件里用Activity上下文没问题但一旦遇到Application上下文就找不到任务栈。3.3 JS 侧调用封装Promise 化与分支处理原生侧有两个方法了JS 侧不能裸调就完事。我们需要把“跳转设置页”也接到业务里。封装示例function openWifi(open true) { return new Promise((resolve, reject) { const wifi uni.requireNativePlugin(WiFiModule); wifi.openWiFi({ open }, (ret) { console.log(原生回调, ret); if (!ret || ret.code undefined) { reject(new Error(插件返回格式异常)); return; } if (ret.code 0) { resolve(ret.data); } else if (ret.action settings) { uni.showModal({ title: 提示, content: ret.msg || 需要去系统设置中手动打开, confirmText: 去设置, cancelText: 取消, success: (res) { if (res.confirm) { wifi.toWifiSettings({}); } reject(new Error(user cancel)); } }); } else { reject(new Error(ret.msg || 打开失败)); } }); }); }参数说明uni.requireNativePlugin(WiFiModule)拿到的对象是原生模块代理调用方法时第一个参数是传给原生的对象第二个是回调。这里wifi.toWifiSettings({})调用原生跳转不需要传业务数据。ret.code的判断顺序很重要先判断undefined避免后面ret.action异常。uni.showModal是 uni-app 自带弹窗里面的success回调里res.confirm为 true 时去设置页。这里还有一个细节原生返回的ret.data在旧系统上可能是true/false也可能是空字符串因为isWifiEnabled()返回 boolean。resolve 时前端拿到true就知道开了拿到false则知道没开。但是要注意用户可能在设置页手动操作后返回 App此时openWiFi的 Promise 已经 reject 了UI 可能没刷新。所以通常我会配合事件推送一起用这就是第五章要说的内容。4. 避坑指南版本、线程与打包的三类坑4.1 Android 10 上调用 openWiFi 没反应现象真机 Android 11 上执行wifi.openWiFi({ open: true })回调一直不触发或者触发了但 WiFi 根本没变化。原因setWifiEnabled在 Android 10 被系统封禁普通应用没有权限调用会抛SecurityException。如果原生侧没写 try-catch调用直接异常退出JS 回调自然不会执行。解决在方法入口做版本判断Android Q 及以上直接返回code: 1引导用户去设置页。我后来把所有系统能力相关的插件都加了“版本守卫”模板避免同一个坑在不同方法里反复踩。4.2 回调不执行Promise 卡住现象原生方法里明明执行了callback.invoke但 JS 那边 Promise 一直没有 resolve。原因UniJSMethod没有加uiThread true且方法里做了耗时操作比如扫描 WiFi 列表回调执行时线程已经切走前端监听器丢失。解决统一在注解上加uiThread true如果是有耗时操作的方法内部自己做子线程但最终回调回到主线程。需要多次回调用invokeAndKeepAlive一次性回调用invoke。我在模块里习惯保存 callback 引用用完立刻置空防止被多次调用导致业务错乱。4.3 云打包后插件没生效requireNativePlugin 返回 null现象离线打包一切正常打到云端安装后uni.requireNativePlugin(WiFiModule)返回null。原因云打包时原生代码没有跟着项目走。如果是自己开发的插件必须放在 uni-app 原生插件工程里插件包里有完整的 Android 工程配置如果直接把前端代码丢去云打包系统根本不知道有WiFiModule这个类。解决开发期用自定义调试基座验证插件有效后再走云打包。上传插件包时确认里面包含dcloud_uniplugins.json且name和class正确。这个文件我每次都会单独检查一遍因为打包平台不会告诉你它漏读了哪个 JSON 字段。4.4 多次调用后内存一直涨现象连续调用openWiFi几十次App 内存占用明显上涨怀疑泄漏。原因原生侧保存了 callback 引用没有释放或者广播接收器反复注册。UniModule实例与前端页面生命周期不同步页面销毁时原生实例不一定销毁。解决在模块里维护一个HashMapString, UniJSCallback缓存在页面onUnload时调用原生release方法清空注册了BroadcastReceiver就一定要在onDestroy里注销。我后来养成了习惯每个插件方法里凡是用到了全局变量都会问自己一句“这变量什么时候被清掉”。4.5 Android 13 上获取 WiFi 列表为空现象小米 Android 13 设备上调用获取 WiFi 列表的代码返回内容一直为空数组。原因Android 13 引入了NEARBY_WIFI_DEVICES运行时权限它和旧的ACCESS_FINE_LOCATION不同即使清单里写了ACCESS_WIFI_STATE没有弹窗授权也拿不到结果。解决在插件首启或首次调用时检查权限没有就用requestPermissions弹窗然后在onRequestPermissionsResult里继续业务流程。注意这个权限弹窗文案要提前准备用户不同意时要有降级方案比如提示“无法搜索 WiFi请手动连接”。5. 双向通信从单向调用到原生事件推送5.1 原生主动推送事件fireEvent 的使用打开 WiFi 不是一次调用就结束。用户从设置页返回后App 需要知道 WiFi 到底开了没或者用户在外面连了不同网络状态变化要实时通知页面。这就要用原生主动推送而不是让 JS 反复拉取。原生侧注册一个广播接收器监听WifiManager.WIFI_STATE_CHANGED_ACTION每次 WiFi 开关变化时通过mUniSDKInstance.fireEvent把事件丢给 JSUniJSMethod(uiThread true) public void startWifiMonitor(UniJSCallback callback) { Context context mUniSDKInstance.getContext(); if (wifiReceiver null) { wifiReceiver new BroadcastReceiver() { Override public void onReceive(Context ctx, Intent intent) { WifiManager wifi (WifiManager) ctx.getSystemService(Context.WIFI_SERVICE); JSONObject payload new JSONObject(); try { payload.put(wifiEnabled, wifi.isWifiEnabled()); payload.put(timestamp, System.currentTimeMillis()); } catch (Exception e) { // no-op } mUniSDKInstance.fireEvent(onWifiStateChanged, payload); } }; context.registerReceiver(wifiReceiver, new IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION)); } if (callback ! null) { JSONObject result new JSONObject(); try { result.put(code, 0); } catch (Exception e) { // no-op } callback.invoke(result); } }逻辑说明startWifiMonitor方法先判断接收器是否已存在避免重复注册。onReceive里组一个 JSONObject带上 WiFi 开关状态和时间戳然后fireEvent推送给 JS。注意fireEvent必须在主线程调用这里方法注解已经保证。参数说明事件名onWifiStateChanged是字符串JS 侧监听时必须用同名变量payload 里可以放任意可序列化的对象。JS 侧监听uni.onNativeEvent(onWifiStateChanged, (payload) { console.log(wifi state changed:, payload.wifiEnabled); updateWifiIcon(payload.wifiEnabled); });这样只要 WiFi 状态一变前端就能自动更新图标不需要轮询。5.2 监听之后要记得释放stopWifiMonitor 方法广播接收器是重资源注册了不注销App 退后台后还在收广播耗电又泄漏。所以配套的方法必须同时提供UniJSMethod(uiThread true) public void stopWifiMonitor(UniJSCallback callback) { if (wifiReceiver ! null) { mUniSDKInstance.getContext().unregisterReceiver(wifiReceiver); wifiReceiver null; } if (callback ! null) { try { callback.invoke(new JSONObject().put(code, 0)); } catch (Exception e) { // no-op } } }JS 侧在页面onUnload时调用onUnload() { const wifi uni.requireNativePlugin(WiFiModule); wifi.stopWifiMonitor({}); }这一步看起来不起眼但真正上线后就能看到效果。我见过不少同事只写了 start 忘了 stop测试阶段看不出问题线上被用户投诉耗电。5.3 实战实时显示 WiFi 状态一个典型的实战流程是页面onLoad时调用startWifiMonitor注册广播接收器。原生立刻回调返回当前状态JS 设置初始 UI。之后每次 WiFi 变化原生 fireEventJS 更新图标。页面onUnload时调用stopWifiMonitor释放。这样通信链路就是双向的JS 可以主动调原生原生也能主动推 JS。注意uni.onNativeEvent和uni.$on是两个完全不同的机制不要混用。前者专门收原生插件的事件后者是应用内页面间事件。我在一个项目里看着两个单词长得像把监听挂到了$on上结果半天收不到事件最后才排查出来。6. 验证方法与兼容策略让插件在真机上“服服帖帖”6.1 用日志确认调用真正到达原生前端同学往往不知道原生侧到底执行了没有。我的做法是在每个原生方法入口和出口各打一条日志用Log.d打印方法名和关键参数。运行时在终端执行adb logcat -s WiFiModule就能看到WiFiModule: openWiFi called, options{open:true} WiFiModule: openWiFi result, code1, actionsettings有了日志判断链路就清清楚楚。如果原生方法没进那问题多半在注册文件或打包环节如果进了没返回就要看是不是卡在耗时操作或回调线程。6.2 用降级策略兼容不同 Android 版本不同系统版本对 WiFi 的控制策略不同我的统一策略是在原生层返回code和action在 JS 层决定怎么处理。这样原生版本升级时不用动前端逻辑。例如 Android 9 和 Android 11 返回的code可能不同但前端都认识ret.code 1就是要引导用户去设置。如果业务必须支持老机型可以在 JS 里先用uni.getSystemInfo获取osVersion判断是否调用原生openWiFi。但我不建议把版本判断写在 JS因为 Android 版本规则会变不如让原生内部自己判断JS 只关心业务结果。6.3 边界测试列表每次改完 WiFi 模块我都会跑一遍下面这个表场景预期验证方法Android 9 打开 WiFicode0WiFi 打开手动滑出状态栏确认开关Android 11 打开 WiFicode1actionsettings弹窗引导去设置从设置页返回 ApponWifiStateChanged 事件日志输出状态变化拒绝“附近设备”权限不崩溃有降级提示检查返回的 msg连续 start/stop monitor无内存上涨adb 内存观察做了这个清单发版前不用再依赖测试小姐姐反复点自己先挡掉八成问题。我见过一些人写完插件直接丢给测试Android 10 真机上静默失败测试一脸懵最后才发现是版本封禁。从那以后我每次都强制走一遍“真机日志 版本分支 回调空指针检查”的流程。这流程看着麻烦但省下来的调试时间可不止一倍。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站