Android NFC读取完整指南:从权限配置到NDEF解析
2026/9/17 18:42:49 网站建设 项目流程

简介:本资源是一份面向Android开发者的NFC标签数据读取实战指南,聚焦移动终端近距离无线通信技术的工程落地,适用于具备基础Java/Kotlin开发能力的中初级工程师快速掌握NFC功能集成。内容系统覆盖NFC硬件准备、AndroidManifest权限与特性声明、三类Intent过滤器(ACTION_NDEF_DISCOVERED/ACTION_TECH_DISCOVERED/ACTION_TAG_DISCOVERED)的配置细节、NfcAdapter初始化与onNewIntent事件处理流程,并附有完整Activity代码示例及NDEF数据解析逻辑。资源为单文件PDF文档,共1个文件,大小118KB,轻量便携,适合作为开发备忘或教学参考。目前已有2810人学习下载,内容结构清晰,从原理简介到代码实现层层递进,特别包含nfc_tech_filter.xml技术白名单配置、多层过滤优先级说明及公交卡等非标准标签的拓展提示,助力开发者规避常见兼容性问题并高效完成NFC功能接入。

1. Android 上用手机 NFC 读取标签数据,不是“打开设置点一下”就能搞定的事

很多开发者第一次尝试在 Android 应用里读取 NFC 标签时,会以为只要在AndroidManifest.xml里加个<uses-permission android:name="android.permission.NFC" />就能立刻调用NfcAdapter.getDefaultAdapter()成功获取实例——结果null,或者enableReaderMode()SecurityException,又或者前台 Activity 死活收不到NDEF_DISCOVEREDIntent。根本原因在于:NFC 在 Android 中不是“即开即用”的传感器,而是一套需严格匹配硬件能力、系统状态、Activity 生命周期和 Intent 过滤规则的事件驱动机制。它要求你同时满足四层条件:设备支持 NFC 硬件(且未被厂商禁用)、用户已手动开启 NFC 开关、应用拥有NFC权限(非危险权限,但需显式声明)、Activity 正处于前台并已注册正确的 Intent Filter 或 Reader Mode 回调。本文面向 Android 8.0(API 26)及以上主流版本,覆盖从targetSdkVersion=33的权限适配到NfcAdapterNdef解析的完整链路,不依赖任何第三方 SDK,所有代码均可在 Android Studio Flamingo 及以上版本直接编译运行。

2. 从 Manifest 声明到 Activity 生命周期:NFC 读取的四大必要条件缺一不可

2.1 Manifest 中必须声明的三类配置:权限、功能、Intent Filter

仅声明<uses-permission android:name="android.permission.NFC" />是远远不够的。Android 要求你明确告知系统:你的应用不仅需要 NFC 权限,还实际使用 NFC 功能,并且希望接收特定类型的 NFC 标签事件。这三者必须同时存在,否则系统不会将 NFC 事件路由给你的 Activity。

<!-- 1. NFC 权限(非危险权限,但仍需声明) --> <uses-permission android:name="android.permission.NFC" /> <!-- 2. 声明 NFC 硬件功能为“非必需”(避免 Google Play 过滤掉无 NFC 设备) --> <uses-feature android:name="android.hardware.nfc" android:required="false" /> <!-- 3. Intent Filter:用于 NDEF_DISCOVERED 场景(最常用) --> <intent-filter> <action android:name="android.nfc.action.NDEF_DISCOVERED" /> <category android:name="android.intent.category.DEFAULT" /> <!-- 必须指定 data scheme 或 mime-type,否则会被系统忽略 --> <data android:scheme="http" /> <!-- 或者:<data android:mimeType="text/plain" /> --> </intent-filter>

