56、NFC近场通信:NFC标签读写、NDEF格式、前台调度系统
NFC,近场通信,说白了就是碰一碰。你拿手机刷门禁、碰一下支付、或者贴一下标签读取信息,背后都是这套技术在干活。我最早接触NFC是在做一款智能名片项目的时候,当时觉得这玩意儿挺酷的,但真正上手才发现——坑也不少。
今天我们就来聊聊Android上的NFC开发。我会从最基础的标签读写讲起,再到NDEF数据格式,最后说说前台调度系统怎么用。嗯,保证都是实战干货。
NFC的基础概念
NFC工作在13.56MHz频率,通信距离一般在4cm以内。它有三种工作模式:
- 读写模式:手机读取或写入NFC标签
- 卡模拟模式:手机模拟成一张卡片
- 点对点模式:两台设备交换数据
我们这节课主要讲读写模式。你想想看,最常见的场景就是:用户把手机靠近一个NFC标签,然后App读取标签里的数据,做相应的处理。
核心要点:NFC通信是短距离、低带宽的。别指望用它传大文件,传个URL、文本或者小量配置数据就够了。
NDEF数据格式
NDEF,全称NFC Data Exchange Format。它是NFC论坛定义的标准数据格式。说白了,就是规定NFC标签里的数据长什么样。
一个NDEF消息可以包含多个NDEF记录。每个记录有以下几个关键字段:
| 字段 | 说明 |
|---|---|
| TNF | 类型名称格式,比如绝对URI、MIME媒体类型等 |
| Type | 记录类型,比如"U"代表URI,"T"代表文本 |
| ID | 记录标识符,可选 |
| Payload | 实际数据内容 |
常见的NDEF记录类型有:
- 文本记录:TNF=1, Type="T",存一段文本
- URI记录:TNF=1, Type="U",存一个网址
- MIME记录:TNF=2,存任意MIME类型数据
- 外部类型记录:TNF=4,用于自定义应用数据
我的经验:在项目中,我习惯用URI记录来存App的启动链接。用户碰一下标签,直接打开App并跳转到指定页面。比存文本方便多了。
读取NFC标签
读取NFC标签,核心就三步:获取Tag对象、获取对应的TagTechnology、读取NDEF数据。
先看代码:
// 在Activity中处理NFC Intent
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
Parcelable[] rawMessages = intent.getParcelableArrayExtra(
NfcAdapter.EXTRA_NDEF_MESSAGES);
if (rawMessages != null) {
NdefMessage[] messages = new NdefMessage[rawMessages.length];
for (int i = 0; i < rawMessages.length; i++) {
messages[i] = (NdefMessage) rawMessages[i];
}
// 处理NDEF消息
processNdefMessages(messages);
}
}
}
private void processNdefMessages(NdefMessage[] messages) {
for (NdefMessage msg : messages) {
NdefRecord[] records = msg.getRecords();
for (NdefRecord record : records) {
String text = parseTextRecord(record);
if (text != null) {
// 显示读取到的文本
textView.setText(text);
}
}
}
}
private String parseTextRecord(NdefRecord record) {
// 判断是否为文本记录
if (record.getTnf() == NdefRecord.TNF_WELL_KNOWN &&
Arrays.equals(record.getType(), NdefRecord.RTD_TEXT)) {
try {
byte[] payload = record.getPayload();
// 第一个字节是状态字节
String textEncoding = ((payload[0] & 0x80) == 0) ? "UTF-8" : "UTF-16";
int languageCodeLength = payload[0] & 0x3F;
return new String(payload, languageCodeLength + 1,
payload.length - languageCodeLength - 1, textEncoding);
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
这段代码看着简单,但我得提醒你:onNewIntent才是处理NFC Intent的正确地方。别在onCreate里搞,因为App可能已经在后台运行了。
我曾经踩过的坑:有一次我忘了在Manifest里声明NFC权限,结果标签死活读不出来。折腾了半天才发现是权限问题。记住,<uses-permission android:name="android.permission.NFC" />必须加。
写入NFC标签
写入标签比读取稍微复杂一点。你需要先连接到标签,然后写入NDEF消息。
private void writeNfcTag(Tag tag, String text) {
Ndef ndef = Ndef.get(tag);
if (ndef != null) {
try {
ndef.connect();
NdefRecord textRecord = createTextRecord(text, "en");
NdefMessage message = new NdefMessage(new NdefRecord[]{textRecord});
ndef.writeNdefMessage(message);
Toast.makeText(this, "写入成功", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "写入失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
} finally {
try {
ndef.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private NdefRecord createTextRecord(String text, String locale) {
byte[] langBytes = locale.getBytes(StandardCharsets.US_ASCII);
byte[] textBytes = text.getBytes(StandardCharsets.UTF_8);
byte[] payload = new byte[1 + langBytes.length + textBytes.length];
payload[0] = (byte) langBytes.length;
System.arraycopy(langBytes, 0, payload, 1, langBytes.length);
System.arraycopy(textBytes, 0, payload, 1 + langBytes.length, textBytes.length);
return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT,
new byte[0], payload);
}
这里有个细节:写入前一定要检查标签是否可写。有些标签出厂就被锁定了,写不进去。我建议在写入前先调用ndef.isWritable()检查一下。
前台调度系统
前台调度系统,英文叫Foreground Dispatch System。它是Android提供的一套机制,让你的App在前台运行时优先处理NFC事件。
为什么要用这个?因为默认情况下,NFC Intent会走系统的分发流程。如果你的App在后台,系统可能会弹出选择器让用户选。前台调度系统可以绕过这个,直接让你的App处理。
实现起来也不复杂:
// 在onCreate中注册
private NfcAdapter nfcAdapter;
private PendingIntent pendingIntent;
private IntentFilter[] intentFilters;
private String[][] techLists;
@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_SHORT).show();
finish();
return;
}
// 创建PendingIntent
pendingIntent = PendingIntent.getActivity(
this, 0,
new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
PendingIntent.FLAG_IMMUTABLE);
// 设置IntentFilter
IntentFilter ndefFilter = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
try {
ndefFilter.addDataType("*/*");
} catch (IntentFilter.MalformedMimeTypeException e) {
e.printStackTrace();
}
intentFilters = new IntentFilter[]{ndefFilter};
// 设置技术列表
techLists = new String[][]{
new String[]{Ndef.class.getName()},
new String[]{NdefFormatable.class.getName()}
};
}
@Override
protected void onResume() {
super.onResume();
if (nfcAdapter != null) {
nfcAdapter.enableForegroundDispatch(
this, pendingIntent, intentFilters, techLists);
}
}
@Override
protected void onPause() {
super.onPause();
if (nfcAdapter != null) {
nfcAdapter.disableForegroundDispatch(this);
}
}
你看,核心就是enableForegroundDispatch和disableForegroundDispatch这对方法。在onResume里启用,在onPause里禁用。这样你的App在前台时,NFC事件会直接走你的PendingIntent。
我个人的习惯:在onResume里先检查NFC是否开启。如果没开,弹个对话框引导用户去设置。用户体验会好很多。
知识体系总览
下面这张图,我把NFC开发的核心知识点串起来了。你可以对照着看,心里有个谱。
避坑指南
最后,我把自己在项目中踩过的坑整理一下,你遇到了可以直接翻出来看:
- 标签类型不兼容:不是所有标签都支持NDEF格式。有些老标签只支持Mifare Classic。写入前先检查
Ndef.get(tag)是否为null。 - 写入速度慢:NFC写入是串行的,别在主线程里搞。我建议用AsyncTask或者协程。
- 标签被锁定:有些标签出厂就写保护了。写入前调用
ndef.isWritable()检查。 - 前台调度没生效:检查一下onPause里有没有调用
disableForegroundDispatch。如果忘了,下次onResume注册会失败。 - Android 12以上PendingIntent:记得加
FLAG_IMMUTABLE,否则会崩溃。
我曾经遇到的一个奇葩问题:有一次在华为手机上,NFC死活读不出来。后来发现是手机壳太厚了。嗯,金属手机壳会屏蔽NFC信号。如果你的用户反馈读不到,先问问他们是不是带了金属壳。
好了,NFC这块的内容就这些。从标签读写到NDEF格式,再到前台调度系统,你照着代码写一遍,基本就能上手了。记住,NFC开发的核心就是Intent分发和TagTechnology操作,把这两块搞明白,剩下的就是细节了。
公众号:蓝海资料掘金营,微信deep3321