ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Android NFC读取完整指南:从权限配置到NDEF解析

Android NFC读取完整指南:从权限配置到NDEF解析 简介本资源是一份面向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:nameandroid.permission.NFC /就能立刻调用NfcAdapter.getDefaultAdapter()成功获取实例——结果null或者enableReaderMode()报SecurityException又或者前台 Activity 死活收不到NDEF_DISCOVEREDIntent。根本原因在于NFC 在 Android 中不是“即开即用”的传感器而是一套需严格匹配硬件能力、系统状态、Activity 生命周期和 Intent 过滤规则的事件驱动机制。它要求你同时满足四层条件设备支持 NFC 硬件且未被厂商禁用、用户已手动开启 NFC 开关、应用拥有NFC权限非危险权限但需显式声明、Activity 正处于前台并已注册正确的 Intent Filter 或 Reader Mode 回调。本文面向 Android 8.0API 26及以上主流版本覆盖从targetSdkVersion33的权限适配到NfcAdapter与Ndef解析的完整链路不依赖任何第三方 SDK所有代码均可在 Android Studio Flamingo 及以上版本直接编译运行。2. 从 Manifest 声明到 Activity 生命周期NFC 读取的四大必要条件缺一不可2.1 Manifest 中必须声明的三类配置权限、功能、Intent Filter仅声明uses-permission android:nameandroid.permission.NFC /是远远不够的。Android 要求你明确告知系统你的应用不仅需要 NFC 权限还实际使用 NFC 功能并且希望接收特定类型的 NFC 标签事件。这三者必须同时存在否则系统不会将 NFC 事件路由给你的 Activity。!-- 1. NFC 权限非危险权限但仍需声明 -- uses-permission android:nameandroid.permission.NFC / !-- 2. 声明 NFC 硬件功能为“非必需”避免 Google Play 过滤掉无 NFC 设备 -- uses-feature android:nameandroid.hardware.nfc android:requiredfalse / !-- 3. Intent Filter用于 NDEF_DISCOVERED 场景最常用 -- intent-filter action android:nameandroid.nfc.action.NDEF_DISCOVERED / category android:nameandroid.intent.category.DEFAULT / !-- 必须指定 data scheme 或 mime-type否则会被系统忽略 -- data android:schemehttp / !-- 或者data android:mimeTypetext/plain / -- /intent-filter提示data标签是硬性要求。如果你不写data即使 NFC 开关打开、标签靠近系统也不会触发你的 Activity。这是因为 Android 为防止恶意应用劫持所有 NFC 事件强制要求 Intent Filter 必须精确匹配标签内容类型。常见组合包括schemehttp对应 URL 类型 NDEF 记录、mimeTypetext/plain纯文本、schemehttps或更严格的hostexample.compathPrefix/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[]标签的 UIDUnique 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解析需切换至MifareClassic或IsoDep技术。3.2 NDEF 消息解析从NdefMessage到NdefRecord的逐层解包NDEFNFC Data Exchange Format是 NFC 标签的标准数据封装格式。一个NdefMessage包含一个或多个NdefRecord每个NdefRecord由 TNFType Name Format、TYPE、ID 和 PAYLOAD 组成。最常见的 TNF 是TNF_WELL_KNOWNTYPE 为UURI 记录或TText 记录。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 且 TYPEUURI 记录 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 且 TYPETText 记录 if (tnf NdefRecord.TNF_WELL_KNOWN Arrays.equals(type, NdefRecord.RTD_TEXT)) { // payload[0] 是状态字节bit70 表示 UTF-8, bit71 表示 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()默认无超时若标签异常线程会阻塞数秒触发 ANRApplication 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 有效。对于旧版本需用HandlerpostDelayed()实现软超时并在connect()后立即disconnect()。此外Ndef.get(tag)可能返回null标签不支持 NDEFndef.getNdefMessage()可能返回null标签为空所有getXXX()调用前必须判空否则NullPointerException必现。4. 针对 targetSdkVersion ≥ 33 的权限适配从NFC权限到后台限制的平滑过渡4.1 Android 13API 33起NFC权限不再需要requestPermissions()但需声明android:exported自 Android 13 起android.permission.NFC被归类为normal 权限安装时自动授予无需运行时申请。但AndroidManifest.xml中的activity若含有 Intent Filter如NDEF_DISCOVERED必须显式声明android:exportedtrue否则应用安装失败INSTALL_FAILED_VERIFICATION_FAILURE。activity android:name.MainActivity android:exportedtrue !-- 此行必须添加 -- android:launchModesingleTask intent-filter action android:nameandroid.nfc.action.NDEF_DISCOVERED / category android:nameandroid.intent.category.DEFAULT / data android:schemehttp / /intent-filter /activity提示android:exportedtrue表示该 Activity 可被其他应用包括系统启动。这是安全设计确保 NFC 事件能跨进程传递。若你的 Activity 仅用于内部跳转且不处理外部 Intent可设为false但此时NDEF_DISCOVERED将完全失效。4.2 后台 NFC 读取被彻底禁止enableReaderMode()必须在前台 Activity 中调用Android 10API 29起系统禁止应用在后台onPause()后持续监听 NFC。这意味着enableReaderMode()只能在onResume()中调用disableReaderMode()必须在onPause()中调用不存在“服务中监听 NFC”的合法方案。任何尝试在Service或WorkManager中启用 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.3NfcAdapter的isEnabled()与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. 检查设备规格是否标注支持 NFC2. 尝试重启设备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 List3. 关闭其他 NFC 应用如支付宝、微信——它们可能抢占 Reader Mode5.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)并 authenticate3. 若为空白卡需先用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:launchModesingleTask并在onNewIntent()中调用setIntent(intent)终极验证命令在终端执行adb shell dumpsys nfc查看mStateNFC_ON、mIsEnabledtrue、mIsConnectedtrue三项均为true且mReaderModeEnabledtrue即证明系统级 NFC 已就绪。此命令无需 root是比代码日志更底层的诊断依据。本文还有配套的精品资源点击获取
返回列表