uniapp对接RFID硬件:原生插件封装与盘存优化实战

发布时间:2026/9/19 17:14:33
uniapp对接RFID硬件:原生插件封装与盘存优化实战 1. 从扫码到RFID为什么uniapp项目需要换一套识别思路做过uniapp扫码功能的同学都知道uni.scanCode这个API用起来有多顺手——一行代码调起摄像头二维码、条形码统统搞定。但当你接到一个“用RFID识别资产标签”的需求时打开uniapp官方文档翻一遍会发现根本没有对应的API。这不是文档没写全而是RFID和扫码在技术链路上压根就是两回事。扫码的本质是光学识别摄像头拍一张图像算法解析图像中的编码信息。而RFID的本质是射频识别读写器发射特定频率的电磁波标签通过感应电流获取能量后回传数据。两者从物理层就不一样所以uniapp不可能用一个统一的JS API把两者都封装了。那在uniapp项目里到底怎么接RFID核心思路就一条RFID硬件厂商通常会提供Android原生SDK一个.jar或.aar包我们需要把它包装成uniapp能调用的原生插件然后在vue页面里通过uni.requireNativePlugin来调用。整个链路涉及Android原生开发、uniapp原生插件规范、广播接收、权限处理等多个环节这也是为什么很多前端同学一听到“RFID”就觉得头大。这篇文章适合两类人看一类是已经有一定uniapp开发经验需要对接RFID硬件的开发者另一类是想了解uniapp原生插件机制为后续对接其他硬件如身份证读卡器、指纹模块做技术储备的同学。我会从硬件选型、原生插件封装、广播数据解析、权限适配到打包上架把整条链路拆开讲清楚尽量让没有Android原生开发经验的前端也能跟着走通。提示本文涉及的RFID方案基于Android平台因为uniapp在iOS上对接RFID硬件的限制较多iOS对NFC的开放程度有限且不同厂商SDK差异大所以如果你的项目必须支持iOS建议先和硬件厂商确认是否有iOS端的SDK和MFi认证。2. RFID硬件选型与Android SDK的对接准备2.1 RFID模块的常见形态与通信方式市面上的RFID模块大致分三种形态一体式手持终端比如带RFID功能的PDA、外接式读写器通过蓝牙或USB连接手机、嵌入式模块集成到设备主板上的模组。在uniapp项目里最常见的是前两种。一体式手持终端通常厂商已经预装了Android系统并且提供了对应的SDK你只需要在uniapp项目里调用即可。外接式读写器则分蓝牙和USB两种连接方式蓝牙的需要处理蓝牙配对和数据传输USB的则需要处理USB权限和串口通信。从通信协议上看RFID读写器和Android设备之间通常走的是串口UART或USB HID协议。厂商的SDK一般会把这些底层通信封装好对外暴露几个关键方法初始化、开始盘存、停止盘存、读取标签数据、设置功率等。2.2 拿到SDK之后先做什么拿到厂商提供的SDK通常是一个.zip包里面有.jar/.aar文件、demo工程、API文档不要急着往uniapp项目里塞。正确的做法是先用Android Studio跑通厂商的demo。这一步至关重要因为你需要确认硬件本身是好的、SDK版本和你的Android设备兼容、权限配置正确。很多后续在uniapp里遇到的问题其实在原生demo阶段就能暴露出来。阅读API文档找出核心类和方法。重点关注初始化方法通常需要传入Context、盘存回调接口标签数据通过回调返回、电源控制方法打开/关闭射频、参数设置方法功率、盘存模式等。确认SDK依赖的Android版本和权限。RFID SDK通常需要android.permission.INTERNET、android.permission.ACCESS_NETWORK_STATE如果是蓝牙读写器还需要BLUETOOTH、BLUETOOTH_ADMINAndroid 12以上需要BLUETOOTH_SCAN、BLUETOOTH_CONNECTUSB读写器需要android.hardware.usb.host权限。2.3 uniapp原生插件的基本结构uniapp的原生插件分两种本地插件和云端插件。本地插件放在项目的nativeplugins目录下云端插件则需要在DCloud插件市场购买或下载。对于RFID这种定制化程度高的场景通常需要自己写本地插件。一个标准的Android原生插件目录结构是这样的nativeplugins/ YourRFIDPlugin/ android/ libs/ # 放厂商的.jar/.aar文件 src/ main/ java/ com/ yourcompany/ rfid/ RFIDModule.java # 插件主类 RFIDWXModule.java # 如果需要微信小程序端支持 AndroidManifest.xml build.gradle package.json # 插件配置文件其中RFIDModule.java是核心它需要继承UniModule类并用UniJSMethod(uiThread true)注解暴露给JS调用的方法。package.json里需要声明插件的id、名称、版本、Android依赖等信息。注意厂商的.jar/.aar文件可能和uniapp的默认依赖有冲突比如都引用了同一个版本的okhttp这时候需要在build.gradle里用exclude排除冲突的模块否则打包时会报Duplicate class错误。3. 把厂商SDK包装成uniapp原生插件的完整过程3.1 创建插件工程与配置package.json在HBuilderX里右键项目根目录选择“新建 - 原生插件”会生成一个插件模板。然后修改package.json{ name: YourRFIDPlugin, id: YourRFIDPlugin, version: 1.0.0, description: RFID读写器原生插件, _dp_type: nativeplugin, _dp_nativeplugin: { android: { plugins: [ { type: module, name: YourRFIDPlugin, class: com.yourcompany.rfid.RFIDModule } ], integrateType: aar, dependencies: [ com.android.support:appcompat-v7:28.0.0 ], abis: [ armeabi-v7a, arm64-v8a ], permissions: [ android.permission.INTERNET, android.permission.BLUETOOTH, android.permission.BLUETOOTH_ADMIN ] } } }这里有几个关键点integrateType指定为aar表示以aar形式集成abis指定支持的CPU架构RFID SDK通常包含.so文件需要根据厂商提供的架构来配置permissions里声明的权限会在打包时自动写入AndroidManifest。3.2 编写RFIDModule主类RFIDModule需要继承UniModule核心方法包括初始化、开始盘存、停止盘存、释放资源。下面是一个简化版的代码框架package com.yourcompany.rfid; import android.content.Context; import android.util.Log; import com.alibaba.fastjson.JSONObject; import com.taobao.weex.annotation.JSMethod; import com.taobao.weex.bridge.JSCallback; import com.taobao.weex.common.WXModule; import io.dcloud.feature.uniapp.annotation.UniJSMethod; import io.dcloud.feature.uniapp.bridge.UniJSCallback; import io.dcloud.feature.uniapp.common.UniModule; public class RFIDModule extends UniModule { private RFIDReader rfidReader; private UniJSCallback inventoryCallback; private boolean isInventorying false; UniJSMethod(uiThread true) public void init(JSONObject options, UniJSCallback callback) { try { Context context mUniSDKInstance.getContext(); rfidReader new RFIDReader(context); boolean success rfidReader.init(); JSONObject result new JSONObject(); result.put(success, success); callback.invoke(result); } catch (Exception e) { JSONObject error new JSONObject(); error.put(success, false); error.put(message, e.getMessage()); callback.invoke(error); } } UniJSMethod(uiThread true) public void startInventory(JSONObject options, UniJSCallback callback) { if (rfidReader null) { JSONObject error new JSONObject(); error.put(success, false); error.put(message, 请先调用init初始化); callback.invoke(error); return; } inventoryCallback callback; isInventorying true; rfidReader.startInventory(new RFIDReader.InventoryListener() { Override public void onTagRead(String epc, String rssi) { if (inventoryCallback ! null isInventorying) { JSONObject tag new JSONObject(); tag.put(epc, epc); tag.put(rssi, rssi); tag.put(timestamp, System.currentTimeMillis()); inventoryCallback.invokeAndKeepAlive(tag); } } }); } UniJSMethod(uiThread true) public void stopInventory(UniJSCallback callback) { isInventorying false; if (rfidReader ! null) { rfidReader.stopInventory(); } JSONObject result new JSONObject(); result.put(success, true); callback.invoke(result); } UniJSMethod(uiThread true) public void release(UniJSCallback callback) { isInventorying false; if (rfidReader ! null) { rfidReader.release(); rfidReader null; } JSONObject result new JSONObject(); result.put(success, true); callback.invoke(result); } }这里用到了invokeAndKeepAlive这是uniapp原生插件里实现持续回调的关键方法。普通的callback.invoke()只能回调一次而invokeAndKeepAlive可以多次回调适合盘存过程中不断返回标签数据的场景。3.3 处理广播接收与数据解析很多RFID读写器尤其是外接式蓝牙/USB读写器是通过**广播Broadcast**的方式把标签数据发给Android系统的。厂商SDK内部通常会注册一个BroadcastReceiver收到广播后解析出EPC、RSSI等数据再通过回调接口抛给上层。如果你发现厂商SDK没有提供回调接口而是直接发广播那就需要自己在插件里注册广播接收器private BroadcastReceiver rfidReceiver new BroadcastReceiver() { Override public void onReceive(Context context, Intent intent) { String action intent.getAction(); if (com.yourcompany.rfid.ACTION_TAG_READ.equals(action)) { String epc intent.getStringExtra(epc); String rssi intent.getStringExtra(rssi); if (inventoryCallback ! null isInventorying) { JSONObject tag new JSONObject(); tag.put(epc, epc); tag.put(rssi, rssi); inventoryCallback.invokeAndKeepAlive(tag); } } } };注册广播的代码放在init方法里IntentFilter filter new IntentFilter(); filter.addAction(com.yourcompany.rfid.ACTION_TAG_READ); mUniSDKInstance.getContext().registerReceiver(rfidReceiver, filter);注意Android 8.0以上对隐式广播做了限制如果厂商SDK用的是自定义action的广播需要确认是否在AndroidManifest里静态注册了或者是否需要在代码里动态注册。动态注册的广播在组件销毁时一定要记得unregisterReceiver否则会内存泄漏。3.4 在uniapp页面里调用插件插件写好后在vue页面里这样调用const rfidModule uni.requireNativePlugin(YourRFIDPlugin); export default { data() { return { tags: [], isScanning: false }; }, methods: { initRFID() { rfidModule.init({}, (res) { if (res.success) { uni.showToast({ title: 初始化成功 }); } else { uni.showToast({ title: 初始化失败 res.message, icon: none }); } }); }, startScan() { this.isScanning true; this.tags []; rfidModule.startInventory({}, (res) { // 这个回调会被多次触发每次返回一个标签 if (res.epc) { const exist this.tags.find(t t.epc res.epc); if (!exist) { this.tags.push({ epc: res.epc, rssi: res.rssi, timestamp: res.timestamp }); } } }); }, stopScan() { this.isScanning false; rfidModule.stopInventory((res) { console.log(停止盘存, res); }); } }, onUnload() { if (this.isScanning) { rfidModule.stopInventory(() {}); } rfidModule.release(() {}); } };这里有个细节startInventory的回调是持续触发的每次读到标签都会调一次。所以在页面里要做去重处理否则同一个标签会被重复添加。去重的逻辑可以基于EPC因为EPC是标签的唯一标识。4. 盘存数据去重、信号强度过滤与实时渲染优化4.1 为什么盘存数据需要去重和过滤RFID盘存的一个特点是同一个标签在盘存周期内会被多次读到。读写器每秒可能盘存几十次每次都会返回标签数据。如果不做处理页面上会瞬间涌入大量重复数据导致渲染卡顿甚至崩溃。去重只是第一步更关键的是信号强度RSSI过滤。RSSI值反映的是标签距离读写器的远近通常是一个负值比如-30dBm到-80dBm。RSSI越接近0说明信号越强标签越近。在实际项目中我们通常只关心一定距离内的标签所以可以设置一个RSSI阈值比如只保留RSSI大于-60dBm的标签。4.2 去重与过滤的具体实现在vue页面里可以用一个Map来存储已读到的标签key是EPCvalue是标签对象。每次收到新数据时先查Map里是否已存在如果存在则更新RSSI和最后读取时间如果不存在则新增。data() { return { tagMap: {}, // 用对象模拟Map tagList: [], // 用于渲染的数组 rssiThreshold: -60 }; }, methods: { handleTag(tag) { if (tag.rssi this.rssiThreshold) { return; // 信号太弱忽略 } if (this.tagMap[tag.epc]) { // 已存在更新RSSI和时间 this.tagMap[tag.epc].rssi tag.rssi; this.tagMap[tag.epc].lastSeen Date.now(); } else { // 新增 this.tagMap[tag.epc] { epc: tag.epc, rssi: tag.rssi, firstSeen: Date.now(), lastSeen: Date.now() }; } this.syncTagList(); }, syncTagList() { // 把Map转成数组按RSSI从强到弱排序 this.tagList Object.values(this.tagMap).sort((a, b) b.rssi - a.rssi); } }4.3 实时渲染的性能优化如果标签数量很多比如上百个每次更新都触发this.tagList ...会导致整个列表重新渲染性能很差。优化的思路有几种节流更新不要每次收到标签都更新UI而是用一个定时器比如每500毫秒把最新的数据同步到tagList。虚拟列表如果标签数量超过几百个用uniapp的scroll-view配合虚拟列表方案只渲染可视区域内的标签。分页展示如果业务上不需要同时展示所有标签可以分页显示每页20个。节流更新的实现data() { return { pendingUpdate: false }; }, methods: { handleTag(tag) { // ...去重逻辑... if (!this.pendingUpdate) { this.pendingUpdate true; setTimeout(() { this.syncTagList(); this.pendingUpdate false; }, 500); } } }提示RSSI值波动比较大同一个标签在不同时刻读到的RSSI可能相差10dBm以上。如果业务对距离判断要求高建议对RSSI做滑动平均滤波比如取最近5次的平均值这样距离判断会更稳定。5. Android权限适配与打包上架的踩坑记录5.1 动态权限申请的时机RFID插件涉及的权限分两类普通权限在AndroidManifest里声明即可和危险权限需要运行时动态申请。蓝牙相关的权限在Android 12以上属于危险权限必须动态申请。在uniapp里可以用plus.android.requestPermissions来申请权限const permissions [android.permission.BLUETOOTH_SCAN, android.permission.BLUETOOTH_CONNECT]; plus.android.requestPermissions(permissions, (result) { if (result.granted.length permissions.length) { // 权限全部授予 this.initRFID(); } else { uni.showToast({ title: 请授予蓝牙权限, icon: none }); } }, (error) { console.error(权限申请失败, error); });这里有个坑权限申请是异步的而且用户可能拒绝。所以初始化RFID的逻辑必须放在权限申请成功的回调里不能直接放在onLoad里。5.2 打包时的常见错误与解决打包uniapp原生插件时最常见的错误是依赖冲突和so文件缺失。依赖冲突的典型报错是Duplicate class com.xxx.xxx found in modules。解决方法是找到冲突的依赖在build.gradle里用exclude排除implementation(com.yourcompany:rfid-sdk:1.0.0) { exclude group: com.squareup.okhttp3, module: okhttp }so文件缺失的报错是java.lang.UnsatisfiedLinkError: dlopen failed: library libxxx.so not found。解决方法是确认abis配置里包含了厂商SDK支持的架构并且so文件确实被打包进了apk。可以用解压工具打开apk检查lib/目录下是否有对应的so文件。5.3 上架安卓应用市场的注意事项上架国内安卓应用市场时有几个和RFID相关的点需要特别注意权限说明如果应用申请了蓝牙权限需要在隐私政策里说明用途比如“用于连接RFID读写器设备”。隐私合规RFID读取的标签数据如果涉及用户信息需要在隐私政策里说明数据的收集、使用和存储方式。targetSdkVersion国内应用市场对targetSdkVersion有要求通常要求不低于30。如果厂商SDK的targetSdkVersion较低可能需要在build.gradle里覆盖配置。注意有些应用市场会检测应用是否真的使用了申请的权限。如果申请了蓝牙权限但实际没有蓝牙功能可能会被驳回。所以不要为了“以防万一”而申请不必要的权限。6. 从单次盘存到批量管理业务层的扩展思路6.1 标签数据的本地缓存与同步在实际项目中RFID盘存往往不是一次性的而是需要把盘存结果保存下来后续做资产盘点、出入库管理等。这时候就需要考虑数据的本地缓存和同步。本地缓存可以用uni.setStorageSync把标签列表存到本地key可以用盘存任务的ID。同步则需要在有网络的时候把数据上传到服务器。这里要注意的是盘存数据可能很大比如一次盘存几千个标签直接上传可能会超时建议分批上传每批100条。6.2 盘存任务的断点续传如果盘存过程中应用被切到后台或者崩溃了重新打开后应该能恢复之前的盘存状态。实现思路是每次盘存到新标签时除了更新内存里的数据还要把数据持久化到本地存储。应用启动时先检查是否有未完成的盘存任务如果有则恢复。// 保存盘存状态 saveInventoryState() { const state { taskId: this.taskId, tags: Object.values(this.tagMap), startTime: this.startTime, isScanning: this.isScanning }; uni.setStorageSync(inventory_state_ this.taskId, state); }, // 恢复盘存状态 restoreInventoryState(taskId) { const state uni.getStorageSync(inventory_state_ taskId); if (state) { this.taskId state.taskId; this.tagMap {}; state.tags.forEach(tag { this.tagMap[tag.epc] tag; }); this.syncTagList(); } }6.3 多读写器场景下的数据合并有些项目需要同时连接多个RFID读写器比如一个仓库有多个门禁读写器这时候需要把多个读写器的数据合并到一起。合并的关键是标签去重和位置标记。每个读写器有一个唯一ID标签数据里带上读写器ID这样就能知道某个标签是被哪个读写器读到的。在插件层面可以支持同时初始化多个读写器实例每个实例有独立的回调。在业务层面用一个全局的Map来合并数据key是EPCvalue里包含一个readers数组记录哪些读写器读到了这个标签。7. 一些实测有效的调试技巧与经验总结调试RFID插件最头疼的问题是看不到原生层的日志。HBuilderX的控制台只能看到JS层的日志原生层的Logcat日志需要单独查看。我的做法是用Android Studio的Logcat工具过滤tag为RFIDModule的日志。在插件的关键节点打日志比如初始化、开始盘存、收到标签、停止盘存。如果插件崩溃了Logcat里会有FATAL EXCEPTION的堆栈信息根据堆栈定位问题。另一个经验是先用厂商demo验证硬件再写插件。我遇到过好几次插件写好了但读不到标签排查半天发现是硬件本身的问题比如天线没接好、标签损坏、功率设置太低。如果先用demo验证硬件是好的就能排除硬件因素专注排查插件代码。还有一个坑是盘存功率的设置。功率太高会导致读取范围过大读到不需要的标签功率太低又读不到远处的标签。通常厂商SDK会提供设置功率的方法建议在初始化时根据实际场景调整。比如资产盘点场景功率可以设高一些门禁场景功率设低一些避免读到门外的标签。最后说一个关于标签数据格式的问题。不同厂商的RFID标签EPC编码格式可能不一样。有的用十六进制字符串有的用Base64有的直接是字节数组。在插件里最好统一转成十六进制字符串再传给JS层这样业务层处理起来比较方便。如果厂商SDK返回的是字节数组可以用bytesToHexString方法转换private String bytesToHexString(byte[] bytes) { StringBuilder sb new StringBuilder(); for (byte b : bytes) { String hex Integer.toHexString(b 0xFF); if (hex.length() 1) { sb.append(0); } sb.append(hex); } return sb.toString().toUpperCase(); }这些细节看起来不起眼但在实际项目里往往就是这些地方卡住你半天。希望这篇内容能帮你在uniapp对接RFID的路上少走一些弯路。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询