NFC 这玩意儿在 Android 上一直有点"薛定谔"的味道——你永远不知道用户手里那台设备到底能不能稳定读到你的标签。我最早接触 NFC 是在做门禁卡模拟的项目里,当时用一台旧手机写 NDEF 数据,换台设备就读不出来,排查了半天才发现是标签容量和 NDEF 格式的兼容性问题。后来接触到 HCE(Host Card Emulation,主机卡模拟),才意识到 Android 从 4.4 开始就已经把"手机变成一张卡"这件事做成了系统级能力,只是大多数人只知道用它来做支付,很少有人拿它来做设备间的数据共享。
这篇内容就是围绕Android HCE 模拟 NFC 标签、实现跨设备 NDEF 数据共享这个主题展开的。我会从 HCE 的底层机制讲起,把 APDU 指令的交互流程拆开揉碎,然后给出一个可以直接跑通的完整实现方案,最后聊聊我在实际调试中踩过的那些坑。适合有一定 Android 基础、想深入理解 NFC 通信原理的开发者,也适合做物联网设备配网、身份凭证传递、离线数据交换这类场景的同行参考。
1. 先搞清楚 HCE 到底在模拟什么
1.1 从物理标签到软件模拟的本质区别
要理解 HCE,得先明白普通 NFC 标签是怎么工作的。一张 NTAG213 或者 Mifare Classic 的标签,本质上就是一块带天线的存储芯片,读卡器发出 13.56MHz 的载波信号,标签通过电磁感应取电,然后把存储区里的数据以特定格式回传。整个过程里,标签是被动的,它没有 CPU,没有操作系统,只是按照预设的协议响应读卡器的指令。
HCE 做的事情,是把这块"存储芯片"用软件来模拟。手机里的 NFC 控制器收到读卡器的指令后,不再去访问物理标签,而是把指令转发给操作系统里的一个服务,由这个服务来决定怎么响应。这个服务在 Android 里就是HostApduService,它跑在应用进程里,你可以用任意代码来生成响应数据。
这个区别带来的最大好处是:数据是动态的。物理标签写进去什么就是什么,而 HCE 可以根据时间、位置、用户状态、后端下发的数据来实时生成响应内容。比如你可以做一个"动态凭证"系统,每次读卡返回的 NDEF 数据都不一样,这在物理标签上是不可能实现的。
但代价也很明显:HCE 依赖手机的电量和 NFC 控制器状态,响应速度受系统调度影响,而且不是所有 Android 设备都支持——虽然现在支持率已经很高了,但一些低端机或者定制 ROM 仍然可能阉割掉这个功能。
1.2 HCE 与 SE、eSE 的三角关系
Android 的 NFC 架构里,卡模拟有三种模式:基于安全单元(SE/eSE)的卡模拟、基于主机的卡模拟(HCE),以及基于 SIM 卡的卡模拟。这三者的核心区别在于"密钥和敏感数据存在哪里"。
SE 方案里,数据存在一颗独立的安全芯片里,这颗芯片通过了各种安全认证,能抵抗物理攻击和侧信道攻击,所以银行支付类应用基本都用这个。但 SE 的访问权限被运营商或者手机厂商牢牢把控,普通开发者根本拿不到。
HCE 方案里,数据存在应用进程的内存或者本地存储里,安全性完全靠应用自己保证。Android 用了一个叫AID(Application ID)路由的机制来把读卡器的指令分发到对应的应用。每个 HCE 应用在 Manifest 里声明自己支持的 AID 列表,当读卡器发送 SELECT AID 指令时,系统会查找匹配的应用并把后续的 APDU 都转发给它。
这里有个关键点:AID 路由是有优先级的。如果多个应用声明了同一个 AID,系统会按照用户设置的默认应用或者安装顺序来决定。而且从 Android 10 开始,后台启动的 HCE 服务会被限制,必须在前台或者有前台服务的情况下才能正常响应。
1.3 NDEF 在 HCE 场景下的特殊处理
NDEF(NFC Data Exchange Format)是 NFC 论坛定义的一种数据封装格式,一条 NDEF 消息可以包含多个 NDEF 记录,每条记录有类型、ID、负载等字段。物理标签通常会把 NDEF 消息直接存在存储区里,读卡器读取时按照 TLV 格式解析。
但在 HCE 场景下,读卡器并不知道对面是物理标签还是手机,它只会按照 NFC Forum 定义的标准流程来操作。对于 Type 4 Tag(也就是 ISO-DEP 协议上的 NDEF 标签),读卡器会先发送 SELECT AID 指令选择 NDEF 应用(AID 是D2760000850101),然后通过一系列 APDU 来读取 Capability Container、NDEF 数据等。
所以 HCE 模拟 NDEF 标签的核心工作,就是在 HostApduService 里正确响应这些标准 APDU 指令,让读卡器以为自己在跟一张真实的 Type 4 Tag 通信。这比简单地返回一段数据要复杂得多,因为你需要实现完整的状态机。
2. APDU 指令交互的完整拆解
2.1 Type 4 Tag 的 APDU 指令集
要让 HCE 模拟的标签被读卡器正确识别,必须实现 Type 4 Tag 规范里定义的那几条核心 APDU。我把它们整理成了一张表,方便对照:
| 指令名称 | CLA | INS | P1 | P2 | 功能说明 |
|---|---|---|---|---|---|
| Select NDEF App | 00 | A4 | 04 | 00 | 选择 NDEF 应用,数据域为 AID |
| Select CC File | 00 | A4 | 00 | 0C | 选择 Capability Container 文件 |
| Read CC | 00 | B0 | 00 | 00 | 读取 CC 文件内容 |
| Select NDEF File | 00 | A4 | 00 | 0C | 选择 NDEF 数据文件 |
| Read NDEF | 00 | B0 | 00 | 00 | 读取 NDEF 数据 |
| Write NDEF | 00 | D6 | 00 | 00 | 写入 NDEF 数据(可选) |
每条指令的响应都包含两部分:数据域和状态字(SW1 SW2)。状态字90 00表示成功,6A 82表示文件未找到,6D 00表示指令不支持。读卡器会根据状态字来判断下一步操作。
2.2 Capability Container 的构造逻辑
CC 文件是 Type 4 Tag 的"身份证",它告诉读卡器这张标签的容量、支持的协议版本、NDEF 文件的位置等信息。CC 文件固定 15 字节,格式如下:
字节 0-1: CCLEN (CC 文件长度,固定 0x000F) 字节 2: 映射版本 (0x20 表示版本 2.0) 字节 3-4: MLe (最大读长度,通常 0x003B 即 59 字节) 字节 5-6: MLc (最大写长度,通常 0x0034 即 52 字节) 字节 7: TLV 类型 (0x04 表示 NDEF 文件控制 TLV) 字节 8-9: NDEF 文件长度 (最大可存储的 NDEF 消息字节数) 字节 10: NDEF 文件 ID (0x00) 字节 11-12: 读访问权限 (0x00 表示自由读) 字节 13-14: 写访问权限 (0x00 表示自由写,0xFF 表示只读)这里有个容易踩的坑:MLe 和 MLc 的值会影响读卡器的行为。如果 MLe 设得太小,读卡器会分多次读取,增加交互轮次;如果设得太大,又可能超出某些读卡器的缓冲区。我实测下来,MLe 设为 0x003B、MLc 设为 0x0034 是比较稳妥的选择,兼容性最好。
NDEF 文件长度字段决定了读卡器认为这张标签能存多少数据。你可以把它设得比实际需要的大一些,但不要超过 0xFFFE,否则某些读卡器会解析异常。
2.3 NDEF 消息的 TLV 封装
NDEF 文件本身不是裸的 NDEF 消息,而是用 TLV(Type-Length-Value)格式封装的。对于 NDEF 数据,TLV 类型是0x04,后面跟两个字节的长度(大端序),然后是 NDEF 消息本体。最后还要加一个0xFE作为终止 TLV。
举个例子,如果 NDEF 消息是D1 01 0A 55 04 65 78 61 6D 70 6C 65 2E 63 6F 6D(一条 URI 记录),那么完整的 NDEF 文件内容就是:
04 00 10 D1 01 0A 55 04 65 78 61 6D 70 6C 65 2E 63 6F 6D FE其中04是 TLV 类型,00 10是长度(16 字节),后面是 NDEF 消息,最后FE是终止符。
在 HCE 的 processCommandApdu 方法里,你需要根据读卡器发来的指令,动态构造这些响应数据。读卡器可能会分多次读取 NDEF 文件,所以你的服务需要维护一个"当前读取偏移量"的状态。
3. 从零搭建一个可用的 HCE NDEF 服务
3.1 项目配置与 AID 注册
先在 Android Studio 里新建一个项目,minSdkVersion 建议设为 21(Android 5.0),因为 HCE 的完整 API 从 19 开始支持,但 21 以上的兼容性更好。
在AndroidManifest.xml里注册服务:
<service android:name=".MyHceService" android:exported="true" android:permission="android.permission.BIND_NFC_SERVICE"> <intent-filter> <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" /> </intent-filter> <meta-data android:name="android.nfc.cardemulation.host_apdu_service" android:resource="@xml/apduservice" /> </service>然后在res/xml/apduservice.xml里声明 AID:
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android" android:description="@string/service_desc" android:requireDeviceUnlock="false"> <aid-group android:description="@string/aid_group_desc" android:category="other"> <aid-filter android:name="D2760000850101" /> </aid-group> </host-apdu-service>这里的D2760000850101就是 NFC Forum 定义的 NDEF Type 4 Tag 应用 AID。requireDeviceUnlock设为 false 表示锁屏状态下也能响应,如果你做的是支付类应用,建议设为 true。
注意:从 Android 10 开始,如果应用在后台,HCE 服务可能无法被唤醒。你需要确保应用在前台,或者启动一个前台服务来维持 HCE 的可用性。
3.2 HostApduService 的核心实现
服务类需要继承HostApduService,重写processCommandApdu和onDeactivated两个方法。核心逻辑是根据 CLA 和 INS 来分发处理:
public class MyHceService extends HostApduService { private static final byte[] NDEF_AID = hexToBytes("D2760000850101"); private static final byte[] CC_FILE = hexToBytes("000F20 003B 0034 04 00FF 00 00 00 00"); private byte[] ndefFile; private int readOffset = 0; @Override public byte[] processCommandApdu(byte[] apdu, Bundle extras) { if (apdu == null || apdu.length < 4) { return hexToBytes("6D00"); } byte cla = apdu[0]; byte ins = apdu[1]; byte p1 = apdu[2]; byte p2 = apdu[3]; if (cla == 0x00 && ins == (byte) 0xA4) { return handleSelect(apdu); } else if (cla == 0x00 && ins == (byte) 0xB0) { return handleReadBinary(apdu); } else if (cla == 0x00 && ins == (byte) 0xD6) { return handleUpdateBinary(apdu); } return hexToBytes("6D00"); } private byte[] handleSelect(byte[] apdu) { if (apdu.length < 5) return hexToBytes("6A82"); int aidLen = apdu[4] & 0xFF; if (apdu.length < 5 + aidLen) return hexToBytes("6A82"); byte[] aid = Arrays.copyOfRange(apdu, 5, 5 + aidLen); if (Arrays.equals(aid, NDEF_AID)) { readOffset = 0; return hexToBytes("9000"); } return hexToBytes("6A82"); } private byte[] handleReadBinary(byte[] apdu) { int offset = ((apdu[2] & 0xFF) << 8) | (apdu[3] & 0xFF); int le = apdu.length > 4 ? (apdu[4] & 0xFF) : 0; byte[] source = (offset < CC_FILE.length) ? CC_FILE : ndefFile; int realOffset = (offset < CC_FILE.length) ? offset : offset - CC_FILE.length; if (realOffset >= source.length) return hexToBytes("6B00"); int len = Math.min(le == 0 ? source.length - realOffset : le, source.length - realOffset); byte[] data = Arrays.copyOfRange(source, realOffset, realOffset + len); return concat(data, hexToBytes("9000")); } }这段代码里有个细节需要注意:CC 文件和 NDEF 文件是分开的两个"文件",读卡器会先 SELECT CC 文件读取能力信息,再 SELECT NDEF 文件读取实际数据。我在代码里用 offset 是否小于 CC_FILE.length 来区分,这是一种简化处理,更严谨的做法是维护一个当前选中文件的状态变量。
3.3 NDEF 消息的构造与动态更新
构造 NDEF 消息可以用 Android 自带的NdefRecord和NdefMessage类,非常方便:
public void updateNdefData(String text) { NdefRecord record = NdefRecord.createTextRecord("zh", text); NdefMessage message = new NdefMessage(new NdefRecord[]{record}); byte[] ndefBytes = message.toByteArray(); // 封装 TLV ByteArrayOutputStream bos = new ByteArrayOutputStream(); bos.write(0x04); bos.write((ndefBytes.length >> 8) & 0xFF); bos.write(ndefBytes.length & 0xFF); bos.write(ndefBytes); bos.write(0xFE); ndefFile = bos.toByteArray(); }如果你想让数据动态变化,可以在每次processCommandApdu被调用时重新生成 NDEF 消息。比如根据当前时间生成一个动态令牌,或者从后端拉取最新数据。但要注意,不要在 processCommandApdu 里做耗时操作,因为这个方法是在 Binder 线程里同步执行的,超过几秒不响应读卡器就会超时。
我的做法是提前在后台线程准备好数据,processCommandApdu 只负责读取内存里的缓存。如果需要实时拉取,可以用一个短超时的本地缓存,或者返回上一次的数据并异步更新。
4. 跨设备调试中那些让人抓狂的问题
4.1 读卡器兼容性:为什么同一张"卡"有的设备读得到有的读不到
这是我在调试中遇到最多的问题。同样的 HCE 服务,小米手机能读到,三星读不到;或者同一个读卡器,读物理标签正常,读 HCE 就失败。排查下来主要有几个原因:
第一是 AID 匹配问题。有些读卡器在 SELECT AID 之后,还会发送一些厂商自定义的指令来探测标签类型。如果你的服务对这些指令返回6D00,读卡器可能就直接放弃了。解决办法是在 processCommandApdu 里对未知指令返回9000加空数据,而不是错误码,让读卡器继续走标准流程。
第二是时序问题。HCE 的响应延迟比物理标签高得多,物理标签通常在几毫秒内响应,而 HCE 可能需要几十毫秒甚至上百毫秒。有些读卡器的超时设置比较短,就会认为标签不存在。这个只能通过优化代码来缓解,比如把 NDEF 数据预先生成好,避免在响应时做计算。
第三是 NDEF 文件长度字段的设置。我遇到过一种情况,CC 文件里声明的 NDEF 文件长度是 0x00FF,但实际返回的数据超过了这个长度,读卡器解析时就出错了。一定要确保声明的长度和实际数据一致。
4.2 状态机维护:读卡器分片读取时的偏移量处理
Type 4 Tag 的读取是分片的,读卡器会发送多次 READ BINARY 指令,每次读取一部分数据。如果你的服务没有正确维护读取偏移量,就会返回错误的数据。
我一开始的实现是每次 READ BINARY 都从头返回数据,结果读卡器读到的 NDEF 消息总是被截断。后来改成根据 APDU 里的 P1P2 字段来计算偏移量,才解决了问题。P1 是高字节,P2 是低字节,组合起来就是 16 位的偏移地址。
但这里还有个坑:CC 文件和 NDEF 文件的偏移量是独立的。读卡器读取 CC 文件时偏移量从 0 开始,读取 NDEF 文件时偏移量也从 0 开始。所以你不能用一个全局的 readOffset,而要根据当前选中的文件来分别维护。
4.3 锁屏与后台限制:Android 版本差异带来的行为变化
Android 各个版本对 HCE 的后台限制越来越严格。在 Android 9 及以前,只要服务注册了,锁屏状态下也能响应。但从 Android 10 开始,如果应用在后台且没有前台服务,HCE 可能会被系统挂起。
我实测下来,最稳妥的方案是:在应用启动时检查 NFC 权限和 HCE 可用性,然后启动一个前台服务来维持 HCE 的活跃状态。前台服务需要显示一个通知,虽然用户体验上有点打扰,但这是目前唯一可靠的做法。
另外,requireDeviceUnlock这个配置项也要注意。设为 false 时锁屏可用,但某些 ROM 会忽略这个设置强制要求解锁。设为 true 时则必须解锁才能响应,适合安全性要求高的场景。
4.4 用另一台手机做读卡器时的特殊处理
调试 HCE 最方便的方式是用另一台支持 NFC 的手机做读卡器。Android 提供了NfcAdapter.enableReaderMode方法,可以让你在应用里直接读取 NFC 标签,包括 HCE 模拟的标签。
Bundle options = new Bundle(); options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250); nfcAdapter.enableReaderMode(this, new NfcAdapter.ReaderCallback() { @Override public void onTagDiscovered(Tag tag) { Ndef ndef = Ndef.get(tag); if (ndef != null) { try { ndef.connect(); NdefMessage message = ndef.getNdefMessage(); // 处理读取到的 NDEF 消息 ndef.close(); } catch (Exception e) { Log.e(TAG, "读取失败", e); } } } }, NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, options);注意FLAG_READER_SKIP_NDEF_CHECK这个标志,加上它可以让读卡器跳过 NDEF 格式检查,直接返回原始数据。这在调试阶段很有用,因为如果 NDEF 格式有问题,不加这个标志的话读卡器可能直接不回调。
还有一个经验:两台手机贴在一起时,NFC 天线位置很关键。不同手机的天线位置不一样,有的在背面顶部,有的在中间。我调试时经常要把两台手机反复调整位置才能触发,建议先用物理标签确认读卡器的天线位置,再对准 HCE 手机的天线。
5. 把 HCE 用在真实场景里的几个思路
5.1 设备配网:用 NFC 传递 Wi-Fi 凭证
这是 HCE 最实用的场景之一。智能家居设备通常没有屏幕和键盘,配网很麻烦。如果设备端带 NFC 读卡器,手机端用 HCE 模拟一张 NDEF 标签,里面包含 Wi-Fi 的 SSID 和密码,设备一碰就能读取并自动连接。
NDEF 消息可以用NdefRecord.createMime("application/vnd.wfa.wsc", wifiConfigBytes)来构造,其中 wifiConfigBytes 是 Wi-Fi 配置的二进制格式。不过这个格式比较复杂,实际实现时建议用 Android 的WifiConfiguration或者WifiNetworkSpecifier来生成配置数据。
这个方案的好处是不需要设备端有屏幕,也不需要手机装额外的 App(如果做成系统级功能的话)。而且 NFC 的通信距离很短(通常几厘米),天然具有"物理接近"的安全性,不会像蓝牙那样被远程嗅探。
5.2 身份凭证:动态令牌的离线验证
HCE 可以模拟一张"动态卡",每次读取时返回不同的数据。比如你可以实现一个基于时间的一次性密码(TOTP)系统,NDEF 消息里包含当前时间窗口的令牌值。读卡器端拿到令牌后,用相同的密钥和算法验证,就能确认身份。
这种方案适合门禁、考勤、活动签到等场景。相比静态的物理卡,动态令牌更难被复制,因为每次读取的值都不一样。但要注意,HCE 不能完全替代安全芯片,因为密钥存在应用进程里,root 过的设备或者被调试的应用都可能泄露密钥。如果安全性要求很高,还是得用 SE 方案。
5.3 数据交换:两台设备之间的离线传输
两台都支持 HCE 和读卡器模式的设备,可以通过 NFC 交换数据。一台开 HCE 模拟标签,另一台开读卡器模式读取。这种方式不需要网络,不需要配对,碰一下就能传数据。
传输的数据量受限于 NFC 的速率和 NDEF 消息的大小。Type 4 Tag 的 NDEF 文件最大可以到 64KB 左右,但实际传输时受读卡器缓冲区限制,通常单次传输几 KB 比较稳妥。如果要传大文件,可以分多次读取,每次读一部分,但这样交互轮次会很多,用户体验不好。
我的建议是:NFC 只用来传递"连接信息",比如蓝牙地址、Wi-Fi 直连的 SSID 和密码,实际的大数据传输走蓝牙或者 Wi-Fi。这样既利用了 NFC 的便捷性,又避开了它的速率瓶颈。
5.4 调试工具:用 HCE 模拟各种标签做测试
如果你在做 NFC 读卡器端的开发,HCE 是一个非常好的测试工具。你可以用 HCE 模拟各种类型的标签,测试读卡器对不同 NDEF 格式、不同容量、不同协议的兼容性。这比买一堆物理标签要方便得多,而且可以随时修改数据。
我通常会做一个"调试模式"的 HCE 应用,里面预置几种常见的标签配置:空标签、只读标签、大容量标签、格式错误的标签等。测试读卡器时切换不同的配置,就能快速定位兼容性问题。
6. 一些不那么显然的实操心得
6.1 NDEF 消息的字节序和长度字段
NDEF 记录里的长度字段有大端序和小端序的区别,具体取决于记录类型。短记录(SR=1)的长度字段是 1 字节,普通记录是 4 字节大端序。TLV 的长度字段是 2 字节大端序。这些细节如果搞错了,读卡器解析出来的数据就是乱的。
我建议在构造 NDEF 消息时,尽量用 Android 提供的NdefMessage.toByteArray()方法,它会自动处理这些格式。只有在需要手动构造特殊格式时才自己拼字节,而且一定要用十六进制工具验证输出。
6.2 服务被系统回收后的恢复
HostApduService 是一个普通的 Android Service,系统在内存紧张时可能会回收它。如果服务被回收了,HCE 就会失效,直到应用重新启动。这个问题在低内存设备上特别明显。
解决办法是在onDeactivated回调里检查服务状态,如果是因为系统回收导致的 deactivate,可以尝试重新启动服务。但更可靠的做法还是用前台服务,让系统知道这个服务很重要,不要轻易回收。
6.3 不同 ROM 的兼容性差异
国内各大厂商的 ROM 对 NFC 和 HCE 的支持程度差异很大。我实测下来,原生 Android 和小米、三星的兼容性最好,华为和 OPPO 在某些版本上会有问题,特别是一些定制化的省电策略会限制 HCE 的后台响应。
如果你的应用需要覆盖多种设备,建议在代码里加一些兼容性检测和降级处理。比如检测到 HCE 不可用时,提示用户手动开启 NFC 或者切换到其他交互方式。
6.4 测试时的一个小技巧
调试 HCE 时,我经常需要反复修改代码然后测试。如果每次都重新安装应用,很浪费时间。我的做法是在应用里加一个"重新加载"的按钮,点击后重新生成 NDEF 数据并重启 HCE 服务。这样不用重新安装就能测试不同的数据配置。
另外,用adb logcat过滤HostApduService相关的日志,可以看到系统分发的 APDU 指令和你的响应,对排查问题非常有帮助。我通常会把 processCommandApdu 的输入输出都打上日志,这样一眼就能看出是哪条指令出了问题。
6.5 关于 NDEF 写入的注意事项
虽然 Type 4 Tag 规范支持写入 NDEF 数据,但在 HCE 场景下,写入操作需要特别小心。因为 HCE 服务是无状态的,每次读卡器连接都是一个新的会话,你写入的数据需要持久化到本地存储,否则下次读取就丢了。
而且写入操作涉及权限控制,如果 CC 文件里声明了写保护,读卡器就无法写入。我一般建议在 HCE 场景下把标签设为只读,数据由应用自己管理,通过其他方式(比如应用内设置或者后端下发)来更新。
6.6 性能优化的几个方向
HCE 的响应速度直接影响用户体验。我做过一些优化,效果比较明显的有:预生成 NDEF 字节数组,避免在 processCommandApdu 里做序列化;用静态变量缓存 CC 文件和 NDEF 文件,避免重复构造;减少日志输出,特别是在生产版本里关掉详细日志。
还有一个容易被忽略的点:processCommandApdu 返回的字节数组不要太大。虽然理论上可以返回几 KB 的数据,但实际测试中,超过 256 字节的响应在某些读卡器上会出现问题。如果需要传输大块数据,还是分片读取比较稳妥。
7. 从 HCE 延伸出去的一些思考
HCE 这个技术本身并不新,但它的应用场景一直在扩展。除了前面提到的配网、凭证、数据交换,我还见过用它来做电子票务、会员卡、设备配对等场景。核心思路都是一样的:把手机变成一张可以动态生成内容的卡。
但 HCE 也有它的边界。它不适合做高安全性的支付,因为密钥保护不如 SE;不适合做大数据量传输,因为 NFC 速率有限;不适合做需要长时间保持连接的应用,因为 NFC 是短连接协议。理解这些边界,才能在对的场景用对的技术。
我在实际项目里,通常会把 HCE 和其他技术组合使用。比如用 HCE 传递蓝牙配对信息,然后切换到蓝牙做实际的数据传输;或者用 HCE 做身份验证,验证通过后切换到 Wi-Fi 做后续通信。这种"NFC 触发 + 其他通道传输"的模式,在实践中效果很好。
最后分享一个我在调试中总结的小经验:每次修改 HCE 相关代码后,最好把手机的 NFC 开关关掉再打开。因为 Android 的 NFC 服务会缓存 AID 路由信息,有时候代码改了但路由没更新,导致调试结果和预期不符。这个坑我踩过好几次,后来养成了改代码就重启 NFC 的习惯,省了不少排查时间。