提示<data>标签是硬性要求。如果你不写<data>,即使 NFC 开关打开、标签靠近,系统也不会触发你的 Activity。这是因为 Android 为防止恶意应用劫持所有 NFC 事件,强制要求 Intent Filter 必须精确匹配标签内容类型。常见组合包括scheme="http"(对应 URL 类型 NDEF 记录)、mimeType="text/plain"(纯文本)、scheme="https",或更严格的host="example.com"+pathPrefix="/nfc"。不要试图用通配符*,它不被支持。

2.2 检查 NFC 硬件可用性与开关状态:不能跳过的运行时校验

NfcAdapter.getDefaultAdapter(this)返回null,意味着当前设备根本不支持 NFC(如部分平板、低端机型),或系统级 NFC 模块被禁用(如 MIUI 中“小米钱包”关闭后 NFC 功能整体失效)。你必须在onCreate()onResume()中做双重检查:

// Java 示例(Kotlin 同理) private NfcAdapter nfcAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); nfcAdapter = NfcAdapter.getDefaultAdapter(this); if (nfcAdapter == null) { // 设备无 NFC 硬件 Toast.makeText(this, "本设备不支持 NFC", Toast.LENGTH_LONG).show(); return; } if (!nfcAdapter.isEnabled()) { // NFC 开关未打开 AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("NFC 未启用") .setMessage("请前往设置 > 连接 > NFC 开启此功能") .setPositiveButton("去设置", (dialog, which) -> { Intent intent = new Intent(Settings.ACTION_NFCS_SETTINGS); startActivity(intent); }) .setNegativeButton("取消", null) .show(); } }

注意nfcAdapter.isEnabled()仅反映系统 NFC 开关状态,不保证硬件物理连通。某些 OEM(如三星、小米)存在固件级限制:即使开关打开,若未安装对应钱包 App(如 Samsung Pay、Mi Wallet),底层 NFC 控制器仍可能无法初始化。此时getDefaultAdapter()可能返回非 null,但后续enableReaderMode()会抛出SecurityException。因此,真实可用性必须通过enableReaderMode()的回调验证。

2.3 Activity 生命周期绑定:为什么 onResume() 才是 NFC 启动的唯一时机

NFC 事件(尤其是NDEF_DISCOVERED)只会在 Activity 处于前台(onResume()状态)时被系统分发。如果你在onCreate()中注册 Reader Mode,或在onPause()后未及时注销,会导致以下问题:

  • 标签靠近时无响应(Activity 不在前台)
  • 多次onNewIntent()调用导致重复解析
  • 内存泄漏(Reader Callback 持有 Activity 引用)

标准做法是:在onResume()中启用 Reader Mode 或处理 Intent,在onPause()中停用。

