恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Android读写Ntag21x:从NfcA到JNI So库的完整实现指南

  • 首页
  • 资讯中心
  • /
  • Android读写Ntag21x:从NfcA到JNI So库的完整实现指南

相关资讯

猫抓 cat-catch:网页视频提取与 M3U8 下载,资源嗅探一键搞定 2026/9/13 14:22:04
Proteus仿真51单片机步进电机控制:从原理到虚拟示波器验证 2026/9/13 14:22:04
3分钟装好第一个语言扩展:Zed 扩展系统实战指南 2026/9/13 14:22:04

最新资讯

如何用 LangChain 把 PDF、网页与 YouTube 音频加载为 Document 对象
OpenClaw部署成本拆解:从几十块到几百块的智能体落地指南
Tube-MPC源码解析:从扰动不变集到约束收紧的鲁棒预测控制实践
Mastra 工作记忆(Working Memory)机制深度解析:Agent 如何跨对话记住用户
多尺度形态学在眼前节组织分割中的应用与优化
Data Formulator 数据库 Docker 集成测试环境:docker-compose 统一编排与 test-dbs.ps1 实战指南

今日推荐

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验
Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化
Flutter应用改名全指南:从Android到iOS的配置与工具实践

本周热门

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验
Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化
Flutter应用改名全指南:从Android到iOS的配置与工具实践

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

Android读写Ntag21x:从NfcA到JNI So库的完整实现指南

