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);
    }
}

你看,核心就是enableForegroundDispatchdisableForegroundDispatch这对方法。在onResume里启用,在onPause里禁用。这样你的App在前台时,NFC事件会直接走你的PendingIntent。

我个人的习惯:在onResume里先检查NFC是否开启。如果没开,弹个对话框引导用户去设置。用户体验会好很多。

知识体系总览

下面这张图,我把NFC开发的核心知识点串起来了。你可以对照着看,心里有个谱。

NFC 近场通信知识体系 读写模式 卡模拟模式 点对点模式 读取 NFC 标签 写入 NFC 标签 NDEF 数据格式 文本记录 URI 记录 前台调度系统 enableForegroundDispatch IntentFilter + TechList 核心:Intent 分发 + TagTechnology 操作

避坑指南

最后,我把自己在项目中踩过的坑整理一下,你遇到了可以直接翻出来看:

  • 标签类型不兼容:不是所有标签都支持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