@Override protected void onResume() { super.onResume(); // 方式一:使用 Reader Mode(推荐,更可控,支持非 NDEF 标签) if (nfcAdapter != null && nfcAdapter.isEnabled()) { nfcAdapter.enableReaderMode(this, new NfcAdapter.ReaderCallback() { @Override public void onTagDiscovered(Tag tag) { // 在此解析 tag 数据 parseNdefTag(tag); } }, // Reader flags:必须包含 READER_MODE_NFC_A/B/F(取决于标签类型) NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_NFC_B | NfcAdapter.FLAG_READER_NFC_F, null); } // 方式二:处理来自 Intent Filter 的 NDEF_DISCOVERED(兼容旧逻辑) if (getIntent() != null && NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) { processIntent(getIntent()); } } @Override protected void onPause() { super.onPause(); if (nfcAdapter != null) { nfcAdapter.disableReaderMode(this); } }

关键参数说明FLAG_READER_NFC_A对应 MIFARE Classic/ULtralight(最常见),FLAG_READER_NFC_B对应 ISO 14443-4B(如身份证),FLAG_READER_NFC_F对应 FeliCa(日本交通卡)。不要盲目全选——开启不支持的协议会降低发现灵敏度。根据你目标标签类型选择,例如读取普通 NFC Forum 标签(如 NTAG215),只需FLAG_READER_NFC_A

3. 解析 NFC 标签数据:从 Raw Tag 到可读字符串的完整链路

3.1 Reader Mode 下的 Tag 对象结构:理解getId()getTechList()getUid()的区别

onTagDiscovered(Tag tag)被回调,tag对象封装了物理标签的全部信息。新手常混淆三个 ID 相关方法:

方法返回值用途是否唯一
tag.getId()byte[]标签的 UID(Unique Identifier),十六进制字节数组是(同一标签每次相同)
tag.getUid()byte[]getId(),已弃用,但行为一致
tag.getTechList()String[]支持的技术列表,如["android.nfc.tech.NfcA", "android.nfc.tech.Ndef"]否(描述能力)
private void parseNdefTag(Tag tag) { byte[] id = tag.getId(); // 获取 UID,可用于日志或去重 String uidHex = bytesToHex(id); // 工具方法:将 byte[] 转为 0x12345678 格式 Log.d("NFC", "Tag UID: " + uidHex); String[] techs = tag.getTechList(); for (String tech : techs) { Log.d("NFC", "Supported tech: " + tech); } // 关键:检查是否支持 NDEF 技术(绝大多数可读标签都支持) if (Ndef.get(tag) != null) { readNdefMessage(tag); } else { // 不支持 NDEF,可能是纯 UID 标签(如门禁卡),需用其他技术(如 MifareClassic)读取 Toast.makeText(this, "标签不包含 NDEF 数据", Toast.LENGTH_SHORT).show(); } }

注意Ndef.get(tag)返回Ndef实例,表示该标签已格式化为 NDEF 标准。如果返回null,说明标签是原始的、未写入 NDEF 记录的空白卡(如新购 NTAG213),或使用了非 NDEF 协议(如 MIFARE Classic 的 sector-based 存储)。此时无法用NdefMessage解析,需切换至MifareClassicIsoDep技术。

3.2 NDEF 消息解析:从NdefMessageNdefRecord的逐层解包

NDEF(NFC Data Exchange Format)是 NFC 标签的标准数据封装格式。一个NdefMessage包含一个或多个NdefRecord,每个NdefRecord由 TNF(Type Name Format)、TYPE、ID 和 PAYLOAD 组成。最常见的 TNF 是TNF_WELL_KNOWN,TYPE 为"U"(URI 记录)或"T"(Text 记录)。

private void readNdefMessage(Tag tag) { try { Ndef ndef = Ndef.get(tag); ndef.connect(); // 必须 connect 才能读取 NdefMessage ndefMessage = ndef.getNdefMessage(); if (ndefMessage == null) { Toast.makeText(this, "标签为空或未格式化", Toast.LENGTH_SHORT).show(); return; } NdefRecord[] records = ndefMessage.getRecords(); for (NdefRecord record : records) { String payload = parseNdefRecord(record); if (payload != null) { Log.d("NFC", "Payload: " + payload); // 更新 UI 显示 TextView tvResult = findViewById(R.id.tv_result); tvResult.setText(payload); } } } catch (Exception e) { Log.e("NFC", "读取失败", e); Toast.makeText(this, "读取失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } finally { try { if (ndef != null) ndef.close(); } catch (IOException e) { Log.w("NFC", "close failed", e); } } } private String parseNdefRecord(NdefRecord record) { short tnf = record.getTnf(); byte[] type = record.getType(); byte[] payload = record.getPayload(); // TNF_WELL_KNOWN 且 TYPE="U":URI 记录 if (tnf == NdefRecord.TNF_WELL_KNOWN && Arrays.equals(type, NdefRecord.RTD_URI)) { // payload[0] 是 URI 前缀码(0x01 = http://, 0x02 = https://) // payload[1] 开始是实际 URI 字符串 byte prefix = payload[0]; String prefixStr = getUriPrefix(prefix); String uri = new String(payload, 1, payload.length - 1, StandardCharsets.UTF_8); return prefixStr + uri; } // TNF_WELL_KNOWN 且 TYPE="T":Text 记录 if (tnf == NdefRecord.TNF_WELL_KNOWN && Arrays.equals(type, NdefRecord.RTD_TEXT)) { // payload[0] 是状态字节(bit7=0 表示 UTF-8, bit7=1 表示 UTF-16) // payload[1] 是语言长度,payload[2] 开始是语言码,之后是文本 int langLength = payload[1] & 0xFF; int textStart = 2 + langLength; return new String(payload, textStart, payload.length - textStart, StandardCharsets.UTF_8); } return null; // 其他类型(如 Smart Poster)需额外解析 } private String getUriPrefix(byte prefix) { switch (prefix) { case 0x01: return "http://"; case 0x02: return "https://"; case 0x03: return "ftp://"; case 0x04: return "ftps://"; default: return ""; } }

关键细节record.getPayload()返回的byte[]不包含前缀和语言信息,这些元数据都在 payload 的头部字节中。直接new String(payload)会乱码。必须按 NDEF 规范解析头字段。上述代码已覆盖 95% 的商用标签(URL、纯文本),无需引入android-nfc-tools等第三方库。

3.3 错误处理与超时控制:避免 ANR 和空指针的实战技巧

NFC 读取是 I/O 操作,可能因标签距离、干扰、低电量而超时。ndef.connect()默认无超时,若标签异常,线程会阻塞数秒,触发 ANR(Application Not Responding)。必须主动设限:

// 在 readNdefMessage() 中替换 connect() 调用 try { ndef.connect(); // 设置 2 秒超时(单位毫秒) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ndef.setTimeout(2000); } } catch (IOException e) { Log.e("NFC", "连接失败", e); return; }

注意setTimeout()仅在 API 26+ 有效。对于旧版本,需用Handler+postDelayed()实现软超时,并在connect()后立即disconnect()。此外,Ndef.get(tag)可能返回null(标签不支持 NDEF),ndef.getNdefMessage()可能返回null(标签为空),所有getXXX()调用前必须判空,否则NullPointerException必现。

4. 针对 targetSdkVersion ≥ 33 的权限适配:从NFC权限到后台限制的平滑过渡

4.1 Android 13(API 33)起NFC权限不再需要requestPermissions(),但需声明android:exported

自 Android 13 起,android.permission.NFC被归类为normal 权限,安装时自动授予,无需运行时申请。但AndroidManifest.xml中的<activity>若含有 Intent Filter(如NDEF_DISCOVERED),必须显式声明android:exported="true",否则应用安装失败(INSTALL_FAILED_VERIFICATION_FAILURE)。

<activity android:name=".MainActivity" android:exported="true" <!-- 此行必须添加 --> android:launchMode="singleTask"> <intent-filter> <action android:name="android.nfc.action.NDEF_DISCOVERED" /> <category android:name="android.intent.category.DEFAULT" /> <data android:scheme="http" /> </intent-filter> </activity>

提示android:exported="true"表示该 Activity 可被其他应用(包括系统)启动。这是安全设计,确保 NFC 事件能跨进程传递。若你的 Activity 仅用于内部跳转,且不处理外部 Intent,可设为false,但此时NDEF_DISCOVERED将完全失效。

4.2 后台 NFC 读取被彻底禁止:enableReaderMode()必须在前台 Activity 中调用

Android 10(API 29)起,系统禁止应用在后台(onPause()后)持续监听 NFC。这意味着:

  • enableReaderMode()只能在onResume()中调用;
  • disableReaderMode()必须在onPause()中调用;
  • 不存在“服务中监听 NFC”的合法方案。任何尝试在ServiceWorkManager中启用 Reader Mode 的代码,都会在 API 29+ 设备上静默失败或抛出IllegalStateException

验证方式:在onTagDiscovered()中打印isFinishing()isDestroyed(),确保 Activity 未被销毁。

@Override public void onTagDiscovered(Tag tag) { if (isFinishing() || isDestroyed()) { Log.w("NFC", "Activity 已结束,跳过解析"); return; } parseNdefTag(tag); }

4.3NfcAdapterisEnabled()isConnected()区别:为何前者总为 true 而后者常 false

开发者常困惑:nfcAdapter.isEnabled()返回true,但nfcAdapter.isConnected()却返回false。这是因为:

  • isEnabled():仅检查系统 NFC 开关是否打开(Settings 中的状态);
  • isConnected():检查 NFC 控制器硬件是否已成功初始化并与 SoC 通信(底层驱动状态)。

后者为false的典型场景:

  • 设备刚开机,NFC 模块尚未完成自检;
  • MIUI 中“小米钱包”App 被 Force Stop,导致 NFC daemon 退出;
  • 系统资源紧张,NFC HAL 被内核回收。

解决方案:不依赖isConnected(),而是以enableReaderMode()的回调是否触发为最终判断依据。只要onTagDiscovered()被调用,即证明硬件链路畅通。

5. 实战排错:5 类高频失败场景与对应日志定位法

5.1 场景一:NfcAdapter.getDefaultAdapter()返回 null —— 硬件或厂商限制

现象日志线索解决方案
getDefaultAdapter()返回nullLogcat 中无 NFC 相关日志,dumpsys nfc显示NfcService: not running1. 检查设备规格是否标注支持 NFC;2. 尝试重启设备;3. 在 Settings > Connection > NFC 页面确认开关存在(若无此选项,硬件缺失);4. 查阅厂商文档(如华为 EMUI 12 需开启“智能卡”开关)

5.2 场景二:enableReaderMode()无回调 —— Intent Filter 冲突或 Reader Flag 错误

现象日志线索解决方案
标签靠近,但onTagDiscovered()从不触发Logcat 出现NfcService: Ignoring reader mode request from ...1. 确认onResume()enableReaderMode()被执行(加断点);2. 检查FLAG_READER_NFC_A是否与标签类型匹配(用 NFC Tools App 读取标签 Tech List);3. 关闭其他 NFC 应用(如支付宝、微信)——它们可能抢占 Reader Mode

5.3 场景三:Ndef.get(tag)返回 null —— 标签未格式化或协议不匹配

现象日志线索解决方案
onTagDiscovered()被调用,但Ndef.get(tag)nullLogcat 输出Tag: TechList=[android.nfc.tech.MifareClassic]1. 用 NFC Tools 确认标签类型;2. 若为 MIFARE Classic,改用MifareClassic.get(tag)并 authenticate;3. 若为空白卡,需先用NdefFormatable格式化(需标签支持)

5.4 场景四:getNdefMessage()返回 null —— 标签内容为空或损坏

现象日志线索解决方案
Ndef.get(tag)非 null,但getNdefMessage()返回nullLogcat 显示NfcService: NDEF format error1. 用 NFC Tools 写入标准 NDEF 文本记录测试;2. 检查标签是否被写保护(如 NTAG213 的LOCK位);3. 尝试ndef.canMakeReadOnly()判断是否可读

5.5 场景五:onNewIntent()未触发 ——launchMode配置错误

现象日志线索解决方案
点击通知或桌面图标启动 App 后,NFC 标签触发onCreate()而非onNewIntent()Logcat 显示ActivityManager: Start proc ...AndroidManifest.xml中为 Activity 添加android:launchMode="singleTask",并在onNewIntent()中调用setIntent(intent)

终极验证命令:在终端执行adb shell dumpsys nfc,查看mState=NFC_ONmIsEnabled=truemIsConnected=true三项均为true,且mReaderModeEnabled=true,即证明系统级 NFC 已就绪。此命令无需 root,是比代码日志更底层的诊断依据。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询