ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

uniapp 通过广播与原生插件接入 RFID 模块实战

2026/9/19 4:39:30 拓冰建站 浏览量
uniapp 通过广播与原生插件接入 RFID 模块实战 1. 项目缘起与整体方案拆解RFID 识别在移动端的落地说直白点就是让一台 Android 手机或平板具备读取电子标签的能力。这件事在工业巡检、仓储盘点、资产管理、门店盘点这些场景里需求非常硬但真正动手做的时候很多人会卡在同一个地方uniapp 本身是跨端框架它没有原生的串口、USB、串口转接、广播接收这些底层能力而 RFID 读写器厂商给的 SDK 几乎清一色是 Android 原生 AAR 或 JAR 包。这两者之间的鸿沟就是整个项目最核心的技术难点。我这次做的项目目标很明确在一台搭载 Android 系统的工业手持终端上通过外接或内置的 RFID 模块实现标签的实时读取、去重、列表展示并且把数据回传到 uniapp 的业务层做后续处理。设备侧用的是常见的 UHF RFID 模块通信方式走的是 Android 广播Broadcast机制模块厂商提供了一个原生 SDK里面封装了串口通信、功率设置、盘存指令等底层操作。uniapp 这边负责 UI、业务逻辑、数据存储和上层的交互。为什么选择“广播 原生插件”这条路线而不是纯 JS 方案原因很现实。RFID 模块的通信协议通常是厂商私有的涉及串口波特率、帧头帧尾校验、CRC 校验、多标签防碰撞算法这些用 JS 根本碰不到。Android 系统里硬件模块和上层应用之间最常见的解耦方式就是广播模块作为一个独立服务运行把读到的标签数据通过 Intent 广播发出来任何注册了对应 Action 的应用都能收到。uniapp 要做的就是通过原生插件去注册这个广播接收器把数据捞回来再通过 uni 的通信机制传给 JS 层。这个方案的优势在于第一不需要自己写串口驱动厂商 SDK 已经把最脏最累的活干了第二广播机制天然支持一对多模块可以同时给多个应用发数据调试的时候可以用厂商自带的 Demo 先验证硬件是否正常第三原生插件的形式让 uniapp 和原生代码的边界非常清晰后续换模块、换厂商只要广播 Action 和数据格式对得上上层业务代码几乎不用动。但这里有个前提必须说清楚这套方案只适用于 Android 平台。iOS 对后台广播、外设访问的限制非常严格RFID 模块在 iOS 上基本走的是蓝牙或 MFI 认证路线和 Android 的广播方案完全不是一回事。所以如果你的项目需要同时覆盖 iOS那得另起炉灶本文不展开。适合谁来参考这篇内容如果你正在做 uniapp 项目需要接入 RFID、NFC、串口设备、扫码枪这类外设或者你手上有厂商给的 Android SDK 但不知道怎么和 uniapp 结合那这篇实战指南就是为你写的。我会从方案设计、原生插件开发、广播接收、数据解析、常见坑位这几个维度把整个流程拆开讲透。2. 核心细节解析与实操要点2.1 为什么是广播而不是 AIDL 或 SocketAndroid 里进程间通信的方式有好几种AIDL、Messenger、Socket、广播、ContentProvider 都能用。RFID 模块厂商选择广播不是随便拍的脑袋。广播最大的好处是松耦合模块服务不需要知道谁在监听它只管把数据发出去应用也不需要绑定服务注册个接收器就行。对于 RFID 这种“数据持续推送”的场景广播的实时性足够开发成本最低。但广播有个坑Android 8.0 之后静态注册的广播接收器对隐式广播限制很严很多系统广播必须动态注册才能收到。RFID 模块发的通常是自定义 Action 的广播不受隐式广播限制但为了保险我建议一律用动态注册在 Activity 的 onResume 里注册onPause 里注销。这样既能保证收到数据又不会在后台被系统干掉。另一个细节是广播的发送方式。有些模块用的是sendBroadcast有些用的是sendOrderedBroadcast。前者所有接收器都能收到后者按优先级顺序接收高优先级的可以截断。RFID 场景一般用普通广播就够了除非你有多个应用同时监听且需要控制优先级。2.2 原生插件的形态选择Module 还是 Componentuniapp 原生插件分两种Module 和 Component。Module 是没有 UI 的功能模块Component 是带 UI 的组件。RFID 识别显然属于功能型选 Module。Module 的生命周期由 JS 层调用触发适合做“初始化—开始盘存—停止盘存—获取数据”这种命令式操作。插件开发用 Android Studio创建一个 Android Library 模块引入厂商的 AAR 或 JAR然后实现 uniapp 的UniModule接口。关键方法用UniJSMethod注解暴露给 JS 调用。广播接收器在 Module 初始化时动态注册收到数据后通过UniModule的fireGlobalEventCallback或callback把数据传回 JS。这里有个经验不要把广播接收器写在 Activity 里因为 uniapp 的 Activity 可能被回收而 Module 是单例的生命周期更长。把接收器放在 Module 里用 Application Context 注册稳定性会好很多。2.3 数据格式的协商与解析厂商 SDK 通过广播发出来的数据格式通常是一个 byte 数组或者一个字符串。常见的有两种一种是直接给 EPC 码的十六进制字符串另一种是给完整的标签信息包括 EPC、TID、RSSI、天线号、读取次数。你在写解析逻辑之前一定要拿到厂商的通信协议文档搞清楚每个字段的偏移量和长度。我遇到过一种情况厂商 Demo 里广播出来的 EPC 是 24 位十六进制但实际标签是 96 位 EPC需要做补零或截取。这种细节文档里不一定写清楚最好的办法是用厂商 Demo 先读几个已知标签把原始数据打出来对照确认格式后再写解析代码。去重逻辑也很关键。RFID 盘存是高频重复的同一个标签一秒可能被读到几十次。如果不去重列表会疯狂刷新UI 直接卡死。我的做法是在原生层用 HashMap 做一级去重key 用 EPCvalue 用时间戳同一个 EPC 在 500ms 内只上报一次。JS 层再做二级去重用 Set 或者对象缓存已展示的 EPC。两级去重下来列表既实时又稳定。2.4 权限与硬件兼容性Android 的权限模型这几年变化很大。RFID 模块如果走 USB 或串口可能需要android.permission.USB_PERMISSION或者串口设备的访问权限。如果是内置模块通常不需要额外权限但蓝牙模块需要BLUETOOTH_CONNECT和BLUETOOTH_SCANAndroid 12。这些权限要在 manifest 里声明并且在运行时动态申请。兼容性方面不同厂商的模块功率设置范围不一样有的 0-30dBm有的 5-26dBm。功率太高会干扰相邻标签太低读不到远处标签。实际调试时我一般从中间值开始比如 20dBm然后根据读取距离和误读率微调。天线号也要注意多天线模块需要指定用哪根天线盘存单天线模块一般默认天线 1。3. 实操过程与核心环节实现3.1 环境准备与工程结构先列一下我这次用的环境供你对照项目版本/型号uniappHBuilderX 3.8Vue2 语法Android Studio2022.3 或更高Android SDKAPI 30 编译最低 API 21RFID 模块UHF 模块厂商提供 AAR测试设备Android 10 工业手持终端工程结构上uniapp 项目根目录下建一个nativeplugins文件夹里面放原生插件的目录。插件目录结构如下nativeplugins/ RFIDModule/ android/ libs/ rfid-sdk.aar src/ main/ java/ com/example/rfid/ RFIDModule.java AndroidManifest.xml package.jsonpackage.json里声明插件的 id、版本、名称、方法列表。这个文件是 uniapp 识别插件的关键格式必须对否则 HBuilderX 打包时会报“插件不存在”。3.2 原生插件核心代码实现先看 Module 的主体结构。我把它简化成最核心的几个方法public class RFIDModule extends UniModule { private Context mContext; private BroadcastReceiver mReceiver; private UniJSCallback mCallback; private MapString, Long mTagCache new HashMap(); UniJSMethod(uiThread true) public void init(UniJSCallback callback) { mContext mUniSDKInstance.getContext(); mCallback callback; registerReceiver(); callback.invoke(init success); } private void registerReceiver() { mReceiver new BroadcastReceiver() { Override public void onReceive(Context context, Intent intent) { String action intent.getAction(); if (com.example.rfid.TAG_DATA.equals(action)) { byte[] epcBytes intent.getByteArrayExtra(epc); String epc bytesToHex(epcBytes); long now System.currentTimeMillis(); Long last mTagCache.get(epc); if (last null || now - last 500) { mTagCache.put(epc, now); JSONObject result new JSONObject(); result.put(epc, epc); result.put(rssi, intent.getIntExtra(rssi, 0)); result.put(antenna, intent.getIntExtra(antenna, 1)); mCallback.invoke(result.toString()); } } } }; IntentFilter filter new IntentFilter(com.example.rfid.TAG_DATA); mContext.registerReceiver(mReceiver, filter); } UniJSMethod(uiThread true) public void startInventory(UniJSCallback callback) { // 调用厂商 SDK 开始盘存 RfidSdk.getInstance().startInventory(); callback.invoke(started); } UniJSMethod(uiThread true) public void stopInventory(UniJSCallback callback) { RfidSdk.getInstance().stopInventory(); callback.invoke(stopped); } UniJSMethod(uiThread true) public void setPower(int power, UniJSCallback callback) { RfidSdk.getInstance().setPower(power); callback.invoke(power set to power); } Override public void onActivityDestroy() { if (mReceiver ! null) { mContext.unregisterReceiver(mReceiver); } super.onActivityDestroy(); } private String bytesToHex(byte[] bytes) { StringBuilder sb new StringBuilder(); for (byte b : bytes) { sb.append(String.format(%02X, b)); } return sb.toString(); } }这段代码有几个关键点。第一UniJSMethod(uiThread true)表示方法在 UI 线程执行因为广播注册和 SDK 调用通常要求在主线程。第二去重缓存mTagCache放在原生层避免高频数据穿透到 JS 层。第三onActivityDestroy里注销广播防止内存泄漏。package.json的内容大概长这样{ name: RFIDModule, id: RFIDModule, version: 1.0.0, description: RFID识别原生插件, android: { plugins: [ { type: module, name: RFIDModule, class: com.example.rfid.RFIDModule } ] } }3.3 uniapp 侧调用与数据展示JS 层的调用非常直接。先引入插件const rfid uni.requireNativePlugin(RFIDModule);然后初始化并监听数据export default { data() { return { tagList: [], tagSet: new Set(), scanning: false }; }, methods: { initRfid() { rfid.init((res) { console.log(RFID init:, res); }); }, startScan() { this.scanning true; rfid.startInventory((res) { console.log(inventory started); }); // 监听标签数据 this.listenTag(); }, listenTag() { // 这里用轮询或全局事件的方式获取数据 // 实际项目中建议用 uni.$on 或 callback 持续回调 }, stopScan() { this.scanning false; rfid.stopInventory((res) { console.log(inventory stopped); }); }, onTagReceived(tag) { if (this.tagSet.has(tag.epc)) return; this.tagSet.add(tag.epc); this.tagList.unshift({ epc: tag.epc, rssi: tag.rssi, antenna: tag.antenna, time: new Date().toLocaleTimeString() }); } }, onLoad() { this.initRfid(); }, onUnload() { this.stopScan(); } };这里有个实际开发中的取舍原生插件的 callback 是一次性的还是持续性的UniJSCallback默认调用一次就释放如果要持续回调需要用UniJSCallback的invokeAndKeepAlive方法或者改用全局事件fireGlobalEventCallback。我这次用的是全局事件在原生层收到广播后mUniSDKInstance.fireGlobalEventCallback(rfidTag, result)JS 层用uni.$on(rfidTag, handler)监听。这种方式更灵活多个页面都能监听同一个事件。3.4 功率与盘存参数调优功率设置不是越大越好。我做过一组对比测试同一批标签不同功率下的读取表现功率(dBm)读取距离误读率备注15约 0.5m极低适合近距离精确读取20约 1.5m低日常盘点推荐25约 3m中远距离快速盘存30约 5m高容易串读相邻区域标签实际项目中我一般把功率设成 20-23dBm配合天线增益和标签灵敏度基本能覆盖 1-2 米的作业范围。如果是仓储整托盘盘点可以临时调到 26dBm但要注意屏蔽相邻货架。盘存指令的重复次数Session 和 Target也影响读取效果。Session 1 适合快速移动的标签Session 2 适合静态盘点。Target A 和 Target B 交替使用可以减少标签“疲劳”提高读取率。这些参数厂商 SDK 一般都有接口建议在初始化时根据场景预设好。4. 常见问题与排查技巧实录4.1 广播收不到数据怎么办这是最高频的问题。排查顺序我总结成一张表现象可能原因排查方法完全收不到广播Action 写错用厂商 Demo 对比 Action 字符串偶尔收到注册时机不对改到 onResume 注册onPause 注销收到但数据为空Extra key 不对打印 intent.getExtras() 看所有 keyAndroid 8 收不到隐式广播限制确认是自定义 Action动态注册锁屏后收不到后台限制申请前台服务或保持屏幕常亮我踩过最坑的一次是 Action 字符串里多了一个空格肉眼根本看不出来用equals对比才发现。所以建议把 Action 定义成常量原生和 JS 层都引用同一个值。4.2 标签重复上报导致列表卡顿前面提过两级去重这里补充一个细节去重的时间窗口设多少合适设太短同一个标签会重复出现设太长标签离开后再回来会被误判为已存在。我的经验值是 500ms 到 1s。如果是高速传送带场景可以缩短到 200ms如果是人工盘点1s 足够。另外列表渲染用unshift把新标签插到最前面比push到末尾体验好因为用户更关注刚读到的标签。但unshift在数据量大时性能差超过 500 条建议改用虚拟列表或分页加载。4.3 原生插件打包后找不到类这个问题通常出在混淆配置上。厂商的 AAR 里如果有反射调用的类混淆后类名变了就会ClassNotFoundException。解决办法是在proguard-rules.pro里 keep 住厂商 SDK 的包名-keep class com.example.rfid.** { *; } -keep class com.rfid.sdk.** { *; }还有一个常见原因是package.json里的 class 路径写错了或者 AAR 没有正确放到libs目录并在build.gradle里引用。打包前一定要用uni.getSystemInfo确认插件是否被识别如果requireNativePlugin返回 undefined说明插件根本没打进去。4.4 权限申请框监听不到热词里有人问“uniapp 能不能实时监听权限申请框的出现和消失”这个问题在 RFID 场景里很实际。Android 的权限申请框是系统弹的JS 层确实监听不到它的显示和消失。但你可以通过plus.android的requestPermissions回调来判断用户是否授权。更稳妥的做法是在原生插件里封装权限申请逻辑申请结果通过 callback 返回给 JSJS 根据结果决定是否继续初始化 RFID。4.5 不同厂商模块的适配经验我前后接过三家厂商的模块广播 Action 和数据格式都不一样。有的用com.rfid.data有的用android.intent.action.RFIDEPC 有的给 byte 数组有的给十六进制字符串。适配的关键是抽象出一层“数据适配器”把不同厂商的原始数据统一转成{epc, rssi, antenna, timestamp}这个标准结构。上层业务只认这个结构换模块时只改适配器不动业务代码。另外有些模块的 SDK 需要在Application里初始化有些需要在Activity里。如果厂商文档没写清楚就去看它的 Demo 工程照抄初始化位置。这个细节看似小但初始化位置不对后面所有调用都会失败。5. 性能优化与上线前的检查清单5.1 高频数据下的 UI 流畅度RFID 盘存时一秒几十条数据是常态。如果每条数据都触发setData页面必卡。我的优化手段有三个第一原生层做时间窗口去重把上报频率压到每秒 5-10 条第二JS 层用requestAnimationFrame或定时器批量更新列表比如每 200ms 把缓冲区的新标签一次性渲染第三列表项尽量简单不要放复杂组件图片用懒加载。实测下来经过这三层优化即使同时读到 200 个标签列表滚动依然流畅。如果还是卡那就上虚拟列表只渲染可视区域的项。5.2 电量与发热控制RFID 模块持续发射射频信号耗电和发热都不小。工业手持终端一般电池容量大但长时间盘存还是要注意。我的做法是不盘存时立即调用stopInventory不要让它空转功率不要长期开最高如果设备支持设置盘存间隔比如读 100ms 停 50ms既能读到标签又降低功耗。发热方面模块天线附近温度会升高这是正常的。但如果烫手就要检查功率是否过高或者模块是否被遮挡导致反射功率过大。5.3 上线前的自检清单打包上架前我一般会过一遍这个清单[ ] 原生插件在 release 包中能正常加载[ ] 混淆规则已添加厂商 SDK 类未被混淆[ ] 动态权限已申请拒绝后有引导提示[ ] 广播接收器在页面销毁时已注销[ ] 去重逻辑生效无重复标签[ ] 功率默认值合理不会干扰其他设备[ ] 异常情况下模块未连接、SDK 初始化失败有兜底提示[ ] 连续盘存 30 分钟无崩溃、无内存泄漏这个清单看着简单但每一条我都踩过坑。尤其是混淆和广播注销前者导致线上包直接闪退后者导致页面退出后还在后台收数据白白耗电。6. 从 RFID 延伸到其他外设接入的思路这套“广播 原生插件”的架构其实不只适用于 RFID。扫码枪、NFC 读卡器、串口传感器、甚至某些蓝牙设备只要厂商提供了 Android 广播接口都可以用同样的模式接入 uniapp。核心思路就是原生层负责和硬件打交道把数据标准化JS 层负责业务逻辑和 UI两层之间用 callback 或全局事件通信。我后来用同样的架构接了一款 NFC 读卡器只改了广播 Action 和数据解析部分插件主体代码几乎没动。这说明架构的抽象层次是对的。如果你后续要接其他外设建议把“广播接收 数据标准化”这部分做成通用模块不同设备只写适配器能省很多重复劳动。最后分享一个调试技巧在原生插件里加一个“原始数据日志”开关把收到的每一条广播的原始 byte 数组和解析后的结果都打到 logcat 里。调试阶段打开上线前关掉。这个日志在排查数据格式问题时非常有用比猜来猜去高效得多。