NFC贴上去没反应,连接状态和前台优先级不是一码事
一个仓库盘点工具,用平板贴货架上的 NFC 标签来核对库位。代码照着文档写了:tag.on('readerMode', ...) 注册监听,回调里拿 TagInfo,然后 getNdef(tagInfo).connect(),再读 NdefMessage。办公室里贴着测试标签,偶尔能读出数据,偶尔什么反应都没有;切到后台回个消息再切回来,之后就再也读不到了,日志里只有一行 nfc unavailable。你把平板重启、把标签换了一张又一张,问题时好时坏。最后发现两个根因叠在一起:connect() 不是“读数据”的附属动作,它是独立的状态迁移,跳过了它,后面的读写全是 3100201;而前台调度不是“注册一次就完事”的常驻开关,应用退到后台再回来,你没有重新举手,系统就把标签事件发给了别人。
connect() 是独立的状态迁移,不是读数据的“顺手一步”
NFC 标签的读取模型很容易让人误以为“拿到 TagInfo 就可以直接读”。getTagInfo 返回的是系统在分发标签时给的描述对象,它告诉你这个标签支持哪些技术、TagInfo 长什么样,但它不代表你已经和标签建立了连接。文档写得很直白:TagSession 是所有 NFC 技术类型的基类,connect() 的作用是和标签建立连接,在从标签读取数据或将数据写入标签之前,必须调用此方法。
不调 connect() 直接读,或者调了 connect() 但返回 false 之后仍然继续往下走,拿到的错误码通常是 3100201(Tag running state is abnormal in the service)。这个错误码的可能原因列了五条,其中一条就是“Tag 操作前,已经处在断开状态”,处理步骤的第一条是“先调用连接,再执行读写操作”。另一个高频错误码是 13023,Tech has not connected,语义比 3100201 更直接:你试图在不处于连接状态的技术上执行操作。
正确的顺序是显式的:拿到 TagInfo → getXXX(tagInfo) 获取具体技术实例 → await session.connect() → 检查连接结果 → 读/写 → session.close() 断开连接。connect() 本身是异步的,不要假设它瞬间完成。
typescript
import { tag } from '@kit.ConnectivityKit';
async function readNdefFromTag(tagInfo: tag.TagInfo): Promise<string> {
const ndef = tag.getNdef(tagInfo);
if (!ndef) {
throw new Error('tag does not support NDEF');
}
const connected = await ndef.connect();
if (!connected) {
// 连接失败,不要继续往下读
throw new Error('connect failed');
}
try {
const message = await ndef.readNdef();
return parseNdefText(message);
} finally {
// 读写结束必须断开,否则下一次触碰可能拿到 13022
await ndef.close();
}
}
finally 里的 close() 不是可选清理。一个没有断开的连接会占着底层会话,下一次同一标签或不同标签靠近时,connect() 可能直接返回 13022(Tech already connected),语义是“已经连上了,你又来连”。连接状态是每个技术实例上的显式状态,它不会因为你“这次没读成功”或者“页面切走了”就自动归零。
readerMode 是前台优先权的声明,不是常驻扫描开关
connect() 解决的是“拿到标签之后怎么和它通信”。但很多“贴上去完全没反应”的现场,问题出在更前面:系统压根没有把标签事件分发给你。
HarmonyOS 的 NFC 前台调度采用“前台优先”机制:设备检测到标签时,系统先问“当前前台是谁,谁来处理这个标签”。tag.on('readerMode', ...) 或者 nfcController.registerForegroundDispatch() 是你在“举手”声明“这个前台页面要处理标签事件”。这个声明有明确的寿命边界:你必须在页面前台可见时注册,在离开前台时注销。只在 onCreate 或 aboutToAppear 里注册一次,应用退到后台再回来,系统认为你已经不在前台处理队列里了,标签事件不会再发给你。
typescript
// 页面级注册,和可见性对齐
aboutToAppear(): void {
tag.on('readerMode', elementName, techList, this.onTagDiscovered);
}
aboutToDisappear(): void {
tag.off('readerMode', elementName);
}
techList 这一行是第二个关键。你只监听你真正需要的技术类型,不要把所有类型都塞进去。NFC_A、NFC_B、NFC_F、NFC_V 覆盖的卡种不同,门禁卡和标准 NDEF 标签可能落在不同的技术分支上。techList 写得太窄,标签靠近时你的应用没有匹配上,系统就会弹出一个“选择应用打开”的对话框,或者直接把事件交给其他注册了匹配技术的应用。
on 和 off 必须成对,页面没 on 就 off 会直接报错
tag.off 不是“幂等清理”,它有一个明确的前置条件:必须先调用过 on,才允许调用 off。错误码 3100203 的语义就是“接口调用顺序错误:必须在 on 已经被调用之后,才允许调用 off”。
一个很常见的泄漏模式是页面销毁时无条件调 off,但页面可能因为某些路径没有走到 on(比如权限没授予、设备不支持、用户快速进出页面)。on 没有执行,off 却执行了,3100203 弹出来,你的 catch 把它当成“清理失败”吞掉,但真正的隐患不是这个错误码本身,而是你无法再信任“当前是否处于监听状态”这个事实。用显式的布尔值或者代次来记录当前页面是否完成了注册,off 只在注册确实发生过的时候调用。
typescript
private registered: boolean = false;
private register(): void {
tag.on('readerMode', this.elementName, this.techList, this.onTagDiscovered);
this.registered = true;
}
private unregister(): void {
if (!this.registered) return;
tag.off('readerMode', this.elementName);
this.registered = false;
}
NDEF 文本不是“整段 payload 转字符串”,状态位和语言码要跳过
connect() 和前台调度都对了之后,下一个让数据“读出来了但不对”的地方是 NDEF 解析。
NdefMessage 里的 NdefRecord 的 payload 不是纯文本。对于 Text Record,payload 的第一个字节是状态位,它告诉你文本的编码是 UTF-8 还是 UTF-16;紧接着是语言码,长度由状态位里的一个字段决定。真正的文本内容从语言码之后才开始。把整段 payload 直接 new TextDecoder().decode(payload),会把状态位和语言码一起解成乱码。
typescript
function parseNdefText(message: tag.NdefMessage): string {
const record = message.getNdefRecords()[0];
const payload = new Uint8Array(record.payload);
const status = payload[0];
const isUtf16 = (status & 0x80) !== 0;
const langCodeLength = status & 0x3F; // 低 6 位是语言码长度
const textStart = 1 + langCodeLength; // 跳过状态位和语言码
const textBytes = payload.slice(textStart);
const encoding = isUtf16 ? 'utf-16le' : 'utf-8';
return new util.TextDecoder(encoding).decode(textBytes);
}
status & 0x3F 取的是低 6 位,那是语言码的长度。status & 0x80 判断编码位,为 1 表示 UTF-16。你贴出来的 FF FE 开头,正是 UTF-16 小端的 BOM,按 UTF-16LE 解码才能拿到正确的中文。门禁卡很多时候根本不是 NDEF 标签,它只提供 NFC_A 或 NFC_B 技术层,没有 NdefMessage 可读。把“读不出文本”归因于解析代码之前,先确认你手里的标签到底是不是 NDEF 格式。
设备能力判断的 API 分界线在 26.0.0,不要拿旧写法赌新设备
isNfcAvailable 的判断方式在 API 26.0.0 前后有变化。API 26.0.0 之前,需要 canIUse("SystemCapability.Communication.NFC.Tag") 并且 nfcController.isNfcAvailable() 同时判断;从 API 26.0.0 开始,以 canIUse 为准,官方明确提示继续依赖旧接口可能导致运行稳定性问题。
这意味着如果你的 targetSdkVersion 升到了 26,能力探测应该按新规来,而不是保留“isNfcAvailable 返回 false 就说明设备没 NFC”的旧假设。NFC 开关关闭(13001)和设备根本不支持 NFC(13000)是两种不同的状态,前者可以引导用户打开,后者只能走降级路径。
排查顺序:先看举手,再看连接,最后看格式
下次 NFC 贴上去没反应、或者时灵时不灵的时候,按这个顺序走,比反复换标签快。
第一步,确认 tag.on('readerMode', ...) 或者 registerForegroundDispatch 是不是在前台可见的生命周期里注册的,off / unregister 是不是在离开前台时成对执行了。只在 onCreate 里注册一次,后台回来不重新举手,是“切走再回来读不到”的第一嫌疑人。
第二步,在 onTagDiscovered 回调里确认标签事件确实到达了你的应用。如果系统弹出了选择应用对话框,说明前台分发没有成为最高优先级,或者 techList 和标签的实际技术不匹配。
第三步,拿到 TagInfo 之后,确认 connect() 被显式调用了,且返回值被检查了。connect 失败或者跳过 connect,读写阶段的 3100201、13023 就会替你把“状态不对”翻译成一个看起来像“读写失败”的错误。
第四步,如果数据读出来了但内容是乱的,检查 NDEF 解析有没有跳过状态位和语言码,有没有按状态位选择 UTF-8 或 UTF-16LE。如果是门禁卡,先确认它是不是 NDEF 标签,不要对着一个没有 NDEF 记录的技术层反复调 readNdef。
NFC 的“没反应”很少是硬件坏了,也很少是某一个接口写错了。它通常发生在两个契约的交界处:前台优先权的举手和放手,以及连接状态的建立和断开。connect() 不是可选的顺手一步,readerMode 不是注册一次就永久生效的扫描开关,NDEF 的 payload 也不是一整段可以直接解码的字符串。下一次标签贴上去安安静静的时候,先别换标签——打开代码看一眼,你的应用在这个前台,到底有没有举手。

更多推荐



所有评论(0)