发布时间:2026/9/13 14:22:04
Android读写Ntag21x:从NfcA到JNI So库的完整实现指南 简介面向Android NFC开发者的Ntag21x芯片读写示例工程适合需要对接NXP Ntag21x系列标签、理解So库调用与JNI机制的移动端工程师对底层NFC协议不熟悉的开发者尤其友好。压缩包共471个文件、8.85MB包含16个so动态库、4个Java源码、6个txt说明文件以及大量flat/json/xml等构建配置与中间资源整体是一个结构完整的Android工程便于直接导入分析。已有五百一十四人学习具有一定的实践参考性。示例围绕Android NFC基础能力展开涉及NfcAdapter与Tag对象的获取、System.loadLibrary加载本地库、JNI接口声明与C/C实现针对Ntag21x芯片的I2C与ISO 14443A协议可看到初始化连接、读取特定区域、写入数据以及异常处理等代码路径也包含权限申请和So库调用前的准备逻辑并延伸涉及P2P、卡模拟等NFC模式的So库调用思路。适合在移动支付、数据交换、智能标签等物联网场景下帮助开发者快速搭建NFC读写能力少走弯路。1. 为什么 Android 读 Ntag21x 不能只靠 NfcAdapter做 NFC 读写最容易被卡住的一步是拿着NfcAdapter从Intent里取出Tag对象后发现只能读到 ID却读不出标签里的业务数据。Ntag21x 这类芯片本身基于 ISO 14443AAndroid 系统只保证把底层NfcA或IsoDep技术实例交给你至于怎么把一个 4 字节的READ命令发给标签、怎么解析 ACK官方 API 并不会替你封装。示例源码把 NXP 的这套指令集封装进了 So 库通过 JNI 暴露给 Java正好补上这一层。适合正在做门禁、智能海报或设备配对的 Android 开发者尤其是需要同时处理多种 Ntag21x 变体、又不想从零啃协议的人。2. Ntag21x 芯片与 Android NFC 技术栈从协议到 Tag 对象2.1 Ntag21x 系列特性I²C、ISO 14443A 与存储结构Ntag21x 是 NXP 推出的 NTAG 系列中的一个分支常见型号包括 Ntag213、Ntag215 和 Ntag216。它们都符合 ISO 14443A工作频率 13.56MHz这也是 Android NFC 默认支持的主要协议。这几个型号最大的差异在于用户内存容量Ntag213 有 144 字节用户可用空间Ntag215 是 504 字节Ntag216 则能达到 888 字节。所谓“用户可用空间”并不是从页 0x00 开始一路畅通而是要从页 0x04 的序列号之后开始算起标签头部的厂商信息、序列号、锁定位和配置字占用了前几页。实际工程里我更关心的是它的页寻址机制。Ntag21x 每个页固定 4 字节读写都以页为单位。写入时一次能写 4 字节读时可以一次连续读多页但 NXP 官方协议要求读取命令一次最多返回 16 字节4 页。如果你用transceive发送READ命令时指定地址为 0x30返回的就是页 0x30 到 0x33 的 16 字节原始数据。这种结构对 So 库的设计影响很大底层 C 代码需要维护一个“页偏移 缓存”的状态机而不是像普通文件 I/O 那样直接按字节流处理。辅助供电也是一个容易忽略的点。Ntag21x 和普通的 Ntag213 不同部分型号如 Ntag21x 系列中的带 I²C 版本支持通过 I²C 接口与 MCU 通信在无 RF 场时也能读写。但在 Android 示例中我们通常只走 NFC 通道I²C 只是芯片多出来的一条旁路。真正要移植到生产环境时你必须在代码里区分当前是通过NfcA还是IsoDep建立的连接因为不同连接拿到的传输层句柄不同直接混用会让 So 库内部的状态指针失效。2.2 Android NFC API 核心NfcAdapter 与 Tag 对象的获取Android 的 NFC 架构是围绕NfcAdapter和Tag两个类展开的。系统在检测到 NFC 标签时会创建一个IntentActivity 通过NfcAdapter.ACTION_TAG_DISCOVERED、ACTION_NDEF_DISCOVERED或ACTION_TECH_DISCOVERED来接收。其中ACTION_TECH_DISCOVERED最精确因为它的 intent filter 可以指定tech-list比如android.nfc.tech.NfcA这样只有支持 NfcA 的标签才会触发你的代码。拿到Intent后通过NfcAdapter.getTag()拿到Tag对象。Tag对象不是一个可以用来收发数据的句柄而是一个描述标签技术类型的容器。真正能用来交换数据的是Tag.getTechList()返回的技术实例例如NfcA、NfcB、IsoDep、Ndef。对于非 NDEF 格式的 Ntag21x 标签最常用的是NfcA因为Ntag21x不会像 Ndef 那样自动把 NDEF 封装信息暴露出来需要你自己发原生命令。代码上常见做法是这样的NfcAdapter adapter NfcAdapter.getDefaultAdapter(context); if (adapter null) { /* 设备不支持 NFC */ } PendingIntent pendingIntent PendingIntent.getActivity(this, 0, new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), PendingIntent.FLAG_MUTABLE); adapter.enableForegroundDispatch(this, pendingIntent, new IntentFilter[]{ new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED) }, new String[][]{ new String[]{ NfcA.class.getName() } });这段代码里enableForegroundDispatch让当前 Activity 优先接收 NFC 事件而不是弹出系统标签选择器。PendingIntent.FLAG_MUTABLE在 Android 12 是必须的否则会抛SecurityException。NfcA.class.getName()表示只希望捕获支持 NfcA 技术的标签。注意这里并没有限制具体芯片型号所以 Ntag21x、MIFARE Classic、甚至其他 ISO 14443A 卡片都会进入这个回调真正的区分要在onNewIntent里通过芯片应答的 SAK 或 ATQA 来判断。2.3 为什么需要 So 库JNI 的边界与性能很多人会问Java 层直接用NfcA收发命令不行吗行但有两个问题。第一NFC 的transceive每次调用都有系统层开销特别是在并发场景下若每个 page 读都从 Java 层发起线程切换和 GC 会拖慢整体节奏。第二NXP 的 Ntag21x 并非只有简单的读写命令它还包含配置扇区的锁定、NFC counter 功能、以及原厂签名验证这些命令往往带 CRC 和特殊状态机处理Java 层写起来啰嗦且易错。放进 So 库后C 代码可以直接复用 NXP 官方 LibNFC 的逻辑或者自行实现紧凑的状态机Java 层只保留一个薄的封装。JNI 的典型分工是Java 声明 native 方法C 实现后编译成 .so运行时通过System.loadLibrary(ntag21x)加载。一个常见的设计是让 native 方法持有一个 long 类型的指针指向 C 层创建的上下文结构体public class Ntag21xNative { static { System.loadLibrary(ntag21x); } public static native long nativeConnect(NfcA nfcA); public static native int nativeReadPage(long handle, int page, byte[] data); public static native int nativeWritePage(long handle, int page, byte[] data); public static native void nativeClose(long handle); }nativeConnect负责把NfcA对象的内部句柄传给 C 层C 层再调用NfcA对应的 JNI 函数执行连接。这里有个容易被忽略的细节C 层不能直接保存NfcA的 Java 引用因为跨线程访问会导致 JNI 全局引用泄漏。正确做法是在nativeConnect里通过GetObjectField拿到NfcA内部mHandle把它存成int或long之后所有读写都靠这个底层句柄不再碰 Java 对象。3. 示例工程结构与 So 库加载路径3.1 解压源码包后先认识这几个目录拿到Ntag21x芯片读写Android示例源码.rar解压后通常能看到一个标准的 Android Studio 工程结构。app/src/main/java下会有入口 Activity 和 JNI 封装类app/src/main/jni或cpp目录下是 C/C 源码libs或jniLibs下则放着编译好的libntag21x.so。这个 So 库一般按 ABI 拆成armeabi-v7a、arm64-v8a、x86三个目录如果你只在真机上跑x86可以删掉但模拟器调式时需要保留。关键文件里NfcActivity.java负责处理 NFC 意图和权限Ntag21xManager.java封装了各页读写的业务逻辑nfc_ntag21x.c实现了命令帧的拼装与 CRC。还有一个容易被忽略的ndkBuild或CMakeLists.txt。如果工程是早期的jni目录它会用ndk-build如果是在cpp目录通常用CMake。两者都行但我们在 Android Studio 新版本里更推荐 CMake因为增量编译和调试都更方便。3.2 System.loadLibrary 与 JNI 函数映射一个 native 方法怎么落到 C先看 Java 侧的声明public class Ntag21xJni { static { System.loadLibrary(ntag21x); // 加载 libntag21x.so } public static native int Ntag21x_ReadPage(int handle, byte[] pageData); }C 侧的实现要遵循 JNI 命名规范Java_包名_类名_方法名。如果包名是com.example.ntag,类名是Ntag21xJni那么函数名就是Java_com_example_ntag_Ntag21xJni_Ntag21x_1ReadPage。这里的_1是 JNI 对 Java 方法名中下划线的转义。很多初学者在这里栽跟头看到 UnsatisfiedLinkError 却找不到原因其实只要用javah生成头文件就能避免。System.loadLibrary(ntag21x)会在应用的 native library 搜索路径里找libntag21x.so。在 Android Studio 里默认搜索路径是jniLibs/abi/libntag21x.so或者由 CMake 构建产物自动打包进 APK。如果你把 .so 放在libs目录必须在build.gradle的sourceSets里指定android { sourceSets { main { jniLibs.srcDirs [libs] } } }不加这行系统会报Couldnt load ntag21x。用 CMake 构建的话则需要把 .so 的输出路径指到jniLibs或者在app/src/main/cpp下直接编译源码最终仍然会被打包到 APK 的 lib 目录。3.3 Android Studio 集成 NDK 时的常见坑在 Android Studio 里打开这个示例工程时如果之前没装过 NDKIDE 会提示Failed to find NDK。不要急着点下载先确认项目是否真的需要从源码编译 C 代码。如果只是引用预编译 .so只需要在local.properties或ANDROID_NDK_HOME里配置一个匹配的 NDK 版本即可。如果项目包含jni目录并且build.gradle里有externalNativeBuild { ndkBuild { path src/main/jni/Android.mk } }那就必须安装 NDK 和 CMake。这里的常见问题是 ABI 不匹配。现在的手机普遍是arm64-v8a而一些旧 So 库只有armeabi-v7a。系统在安装 APK 时会自动选择兼容的 ABI不会出错。但如果你的 So 库同时缺失两个目录就会在运行时抛UnsatisfiedLinkError。我在接入这种老示例时通常会检查jniLibs里的文件目录再用Build.SUPPORTED_ABIS输出设备支持的列表对比一下。android { defaultConfig { ndk { abiFilters armeabi-v7a, arm64-v8a } } }abiFilters的作用是只打包指定 ABI避免把 x86 也塞进 APK 导致包体膨胀。不过要注意如果你在模拟器上调试x86是不能随便删的除非你用的是 ARM 架构的模拟器镜像。4. 核心读写流程从 Tag 到 Ntag21x 命令4.1 建立连接通过 NfcA 实例与标签完成握手先用NfcA.get(tag)拿到技术实例然后调用connect()。connect()不是简单地建立一个 Socket它会执行 ISO 14443A 的防碰撞和选择流程成功后transceive才能正常收发命令。示例中一般把connect()放在子线程因为 NFC 栈对超时很敏感主线程上做几十毫秒的 I/O 同样会触发NetworkOnMainThreadException这类保护更严重的是会让系统 NFC 服务觉得当前进程无响应。NfcA nfcA NfcA.get(tag); if (nfcA null) { return; } nfcA.connect(); byte[] cmd new byte[]{ (byte)0x30, 0x04 }; // READ 页 0x04 byte[] resp nfcA.transceive(cmd); nfcA.close();这段代码的0x30是 Ntag21x 的读取命令码第二个字节是页地址。页 0x04 开始是用户内存的第一页前 4 字节是序列号的一部分。但要注意transceive返回的数据前面还有一个长度字节这是 NFC 栈自动添加的实际的有效载荷要去掉第一个字节。我第一次调试时直接拿返回数组转十六进制结果多出了一个0x10浪费了半天时间。4.2 读取用户内存区多页读取与数据解析对于 Ntag21x单次最多读取 4 页也就是 16 字节。要读 Ntag216 的全部 888 字节用户空间需要循环发 222 次READ命令。示例 So 库通常会提供一个大块读取函数内部用循环或 burst 模式处理。但模拟题里更常见的是单页读让 Java 层传入页号和输出 BufferC 层负责构造命令和处理 ACK。推荐实现一个公共工具方法专门处理“读 16 字节”的完整事务public byte[] readPages(NfcA nfcA, int startPage) throws IOException { byte[] cmd new byte[]{ (byte)0x30, (byte)startPage }; byte[] resp nfcA.transceive(cmd); if (resp null || resp.length 17) { throw new IOException(response length invalid: (resp null ? null : resp.length)); } byte[] data new byte[16]; System.arraycopy(resp, 1, data, 0, 16); return data; }第 0 个字节是协议层状态位不是标签数据。resp.length 17的检查很重要因为 Ntag21x 在非法页地址时会返回 NAK0x00 或 0x04长度只有 1 到 2 字节。如果你不做长度保护ArrayIndexOutOfBoundsException就会在你调试到一半时突然冒出来。读取时还要注意Ntag213 只有页 0x04 到 0x43 共 64 页用户空间Ntag216 则有 236 页读越界会直接 NAK。4.3 写入操WRITE 命令、ACK 与容错写入 Ntag21x 用户页用WRITE命令命令码是0xA2。每条命令带一个页地址和 4 字节数据。写入后标签会返回 ACK0x0A失败则返回 NAK。ACK 的长度是 1NAK 的长度也是 1但值不同。所以判成功不能只看长度还要看内容。public boolean writePage(NfcA nfcA, int page, byte[] data) throws IOException { if (data.length ! 4) { throw new IllegalArgumentException(Ntag21x write requires 4 bytes per page); } byte[] cmd new byte[]{ (byte)0xA2, (byte)page, data[0], data[1], data[2], data[3] }; byte[] resp nfcA.transceive(cmd); return resp ! null resp.length 1 (resp[0] 0xFF) 0x0A; }这里有个工程细节Ntag21x 的写操作不是原子的当页地址在配置区如页 0xE8 之后时可能会因长度字段校验失败而拒绝。更头疼的是如果上一次写入的 NAK 处理不当后续连续写入会全部失败。我的建议是在写完后追加一次读回验证读到数据与写入数据逐字节比较相等才认为成功。示例 So 库一般没有自动验证你在上层业务里加一段 3 次重试逻辑会实用很多。5. 实战封装 Ntag21xManager 并集成到 Android 项目5.1 设计一个可复用的 Ntag21xManager把 native 读写函数和 Java 层 NfcA 连接逻辑组合起来封装成Ntag21xManager可以避免业务代码与协议细节耦合。这个类应该持有当前NfcA实例并且把读页、写页、连页封装成同步方法。public class Ntag21xManager { private NfcA nfcA; public boolean connect(Tag tag) { try { nfcA NfcA.get(tag); nfcA.connect(); return true; } catch (IOException e) { return false; } } public byte[] readPage(int page) throws IOException { return Ntag21xJni.Ntag21x_ReadPage(nfcA, page); } public boolean writePage(int page, byte[] data) throws IOException { return Ntag21xJni.Ntag21x_WritePage(nfcA, page, data); } public void close() { try { if (nfcA ! null) nfcA.close(); } catch (IOException ignored) { } } }在 Java 层调 So 库函数时nfcA参数在这种情况下其实并不直接传给 C而是通过Ntag21x_ReadPage内部调用 JNI 的NfcA句柄。示例源码里Ntag21x_ReadPage的第二个参数是nfcA对象本身C 层通过GetObjectField取出mHandle。这种设计可以让你在 Java 层不感知底层句柄但代价是每次调用都要重新解析 JNI 对象性能略差。更优的方案是在connect时就把句柄转成 long 存起来之后只传 long。5.2 前台调度、权限与 Android 版兼容NFC 权限只需要在 Manifest 里声明android.permission.NFC但这个权限是普通权限不需要运行时动态申请。真正影响体验的是前台调度如果不调用enableForegroundDispatch扫码后系统可能会弹出一个选择器让用户选择用哪个应用打开这对工具型 App 来说不可接受。配置项内容说明intent filterandroid.nfc.tech.NfcA只响应 NfcA 技术PendingIntent flagFLAG_MUTABLEAndroid 12 必需回调入口onNewIntent(Intent)获取并处理 tag页读写线程Executors.newSingleThreadExecutor避免阻塞 UI在onResume里启用前台调度onPause里禁用。如果不按这个生命周期走Activity 退到后台时仍然会抢占 NFC 事件导致系统原生“快速标签”工具无法工作。Android 13 及以上没有额外行为变化但如果你适配到 Android 14需要确认PendingIntent的可见性声明否则可能崩溃。5.3 错误处理TagLostException 与 So 库加载异常NFC 通信和网络不同不会给你优雅的 RST 响应更多的是TagLostException——标签被移走、信号弱、或者被另一个 NFC 应用抢占。这个异常必须在transceive调用点捕获不能让它冲到 UI 层。try { byte[] data manager.readPage(0x05); } catch (TagLostException e) { // 重试或提示用户重新贴卡 retryCount; if (retryCount 3) { showMessage(请重新贴近标签); } } catch (IOException e) { // 关闭连接并重新初始化 manager.close(); }另一个典型异常是UnsatisfiedLinkError通常发生在 So 库与系统 ABI 不匹配时。遇到这个错误先用adb shell getprop ro.product.cpu.abi查看设备 ABI再检查 APK 内lib/arm64-v8a是否存在该 .so。示例源码里的 So 库如果只提供了 32 位版本就必须在abiFilters里把arm64-v8a排除掉或者重新编译 64 位版本。6. 验证读写结果与降低 NFC 延迟的实用技巧6.1 用日志和第三方 TagInfo 应用交叉验证拿到读出的数据后先用十六进制打印再用支持 Ntag21x 的第三方 App如 NXP TagInfo扫同一标签比对页 0x04 到 0x07 的内容。如果两者一致说明协议解析正确如果不同优先检查是否把协议层长度字节误当成数据。在 Logcat 里输出时用HexDump或简单循环拼接都可但记得去掉应变删的resp[0]。我常用一行Log.d(TAG, page page hex bytesToHex(data))把 page 和 hex 放一起方便在长日志里检索。6.2 降低读写延迟复用连接与调整超时NFCtransceive的默认超时在部分机型上比较长标签信号不稳定时一次失败会卡住 200ms 以上。可以在connect()后调用nfcA.setTimeout(100)把超时缩到 100ms。这样快速扫码场景下即使标签未贴稳也能很快抛出异常并进入重试而不是长时间卡死。连接本身不建议每次都关连续读写时保持同一个NfcA连接可以减少重新防碰撞的时间。等整个页面操作完成后再close()。6.3 避免在主线程做 NFC 事务注意页地址边界最后提醒两个高频坑第一transceive必须放在子线程否则会触发NetworkOnMainThreadException或让系统 NFC 进程崩溃。用协程或线程池都行但一定要确保同一个标签的连接在同一线程内操作跨线程传递NfcA对象会导致内部句柄失效。第二写页时对0x00到0x03的厂商区没有写权限某些型号的配置区如页 0xE8需要先解锁才能写入。我看到很多人在调试时把页 0x00 当作用户数据入口写上就报 NAK其实只要从页 0x04 开始规划数据布局问题就解决了。验证读写是否成功最终标准是读回一致。对于 Ntag215推荐把产品序列号写在页 0x10 0x13把状态标志放在页 0x14每次写完立即读回并校验。这个操作顺序能让你在开发阶段就区分出“数据没写进去”和“读错页”两种问题比盲调 So 库命令高效得多。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号