
1. 东集PDA扫码的底层链路为什么是广播而不是API调用1.1 激光扫描头到应用层的完整数据流先花两分钟把原理讲清楚后面调参排错的时候你会感谢这一段。东集PDA东大集成AUTOID系列说白了是一台带激光扫描头的安卓手机。它的扫描头不是USB外设那种简单HID设备而是挂在系统内部的硬件模块。你按下物理扫描键侧键或背面扳机键扫描头开始工作激光反馈识别条码之后数据流其实有三跳第一跳扫描头将识别的条码内容通过串口/内核驱动传给系统里一个常驻后台服务。东集把这个服务预装在系统固件里负责管理扫描头的工作模式、声音、震动、广播分发。这个服务你在应用层直接访问不到它也不对外开放业务接口。第二跳这个常驻服务拿到条码内容后按照厂商预置的规则把结果封装成一个Android系统广播发出去。广播的action名称、extra字段名在不同型号、不同安卓版本上可能不一样这是后面避坑的根源。第三跳我们自己的uniapp应用通过plus.android动态注册一个BroadcastReceiver接住这个广播从intent的extra里取出条码字符串然后做业务处理——查询、入库、盘点、校验随你。理解了这三跳你就知道为什么网上大量东集PDA扫码教程都是原生Android开发因为厂商文档默认你是在写原生App。而uniapp这边虽然也有官方插件市场和第三方封装的原生插件但很多要么收费、要么适配不全、要么版本老旧。广播监听方案纯JS实现不依赖第三方插件反而成了反应最快、最可控的路线。1.2 广播监听相比厂商SDK集成的优势可能有同学会问为什么不去集成东集官方SDK我最初也想走这条路调研一圈放弃了。东集官方SDK是基于原生Android jar包设计的里面大量API需要你在MainActivity里调用或者通过AIDL绑定厂商服务。uniapp要复用这套东西只有两种做法一是写原生插件用Android Studio封装Module再离线打包门槛高、迭代慢二是用uts插件虽然现在uniapp支持uts写Android原生逻辑但仍需要你懂Java/Kotlin、懂Android接口对前端背景的团队来说学习成本不小。广播监听完全绕开了这些。你不需要绑定任何服务不需要引入任何jar包不需要和厂商的AIDL接口打交道。本质上就是系统里一个喊话机制——扫码服务把结果喊出来你竖着耳朵听就行。这种模式天然适配uniapp这种基于WebView/JS引擎的技术栈因为plus.android已经把Android的运行时能力暴露给了JS层。从维护成本看广播方案的稳定性也足够好。只要PDA系统里那个扫码服务不被杀掉、广播action配置正确它就能一直工作。我手上跑了两个月的库存盘点项目每周几百条码扫下来没有因为这个方案出过线上事故。1.3 这套方案能用在哪些设备上很多读者手上其实不一定是东集可能是优博讯、霍尼韦尔、斑马、新大陆。这里统一回答凡是基于Android系统、且扫码结果通过系统广播广播的PDA这套方案都适用。区别只在于广播action和extra字段名不同。以我实测过的几款为例设备品牌常见广播action常见extra字段东集AUTOID系列android.intent.action.SCANRESULTSCAN_BARCODE部分安卓扫码枪android.intent.action.SCANRESULTBARCODE优博讯部分型号android.intent.action.DECODE_DATAbarcode厂商自定义协议com.seuic.scan.ACTION_SCANRESULT等code/result所以拿到任何一台新设备先不要急着写代码花几分钟做一次广播抓取确认action和字段名后面就一路通畅。抓取方法在第4章会详细讲。2. 动手前的三个准备权限、构建、测试机2.1 manifest.json权限配置清单这个环节最冤——明明代码写对了打包出来就是收不到广播一查权限没配。在HBuilderX里打开manifest.json切到App权限配置页界面上的权限勾选只是可视化的更可靠的方式是直接编辑源码视图下的app-plus.distribute.android.permissions确保这些权限在列表里permissions: { Android: { androidPermissions: [ android.permission.VIBRATE, android.permission.CAMERA, android.permission.FLASHLIGHT, android.permission.WAKE_LOCK ] } }VIBRATE和FLASHLIGHT对应扫码时的震动和闪光灯反馈如果你希望在应用内控制这些必须声明。CAMERA是为了确保激光头在某些厂商固件里被识别为摄像设备部分机型扫描头驱动依赖摄像头权限。WAKE_LOCK是防止扫描过程中系统休眠导致服务异常。还有一个经常被忽略的如果你的PDA系统是Android 6.0以上动态权限也需要处理。CAMERA这类危险权限在打包安装后默认可能没有授出。最简单的方式是安装后到系统设置里手动授权或者用uni.authorize主动申请。2.2 云打包与离线打包对广播监听的影响广播监听方案本身不依赖原生工程所以传统云打包就能跑通不需要自定义调试基座。但这里有一个前提你必须在标准基座里测试过再打正式包。为什么特意提这个因为HBuilderX的标准基座包含了一整套plus.android的实现广播注册在标准基座里没问题。但如果你勾选了使用自定义基座而这个基座是其他同事用别人电脑打的、里面的uniapp原生引擎版本和你本机不一致偶尔会出现plus.android.implements在运行时抛异常的情况。遇到这种怪问题先换回标准基座或重新打自定义基座再说。另外plus.android的implements和registerReceiver在HBuilderX 3.x各个版本中API是稳定的只要你用的是近两年的版本不用纠结升级。我项目里一个3.2.x的老工程和一个3.99的新工程这套代码原样复制都能跑。2.3 没有真机时怎么模拟测试做PDA开发最尴尬的就是没有真机。广播监听方案不是纯前端逻辑模拟器里没有扫码服务你怎么测我的做法是先做一个模拟扫码广播的小工具页面开发阶段放在App里通过按钮手动触发一次广播发送内容是一个写死的测试条码。这样在普通安卓手机上也能验证广播接收器的注册、解析、回调链路是否正常。sendTestBroadcast() { const Intent plus.android.importClass(android.content.Intent) const mainActivity plus.android.runtimeMainActivity() const intent new Intent() intent.setAction(android.intent.action.SCANRESULT) intent.putExtra(SCAN_BARCODE, TEST20240101001) mainActivity.sendBroadcast(intent) }这个技巧还有一个额外用途等你在真机上调试时如果怀疑是扫码服务的问题也可以用这个工具页面手动发广播快速判断是广播没发出来还是接收器没收到。这个定位思路排错时能帮你省半小时。3. 5分钟核心实现uniapp注册广播接收器全代码3.1 核心代码注册监听与扫描结果解析直接上完整代码这是我项目里精简后的一个扫码页面拷贝过去改改业务逻辑就能跑。template view classscan-page view classscan-tips请扫描条码/view view classscan-result text v-ifscanCode{{ scanCode }}/text text v-else classplaceholder等待扫码.../text /view /view /template script export default { data() { return { scanCode: , receiver: null, receiverRegistered: false } }, onShow() { this.registerScanner() }, onHide() { this.unregisterScanner() }, onUnload() { this.unregisterScanner() }, methods: { registerScanner() { // plus环境在App端必然已就绪但稳妥起见加个判断 if (!window.plus || this.receiverRegistered) { return } const mainActivity plus.android.runtimeMainActivity() // 创建广播接收器实例 // 注意implements第二个参数是JSON对象onReceive是回调方法 this.receiver plus.android.implements(io.dcloud.android.content.BroadcastReceiver, { onReceive: (context, intent) { // 这个箭头函数内部的this指向由于箭头函数没有自己的this // 所以这里能直接用外部this指向Vue实例 const action intent.getAction() console.log(扫码广播收到action:, action) // 从intent中取extra兼容不同字段名 let code try { code intent.getStringExtra(SCAN_BARCODE) || intent.getStringExtra(BARCODE) || intent.getStringExtra(scan_result) || intent.getStringExtra(barcode) || } catch (e) { console.error(解析广播extra出错, e) } if (code) { this.scanCode code.trim() this.handleScanResult(this.scanCode) } } }) // 注册多个action兼容不同固件版本 // 同一个receiver实例可以注册多个action plus.android.registerReceiver(this.receiver, android.intent.action.SCANRESULT) this.receiverRegistered true // 如果已知你的设备固件会发自定义action可以追加注册 // plus.android.registerReceiver(this.receiver, com.seuic.scan.ACTION_SCANRESULT) }, unregisterScanner() { if (this.receiver this.receiverRegistered) { // 注销必须和注册用同一个receiver实例 plus.android.unregisterReceiver(this.receiver) this.receiver null this.receiverRegistered false console.log(扫码监听已注销) } }, handleScanResult(code) { // 这里写你的业务逻辑查询、校验、跳转等 console.log(最终扫码结果:, code) uni.vibrateShort() } } } /script这里重点说明三件事。第一plus.android.implements(io.dcloud.android.content.BroadcastReceiver)的类名不能写错。很多人从旧项目复制别人的代码写的是android.content.BroadcastReceiver你会发现implements之后运行直接报类不存在。正确写法是io.dcloud.android.content.BroadcastReceiver这是dcloud封装在5运行环境里的实现类。第二onReceive回调里处理业务逻辑时不要做耗时操作。这个回调运行在UI线程上如果你在里面同步查询本地数据库、或者执行复杂的字符串处理连续快速扫码时可能出现界面卡顿。我习惯在回调里只做两件事取码、setData真正的业务逻辑丢到setTimeout或uni.$nextTick里再执行。第三receiver实例一定要存到data()里不要用局部变量。之前遇到过一个情况同事把receiver写成了registerScanner里的局部变量注册完函数执行结束局部变量被回收接收器对应的JS对象被垃圾回收了然后扫码就再也不触发onReceive了。这个坑很隐蔽因为注册那一刻不会报错只有后续扫描时才暴露。3.2 多个广播action兼容注册的写法东集PDA不同批次、不同安卓版本的固件广播action会变。我自己遇到过一个设备列表里有三台不同批次的AUTOID6两台发android.intent.action.SCANRESULT一台发com.seuic.scan.ACTION_SCANRESULT。解决思路很简单同一个receiver实例注册多个action。plus.android.registerReceiver(this.receiver, android.intent.action.SCANRESULT) plus.android.registerReceiver(this.receiver, com.seuic.scan.ACTION_SCANRESULT)这样无论设备发哪个action都能被同一个onReceive接住。但要注意如果两个action都注册了有些固件会把同一条码重复发两次广播发完action A又发action BonReceive会被调用两次。前端需要做一个去重处理。最简单的去重方案记录上一次的扫码值和时间如果在200ms内收到相同内容就忽略。this.lastCode this.lastTime 0 // onReceive中 const now Date.now() if (code this.lastCode now - this.lastTime 200) { return // 重复广播忽略 } this.lastCode code this.lastTime now这个去重逻辑在双广播注册场景下几乎是必需的不加的话线上会出现扫一条码、录两条数据的严重事故。不要问我怎么知道的。3.3 页面生命周期注册、注销的最优时机我见过很多同事写的扫码页面onLoad里注册、onUnload里注销看起来没毛病实际跑起来一堆问题。核心问题在于广播接收器是全局级的不是页面级的。onLoad注册之后如果用户扫码时按下Home键或者跳转到另一个页面页面还在栈里接收器依然活着。此时如果扫码onReceive还是会触发然后在当前这个不可见的页面里执行了业务逻辑等用户切回来发现数据已经变了一脸懵。正确做法是遵循可见才监听原则onShow里注册接收器onHide里注销接收器onUnload里注销接收器兜底防止页面被销毁时忘了注销onShow注册还有个额外好处从扫码页面跳转到详情页再从详情页返回时onShow触发接收器重新注册立刻恢复到可用状态。这个时序恰恰是仓库盘点场景最常见的操作路径。还有一个细节onHide和onUnload都会注销会不会报错不会。unregisterReceiver(this.receiver)在receiver已经注销的情况下调用5运行环境并不会抛异常只是静默忽略。但我在代码里还是加了receiverRegistered标志位一方面避免重复注销另一方面让自己心里有数。4. 踩坑实录东集PDA广播监听的六个经典问题4.1 广播action对不上用adb logcat一分钟定位这是所有坑里出现频率最高的。你按厂商文档写的android.intent.action.SCANRESULT在一台老设备上就是收不到。大部分情况下不是代码问题而是固件版本换了action。定位方法非常朴素抓日志。把PDA用USB连上电脑开启开发者模式然后执行adb logcat -v time | grep -i -E scan|barcode|seuic|SCANRESULT注意如果设备有多个USB模式一定要选文件传输/MTP模式有的设备在仅充电模式下adb连不上。然后按一下扫描键真扫一个条码。看日志输出重点找类似这样的行BroadcastQueue: enqueue order broadcast: Intent { actandroid.intent.action.SCANRESULT flg... has extras }act后面的值就是这款固件实际发出的action。再看后面几行会有Bundle[{SCAN_BARCODE6901234567890}]之类的输出那就是extra字段名和值。把抓到的action替换到代码里问题就解了。这个方法在优博讯、霍尼韦尔、新大陆等设备上一样适用属于通用排查手段。4.2 连续扫描丢码扫描模式和UI线程的处理在仓库批量盘点场景作业员会一直按住扳机键连续快速扫码。这时候最容易出现的问题扫了30条码页面上只出现28条丢了两条。排查下来有两层原因。第一层是PDA自身的扫描模式设置。东集PDA在设置-扫描设置里有手动模式和连续扫描模式。连续模式下激光一直开启扫到条码就触发一次广播。如果两个条码贴得很近、扫得又快部分老固件的扫描服务本身会丢。这个层级的丢码应用侧很难完全弥补。我建议在要求高准确率的场景下把PDA设成手动模式强制一次按键只扫一条代价是效率降低换来准确性。第二层是UI线程处理不过来。onReceive回调被频繁触发时如果每条回调里都执行复杂的业务代码UI线程被占满后续的广播事件在一段时间内得不到处理表现就是丢码。解决方案我在前面提过回调里只做取码存值业务逻辑异步执行。// 错误示范回调里直接做耗时业务 onReceive: (context, intent) { // 查询几百条数据的本地库同步执行严重阻塞 const list uni.getStorageSync(bigList) const result list.filter(item item.code code) // ... } // 正确做法回调里只收码业务丢到异步队列 onReceive: (context, intent) { if (code) { this.$nextTick(() { this.handleScanResult(code.trim()) }) } }4.3 结果自带回车符数据清洗细节这个坑特别隐蔽。PDA扫码服务模仿老式USB键盘扫码枪的行为扫码结束会在条码内容后面自动追加一个回车符或换行符方便老系统的判定逻辑。在uniapp里你把这个值直接赋给input的v-model界面上看不出问题某些情况下\n还是能显示出来但当你拿去和数据库里的商品编码做精确匹配时永远匹配不上。所以无论从哪个字段取到code第一时间做数据清洗// 去掉首尾空白、回车换行、制表符 const cleanCode code.replace(/[\r\n\t\s]/g, )注意我用的是全局替换\s这样即使条码中间混入了特殊字符也能清掉。不过也要看业务场景——如果条码本身允许包含空格某些物流码会那你就只去掉首尾的\r\nconst cleanCode code.trim()两种策略按你的条码规则选。普通商品条码、固定资产标签用trim()就够物流面单、混合编码建议用全局替换。4.4 页面跳转后收不到广播接收器生命周期管理典型案例盘点页面A扫码弹出详情页BB页面里有个继续扫码按钮点击后调用扫描头再扫。结果发现B页面收不到广播。原因就是B页面没有注册接收器。很多人把注册逻辑写在A页面的onLoad里切到B页面时A页面只是onHide了接收器还活着但onReceive里的this还是A页面的实例它收到的数据无法传给B页面。解决方案有两个层级。如果只是A、B两个页面的数据传递可以在B页面的onShow里重新注册接收器让B页面的业务逻辑独立处理。这是最直白的做法。如果是多个页面都要用扫码能力就不要在每个页面里各写一遍注册代码。把扫码监听封装成一个共享模块单例模式在需要扫码的页面onShow里绑定回调onHide解绑回调。第5章我会给出封装代码。4.5 部分机型收不到广播动态权限和厂商设置这是个综合问题。同一套代码在东集AUTOID9上跑得好好的拿到另一台AUTOID6上就收不到广播。排查顺序参考确认这台设备的固件广播action是否不同用4.1的日志法确认应用是否被系统识别为受保护应用。东集PDA的设置-应用-应用管理里有类似信任此应用的选项如果没勾选系统省电策略可能在应用进入后台后杀掉它的进程或限制它的广播接收。这个是厂商定制ROM的问题通用安卓机不会遇到确认Android 8.0以上系统对隐式广播的限制。注意我们动态注册接收器不受这个限制影响但个别厂商固件把扫码广播定义成了隐式广播同时应用处于后台时被系统拦截。解决思路是让应用保持前台可见或者在扫码前先回到App页面这类问题没法写死一个步骤核心思路就是先抓日志确认广播发没发再查应用命运限制最后查系统版本差异。4.6 扫码自动弹输入法焦点控制的两种方案最后一个坑不是技术问题是体验问题。扫码结果回填到input时input自动获得焦点安卓软键盘弹出来把页面挤上去操作员还要手动收起键盘才能继续看结果。两种解决方式。方案一不要用input展示结果用text或view。只读展示场景完全不需要input。view classscan-result{{ scanCode }}/view方案二如果业务上确实需要在结果基础上编辑比如扫码之后还要手工改几位就把input设为只读配合confirm-type和手动控制焦点。PDA往往带实体键盘操作员用实体键输入时弹出的软键盘反而碍事可以用focus属性来精细化控制。input v-modelscanCode :focusinputFocus confirm-typedone /再加一个自定义的软键盘隐藏逻辑// 扫码回调里先收回焦点防止弹键盘 this.scanCode code this.inputFocus false this.$nextTick(() { // 需要用户手动点击输入框时再 focus })核心思想扫码场景下的输入框焦点不应该由系统自动接管而是要完全掌握在业务手里。5. 从一个能跑的Demo到线上稳定运行5.1 长时间运行内存泄漏的预防广播接收器用不好确实有内存泄漏风险但主要原因不是广播没注销而是receiver实例被全局持有同时它内部又持有Activity或页面实例。在uniapp里如果你在onShow里注册、onHide里注销每次注册都是新建receiver实例注销后老实例不再被系统持有理论上可以被回收。但要注意如果你在data里存了receiverVue实例和receiver互相持有页面销毁后这个循环引用如果没被正确解开就会泄漏。所以我的习惯是注销receiver之后立刻把this.receiver置为null切断引用链。这个习惯已经帮我避过至少两次页面退出去再进来扫码越来越卡的问题。5.2 多页面共用扫码能力的封装思路项目里有三四个页面都要扫码每个页面黏贴一遍注册代码维护起来很痛苦。我后来把它抽成了一个独立的模块供所有页面调用。// utils/scanner.js let receiver null let registered false const actionList [ android.intent.action.SCANRESULT, com.seuic.scan.ACTION_SCANRESULT ] let callbackList [] function initScanner() { if (registered || !window.plus) { return } receiver plus.android.implements(io.dcloud.android.content.BroadcastReceiver, { onReceive: (context, intent) { const code intent.getStringExtra(SCAN_BARCODE) || intent.getStringExtra(BARCODE) || intent.getStringExtra(scan_result) || if (code) { // 通知所有当前关心的页面 callbackList.forEach(cb { try { cb(code.trim()) } catch (e) { console.error(扫码回调执行异常, e) } }) } } }) actionList.forEach(action { plus.android.registerReceiver(receiver, action) }) registered true } function destroyScanner() { if (receiver registered) { plus.android.unregisterReceiver(receiver) receiver null registered false } } // 页面级绑定 function bindScan(callback) { callbackList.push(callback) // 确保接收器已注册 initScanner() return () { callbackList callbackList.filter(cb cb ! callback) } } module.exports { bindScan, destroyScanner }页面里这样用import { bindScan } from /utils/scanner.js export default { data() { return { unbindScan: null } }, onShow() { this.unbindScan bindScan((code) { this.scanCode code // 业务处理 }) }, onHide() { if (this.unbindScan) { this.unbindScan() this.unbindScan null } } }这个封装的好处是无论有多少个页面整个应用永远只维护一个广播接收器实例。bindScan返回一个解绑函数页面隐藏时调用解绑即可。彻底解决重复注册导致onReceive被触发多次的问题。5.3 实测体会与后续可扩展方向广播方案我在两个正式项目里跑过一个是仓储盘点东集AUTOID6一个是门店收货验收东集AUTOID9。整体稳定性满足生产需求上线以来没有出现过因广播方案本身导致的扫码漏读事故。后续如果你想做得更完善有几个方向可以探索一是对接东集自带的Datalogic扫描头配置工具通过广播指令切换扫描码制二是在应用内监测扫码服务是否被杀如果被杀通过uni.requireNativePlugin调用系统能力重启服务这步需要厂商ROM支持三是把扫码能力进一步下沉做一个独立的扫码页面通过uni.$emit或事件总线广播给业务页面实现扫码与业务彻底解耦。就我个人而言最想再强调一次的是拿到设备后一定先花5分钟用adb logcat抓一次广播确认action和extra字段名再开始写代码。别问问就是我在第三个项目里不信邪结果被一台改过固件的设备折磨了一整天。