27、权限与NFC:NFC权限、NFC读写权限、NFC前台分发

NFC 这个功能,说实话,在 Android 开发里一直是个「小众但关键」的存在。我做安全审计时,经常发现大家对 NFC 权限的理解停留在「加个 uses-feature 就完事」的阶段。但实际坑不少,尤其是 Android 10 之后,NFC 的权限模型和前台分发机制都变了。

今天我们就把它彻底讲透。

NFC 权限:你需要的其实不多

先看最基础的。NFC 权限声明很简单,就一个:

<uses-permission android:name="android.permission.NFC" />

嗯,就这一个。但注意,它属于 normal 级别权限,系统自动授予,不需要运行时申请。所以很多开发者觉得「那不就完了?」——其实不是。

真正容易出问题的是 <uses-feature> 声明。如果你在 Google Play 上架应用,这个声明会决定你的应用是否对没有 NFC 硬件的设备可见。

<uses-feature android:name="android.hardware.nfc" android:required="true" />

我个人习惯是:如果 NFC 只是辅助功能(比如扫码后顺便读个标签),我会设成 required="false"。这样没有 NFC 的手机也能下载,只是功能受限而已。

⚠️ 注意: 如果你声明了 required="true",但代码里没有做运行时检测,用户在没有 NFC 的设备上安装后直接崩溃——这种问题我在项目里见过不止一次。

NFC 读写权限:别被名字骗了

很多人以为「读写权限」需要额外声明。其实 Android 并没有单独的 NFC 读权限或写权限。你只需要 android.permission.NFC 这一个就够了。

但为什么我还是要单独讲「读写权限」?因为真正的限制不在权限声明里,而在 Intent Filter 和前台分发 的配置上。

举个例子。你想让应用在后台也能读到 NFC 标签?对不起,Android 10 之后,后台 NFC 读取被严格限制。你必须让 Activity 处于前台,或者使用前台服务。

我曾经遇到一个项目:客户要求「手机锁屏时靠近 NFC 标签自动打卡」。我直接告诉他,这个需求在原生 Android 上做不到,除非用厂商定制接口。为什么?因为 NFC 的前台分发机制决定了——只有前台 Activity 才能处理 Tag 发现事件。

NFC 前台分发:核心机制

前台分发(Foreground Dispatch)是 NFC 开发里最重要的概念。说白了就是:让你的 Activity 优先处理 NFC 标签,而不是让系统去匹配 Intent Filter

为什么要用前台分发?因为 Intent Filter 匹配太死板了。比如你只想处理特定类型的 NDEF 格式标签,但系统可能把其他标签也分发给你。前台分发让你可以动态控制。

实现步骤很简单,就三步:

  1. 创建 PendingIntent,用于系统回调
  2. 声明你要拦截的 Intent Filter 数组
  3. onResume() 中启用,在 onPause() 中禁用

看代码:

private NfcAdapter nfcAdapter;
private PendingIntent pendingIntent;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    nfcAdapter = NfcAdapter.getDefaultAdapter(this);
    // 创建 PendingIntent,系统用它来启动当前 Activity
    pendingIntent = PendingIntent.getActivity(
        this, 0,
        new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
        PendingIntent.FLAG_IMMUTABLE
    );
}

@Override
protected void onResume() {
    super.onResume();
    if (nfcAdapter != null) {
        nfcAdapter.enableForegroundDispatch(
            this, pendingIntent,
            null,  // IntentFilter 数组,传 null 表示接收所有 Tag
            null   // TechList 数组,传 null 表示接收所有技术类型
        );
    }
}

@Override
protected void onPause() {
    super.onPause();
    if (nfcAdapter != null) {
        nfcAdapter.disableForegroundDispatch(this);
    }
}

@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    // 处理 NFC 标签
    if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction())) {
        Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
        // 读取标签数据...
    }
}
💡 小技巧: 我建议在 onNewIntent() 里处理标签,而不是 onResume()。因为 onNewIntent() 只会在有新 Intent 时触发,而 onResume() 每次恢复都会执行,容易重复处理。

前台分发的优先级

这里有个关键点:前台分发的优先级高于 Intent Filter。也就是说,如果你的 Activity 启用了前台分发,那么所有 NFC 标签都会先经过它,而不是去匹配 AndroidManifest 里声明的 Intent Filter。

我画了一张图,帮你理解整个分发流程:

NFC 标签分发流程 NFC 标签被检测到 前台分发 已启用? 直接分发到 前台 Activity 匹配 Intent Filter 匹配成功? 成功 启动对应 Activity 失败 标签被忽略 成功路径 失败路径 判断节点

从图里可以清楚看到:前台分发是「截胡」机制。一旦启用,系统就不再走 Intent Filter 匹配流程了。这在你需要动态控制 NFC 行为时非常有用。

实际项目中的避坑指南

讲几个我踩过的坑:

  • 不要忘记在 onPause 中禁用前台分发。否则你的 Activity 退到后台后,其他应用可能无法正常使用 NFC。我曾经因为这个被测试团队追着改了一个星期。
  • Android 12 之后,PendingIntent 必须声明 FLAG_IMMUTABLE。否则会 crash。这是 Google 强制的安全要求。
  • NFC 标签的读写操作必须在子线程执行。主线程操作 NFC 会导致 ANR。我习惯用 AsyncTask 或者 Coroutine 来处理。
  • 注意 NDEF 格式的兼容性。有些标签是 Mifare Classic,有些是 Type A/B。你的代码要能处理多种技术类型。

核心总结:

  • NFC 权限只有一个 android.permission.NFC,属于 normal 级别
  • 前台分发是处理 NFC 标签的首选方式,优先级高于 Intent Filter
  • 前台分发必须在 onResume 启用,onPause 禁用
  • Android 12+ 要求 PendingIntent 使用 FLAG_IMMUTABLE

好了,NFC 权限这块就这些。说实话,它比普通运行时权限简单得多,但细节不少。你只要记住「前台分发是核心」这句话,基本就能搞定大部分场景。


公众号:蓝海资料掘金营,微信 deep3321