news 2026/10/7 1:32:38

NFC标签双端App唤起与未安装兜底:Android/iOS全链路配置

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
NFC标签双端App唤起与未安装兜底:Android/iOS全链路配置

开头先抛个场景:公司前台、会议室门口、展厅展台,甚至外卖柜上贴一枚圆形小标签,手机背面一贴,装了 App 的直接跳进 App 对应页面,没装的自动引导去应用市场下载。这个需求我前后做过三四个版本,第一次做的时候天真地以为「写个 URL 进标签就完事了」,结果 Android 和 iOS 给出了完全不同的两套反馈:Android 那边偶尔弹个选择框问用户用哪个应用打开,iOS 那边碰半天屏幕只弹一张小卡片还要用户自己点。折腾了两轮之后才把这套链路理顺,今天就把它完整写下来,包括标签怎么选、NDEF 记录怎么构造、双端的域名校验怎么做、没装 App 的兜底页怎么写,还有几个官方文档里不会明说的坑。只要你对 Android 或 iOS 有一点开发基础,跟着这篇走基本能一次跑通,前端同学也能看懂落地页那部分。

1. 需求拆解与整体方案设计

1.1 一枚标签要同时伺候两种系统,难点在哪

先把这个需求掰开看。用户动作只有一个:手机贴近标签。但系统层面的处理路径是两条完全独立的线。Android 的 NFC 分发机制是「谁声明了能处理这个 URI,就把 Intent 派发给谁」,如果没有任何 App 声明,系统会交给浏览器去打开这个 URL。iOS 的后台标签读取则是「系统自己解析 NDEF 记录,如果这个 URL 与某个已安装 App 的关联域名匹配,就悄悄把 App 拉起来;不匹配就只在通知栏显示一条 Safari 提示」。

所以真正的难点不是「怎么写标签」,而是「怎么让系统在 App 已安装和未安装两种状态下,都做出你期望的动作」。已安装时希望直接唤起 App 并带上参数,未安装时希望落到一个你能控制的页面,再由这个页面决定去哪个应用市场。这里的关键洞察是:标签里只需要写一个东西——你自己域名下的 HTTPS 短链。剩下所有的分支判断,全部交给系统和这个短链的落地页去处理。

为什么不直接在标签里写market://或者itms-apps://这种自定义 scheme?因为这类 scheme 系统层不认识,NFC 分发时找不到处理者就直接失败,用户贴上去毫无反应,体验比不贴还差。HTTPS 链接是系统层通用的「最大公约数」,这是整个方案能成立的地基。

1.2 为什么最终选 HTTPS URI 记录 + 域名兜底页

我试过三种写法,最后留下的是最朴素的那种,理由如下表。

标签写法已安装 App 时未安装时结论
自定义 scheme(如myapp://detail/1)Android 可唤起,iOS 基本无反应完全无反应弃用
应用市场链接(market:///itms-apps://)直接跳市场,无法唤起 App跳市场弃用
自有域名 HTTPS 短链App Links / Universal Links 直接唤起 App浏览器打开落地页,由落地页分流采用

第三种方案的精髓在于「把判断权后置」。标签本身是死物,写进去就改不了了,所以标签里放的东西必须足够稳定。而落地页是你自己服务器上的代码,随时能改市场链接、随时能加渠道统计、随时能针对某个机型做特殊处理。这个「标签内容尽量简单,业务逻辑尽量放在可服务端控制的地方」的原则,是我这几年做物联网相关需求最常复用的经验。

另外一点,HTTPS 短链还有个隐性好处:它同时是一张可点击的链接。你可以在微信图文、短信、海报二维码里复用同一个 URL,不用为线上渠道和线下 NFC 渠道分别维护两套逻辑。

1.3 标签硬件选型与容量估算

标签芯片选型这块踩过一次坑。最早买的是某宝上一块钱十个的 NTAG213 白卡,写一条 URI 加一条文本就报「容量不足」。后来算了一下就明白了。

NDEF 记录本身有结构开销:一个 URI 记录大约需要 7 字节头部(TNF、类型长度、payload 长度等)+ 类型字段「U」1 字节 + URI 前缀标识 1 字节 + 实际 URI 内容。一个 40 字符的 HTTPS 链接,大概占到 45 到 50 字节。如果你还想加一条 AAR(Android Application Record)来辅助 Android 跳市场,再加约 30 字节。再加上 TLV 包装和终结符,一条消息很容易超过 80 字节。

芯片型号用户可用容量能装什么建议
NTAG213144 字节一条短 URL,极限不推荐
NTAG215504 字节URL + AAR + 若干预留,宽裕推荐起步
NTAG216888 字节多条记录、长 URL、离线配置预算够就上

我现在的标准配置是 NTAG215 的 PVC 圆卡或者抗金属贴片。抗金属贴片贵一些,但贴在金属机柜或者门禁面板上必须用它,普通卡贴上去读不到,因为金属会吸收磁场。形状上会议室桌面用圆卡,户外或设备上用 PVC 挂扣卡,防水防磕。

还有个物理层面的注意点:读卡距离。手机 NFC 天线的位置在机身背面靠上三分之一处,不同机型差异很大。iPhone 的天线在顶部摄像头附近,很多安卓机在中上部。所以标签贴的位置要留出至少 2 到 3 厘米的操作空间,别贴在特别低的角落,用户蹲半天贴不上会很烦。实测下来,笔尖大小(直径 20 到 30 毫米)的标签配合「贴纸 + 图标提示」的视觉引导,一次成功率能到九成以上。

2. Android 侧:NDEF 分发、App Links 与市场兜底

2.1 标签内容怎么构造才不踩坑

Android 侧最省事的做法是用现成的写卡工具,我自己常用的是手机上的 NFC 写卡类工具 App,加一条「URL / URI」类型的记录,把https://nfc.example.com/t/a1b2c3填进去,然后写入。

如果你想在代码里批量写卡,逻辑大概是这样的(Kotlin,Android SDK 的NdefRecord):

val uriBytes = url.toByteArray(Charsets.UTF_8) // 第一步:压缩 URL 前缀,能省下 10 个字节左右 // NDEF URI 记录的 payload 第一个字节是前缀缩写码,0x04 代表 "https://" // 后面的字节只放去掉前缀的部分 val payload = ByteArray(uriBytes.size + 1) payload[0] = 0x04 System.arraycopy(uriBytes, 0, payload, 1, uriBytes.size)

注意:如果 URL 里带了https://前缀,payload 首字节写 0x04,后面的内容要从https://之后开始截取。我第一次写反了,把完整 URL 都塞进去,结果读出来的 URL 变成https://https://xxx,排查了小半天。

写完 URL 记录之后,是否要再写一条 AAR,这是个需要权衡的选择。AAR 的 payload 是包名,MIME 类型是application/vnd.android.package-archive。它的作用是:当系统解析标签时,如果这个包名的 App 没装,系统会尝试去应用商店搜索这个包。听起来很美好,但在国内机型上,AAR 的表现非常不一致——很多国产 ROM 没有 GMS,AAR 指向的是 Google Play,结果就是什么都不发生。所以我的建议是:AAR 可以写,但绝不能把它当成唯一的兜底手段,真正的兜底必须靠域名落地页。

2.2 AndroidManifest 三处配置缺一不可

Android 这边要让「贴标签直接进 App」生效,Manifest 里需要配两个 intent-filter,一个负责 NFC 分发,一个负责网页链接分发。两个都配上,才能覆盖「贴卡」和「点链接」两条入口。

<activity android:name=".EntryActivity" android:exported="true" android:launchMode="singleTop"> <!-- 入口一:NFC 标签分发 --> <intent-filter> <action android:name="android.nfc.action.NDEF_DISCOVERED" /> <category android:name="android.intent.category.DEFAULT" /> <data android:scheme="https" android:host="nfc.example.com" /> </intent-filter> <!-- 入口二:App Links,域名校验通过后才自动生效 --> <intent-filter android:autoVerify="true"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="https" android:host="nfc.example.com" android:pathPrefix="/t/" /> </intent-filter> </activity>

三个关键点解释一下。android:launchMode="singleTop"是为了防止用户在 App 已经打开的情况下再贴一次卡、又新建一个 Activity 实例导致栈里堆一串。android:exported="true"是 Android 12 之后的硬性要求,这个 Activity 需要被系统外部唤起,必须显式声明,不写直接编译报错。

android:autoVerify="true"这个属性最容易被忽略。它决定了系统是否在安装时主动去校验你的域名归属。如果不写,系统在打开链接前会弹一个「用哪个应用打开」的选择框,用户点了「总是」才记住,体验上多一步,而且很多用户会点错去浏览器。写了之后系统会去https://nfc.example.com/.well-known/assetlinks.json拉取校验文件,通过之后链接就静默直达 App。

2.3 assetlinks.json 校验失败会怎样

assetlinks.json这个文件是 Android App Links 的核心。它必须放在域名的/.well-known/路径下,且必须是无重定向的 HTTPS 直出,Content-Type 建议application/json。

[ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.example.nfcdemo", "sha256_cert_fingerprints": [ "3A:5B:...你签名证书的SHA256指纹..." ] } } ]

拿到这个 SHA256 指纹的方式,调试版和发布版是不同的。调试版可以用keytool命令查看默认的 debug keystore:

keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android

发布版则要去你的签名文件里取。这里有个大坑:如果你用了应用市场的「签名重签」服务(国内几家市场都有这个功能),线上包的签名指纹和你在本地签名文件里的指纹是不一样的,assetlinks.json 里必须填市场重签后的指纹,否则线上校验必然失败。我第一次上线就翻在这里,本地测试全通,发到线上链接全被浏览器接管。解决办法是去开发者后台把重签后的证书指纹复制出来补进去。

校验失败的表现是:用户点击链接后,系统弹出一个包含浏览器和你的 App 的选择列表。用户如果手快选了「浏览器」,下次打开还是浏览器,因为默认设置已经被记住了。这种情况只能引导用户去系统设置里清除默认打开方式,非常麻烦。所以每次发布新版本、每次换签名配置,都要重新跑一遍校验验证。

验证方法很简单,把手机连上电脑执行:

adb shell pm verify-app-links --re-verify com.example.nfcdemo adb shell pm get-app-links com.example.nfcdemo

输出里如果看到verified状态就说明通过了。如果显示none或者legacy_failure,就要回头查 assetlinks 文件是不是能公网直接访问、指纹对不对、有没有被 CDN 缓存了旧版本。

2.4 App 未安装时的降级路径

App 没装的情况下,Android 系统的行为是:NFC 分发找不到处理者,于是把 NDEF 记录里的 HTTPS URL 交给默认浏览器打开。这时候落地页就登场了,落地页会识别 UA 并跳转到对应市场。这部分逻辑我会在第 4 章统一讲,这里先说 Android 端的一个特殊处理。

如果一定要在 Android 端用代码直接唤起市场(比如在 App 内部做强制更新),可以用intent://格式的链接,它比market://更可控,因为可以指定包名:

intent://details?id=com.example.nfcdemo#Intent;scheme=market;action=android.intent.action.VIEW;category=android.intent.category.BROWSABLE;package=com.huawei.appmarket;end

这段写法的意思是:构造一个指向market://details?id=xxx的 Intent,只允许华为应用市场(com.huawei.appmarket)来处理。如果该市场没装,用S.browser_fallback_url=参数指定一个网页兜底,比如直接跳官网下载页。这个参数在落地页里非常有用,它能让「市场未安装」这个边缘情况也有出路。

提示:intent://形式在微信内置浏览器里会被拦截,这一点在第 4 章细说。

3. iOS 侧:后台读取的能力边界与 Universal Links

3.1 iOS 到底能不能「碰一碰自动打开 App」

这是被问得最多的一个问题。直接给结论:从 iPhone XS 及之后的机型、系统在 iOS 14 及以上,是支持的,但有几个硬性前提。

第一,必须是「后台标签读取」(Background Tag Reading)。这个能力不需要你的 App 在前台运行,甚至不需要 App 在后台活着,系统框架自己会去解析标签上的 NDEF URI 记录。第二,标签上必须是一条有效的 NDEF URI 记录,其他类型的记录(纯文本、MIME、自定义外部类型)不会被触发。第三,手机屏幕必须处于点亮状态(锁屏也可以,但息屏不行),因为系统要在屏幕上弹出提示卡片。

满足这三个条件之后,系统的判断逻辑是:把标签上的 URL 和自己数据库里所有已安装 App 的 Associated Domains 做比对。命中就直接把 App 拉起来(用户甚至会感觉不到卡片弹出);没命中就显示一条通知,点击后打开 Safari。

这里必须说清楚一个预期管理问题:iOS 的后台读取在「已安装且域名匹配」的情况下体验最好,几乎是秒开。但如果是「未安装」,用户会看到一张卡片,上面写着「Safari 中打开」或者 App Clip 的名字,需要用户主动点一下。这是系统层面的设计,开发者无法绕过,也没法让 NFC 标签直接把用户扔到 App Store。所以 iOS 端的「未安装跳市场」这个动作,永远是「落地页 + 用户一次点击」的组合,做不到 Android 那种全自动。

至于 Android,情况正好相反:Android 的后台分发是真正意义上的自动,连提示卡片都不一定有。

3.2 AASA 文件与 Associated Domains 实操

iOS 这边的域名归属校验靠的是 AASA 文件(apple-app-site-association),放在https://nfc.example.com/.well-known/apple-app-site-association,注意它没有后缀名,不能写成.json,而且必须无重定向、必须是 HTTPS。

{ "applinks": { "apps": [], "details": [ { "appID": "ABCDE12345.com.example.nfcdemo", "components": [ { "/": "/t/*", "comment": "NFC 标签落地路径" } ] } ] } }

appID是「Team ID + Bundle ID」拼起来的,中间用点号连接。Team ID 在开发者账号后台的 Membership 页面能看到,是十位字符。components是 iOS 13 之后推荐的新写法,比老的paths数组更灵活,支持排除规则和查询参数匹配。如果你的 App 还要兼顾 iOS 12,那两种写法都要保留。

Xcode 侧需要在 Signing & Capabilities 里加上 Associated Domains 能力,然后添加一条applinks:nfc.example.com。注意这里不要带https://,只写applinks:加域名。我见过有人写成applinks:https://nfc.example.com,校验直接失败。

注意:AASA 文件会被系统缓存,而且缓存策略不太透明。官方说安装 App 时拉取一次,卸载重装会重新拉取。但实测下来,用 TestFlight 或者直接覆盖安装时,缓存有时会保留。开发阶段的调试技巧是:卸载 App、重启手机、重新安装,三步下来基本能拿到最新配置。

关于 AASA 文件,Apple 提供了一个官方的校验接口,虽然不公开宣传,但社区里一直在用:

curl -i https://app-site-association.cdn-apple.com/a/v1/nfc.example.com

这个地址返回的是 Apple 自己 CDN 上缓存的那份 AASA 内容。如果你更新了文件但线上不生效,直接请求这个地址看看是不是还返回旧内容,能快速定位是「服务器没更新」还是「Apple 缓存没刷新」。

3.3 App Clip:未安装也能跑一段的折中方案

如果你的场景是「用户没装 App,但希望他立刻完成一个轻量动作」,比如扫码签到、领取优惠券、查看展品信息,那么 App Clip 是这个需求在 iOS 上最优雅的答案。

App Clip 本质是一个「按需加载的 App 轻量版」,不安装也能启动,体积有严格的体积上限(官方给的门槛是 10MB 量级,具体以官方文档为准,超了会被拒审)。用 NFC 标签触发 App Clip 的配置方式是:在原有 AASA 文件里增加appclips段,同时把 App Clip 的 Bundle ID 也加进appIDs里。

{ "applinks": { "details": [ { "appIDs": [ "ABCDE12345.com.example.nfcdemo", "ABCDE12345.com.example.nfcdemo.Clip" ], "components": [ { "/": "/t/*", "comment": "NFC 与 App Clip 共用路径" } ] } ] }, "appclips": { "apps": ["ABCDE12345.com.example.nfcdemo.Clip"] } }

配置好之后,用户贴标签时:已安装完整 App 的直接打开完整 App;没装的会弹出 App Clip 的系统卡片,用户点一下就能用上轻量功能,用完即走,还可以在 App Clip 内部引导用户下载完整版(系统提供了SKOverlay组件来做这件事,展示一个官方样式的下载浮层)。

App Clip 在 Xcode 里是作为一个独立的 Target 存在的,需要单独配置NSAppClip相关的 Info.plist 键,还要在 App Store Connect 里单独提交审核。审核周期和主 App 是分开的,所以上线节奏上要留出余量。

3.4 前台主动读卡:Core NFC 代码怎么写

后台读取虽然省事,但如果你需要读取标签里的自定义数据(比如一张工牌卡里的工号),那就必须用前台会话。Core NFC 的前台读取需要用户在 App 内主动触发,系统会弹出一个扫描面板。

import CoreNFC final class TagReader: NSObject, NFCNDEFReaderSessionDelegate { private var session: NFCNDEFReaderSession? func begin() { // invalidateAfterFirstRead: true 表示读到一个标签就自动结束会话 session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: true) session?.alertMessage = "请将手机背面靠近标签" session?.begin() } func tagReaderSessionDidBecomeActive(_ session: NFCNDEFReaderSession) { // 扫描面板已显示 } func tagReaderSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) { // 用户取消、超时、读取失败都会走到这里 self.session = nil } func tagReaderSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) { for message in messages { for record in message.records { if let url = record.wellKnownTypeURIPayload() { print("读到链接: \(url)") } } } } }

Info.plist 里必须加NFCReaderUsageDescription,描述文案会展示在系统权限弹窗里。另外 Xcode 的 Signing & Capabilities 里要打开 Near Field Communication Tag Reading 能力,并在 entitlements 里声明com.apple.developer.nfc.readersession.formats包含NDEF。

提示:前台会话在模拟器上完全无法测试,必须真机。我吃过一次亏,模拟器上代码跑得顺顺利利,真机一测发现会话根本没起来,原因是 entitlements 没配。

4. 统一落地页:一套分流逻辑搞定双端

4.1 UA 识别与厂商市场映射表

落地页是整个方案的「交通枢纽」,它的职责是:App Links / Universal Links 没生效(也就是 App 没装)的时候,把用户精准送到对应市场。核心逻辑就一段 JS,但里面有不少细节。

(function () { var ua = navigator.userAgent || ''; var isIOS = /iPhone|iPad|iPod/i.test(ua); var isAndroid = /Android|HarmonyOS/i.test(ua); if (isIOS) { // itms-apps 会直接拉起 App Store 应用,不经过 Safari 中转 // 注意:必须先尝试 Universal Links,失败后再走这里,靠延时兜底 location.replace('itms-apps://itunes.apple.com/app/id1234567890'); return; } if (isAndroid) { var pkg = 'com.example.nfcdemo'; var target = ''; if (/HUAWEI|HONOR|HMSCore/i.test(ua)) { // 华为优先走 AppGallery 网页版,兼容性最好 target = 'https://appgallery.huawei.com/app/C100000001'; } else if (/MIUI|XiaoMi|Redmi/i.test(ua)) { target = 'https://app.mi.com/details?id=' + pkg; } else if (/OPPO|ColorOS|OnePlus/i.test(ua)) { target = 'oppomarket://details?packagename=' + pkg; } else if (/vivo|iQOO/i.test(ua)) { target = 'vivomarket://details?id=' + pkg; } else { // 兜底:用 intent 语法唤起任意市场,失败则走网页下载页 target = 'intent://details?id=' + pkg + '#Intent;scheme=market;action=android.intent.action.VIEW;' + 'category=android.intent.category.BROWSABLE;' + 'S.browser_fallback_url=' + encodeURIComponent('https://nfc.example.com/download') + ';end'; } location.replace(target); } })();

这张映射表是我踩了不少坑之后稳定下来的。几个经验点说一下。

华为这边我最终选了网页版 AppGallery 而不是appmarket://的 scheme,原因是华为机型上 scheme 唤起有时会闪一下就回到浏览器,尤其在新版 HarmonyOS 上表现不稳定,网页版反而更可靠——它会先在浏览器打开 AppGallery 的详情页,再自动唤起本地市场,多一跳但成功率高。小米用https://app.mi.com/...同理,这个地址在小米自带浏览器里会直接拉起应用商店。

OPPO 和 vivo 我保留了 scheme 写法,因为这两家的网页版在非本品牌浏览器里会提示「请用手机自带浏览器打开」,反而更麻烦。

4.2 跳转时序、防死循环与降级兜底

落地页还有个必须处理的时序问题。用户贴标签时,Android 或 iOS 可能已经通过 App Links 唤起 App 了,但同时浏览器也加载了落地页。如果落地页无脑跳市场,用户就会经历「App 打开一下又被扔到市场」,非常难受。

标准的处理手法是「延时兜底 + 页面失焦检测」:

var jumped = false; // 页面被切到后台(也就是 App 被成功唤起)时,取消兜底跳转 document.addEventListener('visibilitychange', function () { if (document.hidden) { jumped = true; } }); window.addEventListener('pagehide', function () { jumped = true; }); window.addEventListener('blur', function () { jumped = true; }); setTimeout(function () { if (jumped) return; // App 已经起来了,什么都不做 doMarketJump(); // 1500ms 后还没动静,说明 App 没装,走市场 }, 1500);

1500 毫秒这个数字是实测调出来的。太短(比如 500ms)在低端机上会误判,App 还没启动完就跳市场了;太长(3000ms 以上)用户会觉得页面卡住了。1500 到 2000 是比较舒服的区间,我一般配 1600。

另外必须做的是防死循环。有些场景下落地页会把自己当成目标,比如用户在市场上没找到 App、又回到落地页,落地页再次跳市场,来回循环。解决办法是在 URL 上带一个一次性标记,比如落地页打开时先写sessionStorage.setItem('jumped', '1'),如果读到这个标记存在就不再执行自动跳转,改为展示一个手动下载按钮页面。

4.3 微信/QQ 内置浏览器的处理

如果你的标签可能会被分享到社交软件,那必然要面对内置浏览器。微信内置浏览器对itms-apps://、market://、intent://这几类 scheme 全部拦截,iOS 上还好说,可以用https://apps.apple.com/...网页链接,微信会弹出「即将离开微信,打开 App Store」的确认;Android 上就比较麻烦,只能提示用户点右上角「在浏览器中打开」。

判断方式很直接:

var ua = navigator.userAgent.toLowerCase(); var isWechat = /micromessenger/.test(ua); var isQQ = /\sqq/i.test(ua) || /mqqbrowser/.test(ua); if (isWechat || isQQ) { // 展示一个遮罩层,用箭头指向右上角,引导用户用外部浏览器打开 showOpenInBrowserGuide(); return; }

这个遮罩层的设计要点是:箭头要指向真实的「···」按钮位置(iOS 在右上,Android 在右上或右下,各家微信版本略有差异),文案要直白——「点击右上角,选择在浏览器中打开,即可自动跳转下载」。我做过 AB 测试,加了引导层之后 Android 端的下载转化率大概翻了一倍,因为不加引导用户根本不知道发生了什么,只看到页面没反应就关掉了。

5. 实测问题排查与避坑清单

5.1 常见问题速查表

这一节是我自己维护的一份排查清单,遇到问题先过一遍表。

现象可能原因排查动作
贴卡完全没反应标签容量不足写失败 / 手机 NFC 未开启 / 标签已损坏换手机试试,用读卡工具确认标签内容
弹「用哪个应用打开」选择框App Links 校验未通过用adb shell pm get-app-links查状态,重查 assetlinks.json
Android 正常,iOS 完全没反应AASA 文件 404、有重定向、或路径写成了.json请求app-site-association.cdn-apple.com确认 Apple 侧缓存
iOS 弹 Safari 卡片而不是 App标签 URL 的域名与 Associated Domains 不匹配检查域名是否完全一致,含子域名
落地页跳到市场但市场没装该机型没有配置的市场应用加S.browser_fallback_url兜底
每次贴卡都新建一个页面ActivitylaunchMode不是singleTop改为singleTop并重写onNewIntent
线上包链接打不开,本地包正常市场重签名导致证书指纹变化用重签指纹更新 assetlinks.json
读了标签两三次才成功标签贴的位置与手机天线错位调整标签位置,加大标签尺寸

5.2 几个我踩过的坑

第一个坑:Android 12 的 PendingIntent 可变性。如果你用的是enableForegroundDispatch这套老 API,升级到 Android 12 之后会直接崩,因为创建 PendingIntent 时必须显式指定FLAG_MUTABLE或FLAG_IMMUTABLE。NFC 分发场景必须用FLAG_MUTABLE,因为系统需要往 Intent 里填数据。Android 14 之后官方更推荐enableReaderMode,它不需要 PendingIntent,代码更干净,还能同时关闭系统提示音(FLAG_READER_NO_PLATFORM_SOUNDS),在会议室这种安静场景很实用。

第二个坑:onNewIntent别忘了处理。Activity 是singleTop的时候,第二次贴卡不会走onCreate,而是走onNewIntent。我见过很多实现只在onCreate里解析 Intent,结果第二次贴卡就停在原页面没反应。正确做法是把解析逻辑抽成一个方法,两个入口都调用它。

第三个坑:iOS 的 AASA 文件不能带重定向。这个坑特别隐蔽,因为 curl 看不出来(加-L就跟没事一样),但系统拉取时不跟随重定向,直接判定校验失败。常见的触发场景是:http://到https://的跳转、不存在的路径被 CDN 的 404 页面兜底返回 200、或者.well-known目录被某个安全策略拦截。用curl -I看返回码和Location头,必须是 200 且没有Location。

第四个坑:标签被误锁定。NTAG 系列芯片支持「锁定」操作,锁定后数据永久只读,无法擦写。有些写卡工具里「保护标签」的按钮位置很显眼,手一抖点下去这张卡就废了。生产环境批量做卡时,建议先小批量试产、验证链路跑通,再批量写卡,别一次性写五百张。

5.3 安全与运营上的几个提醒

NFC 标签本身是个纯物理载体,它不加密、不认证,只负责把一段字节交给手机。这意味着任何人拿一台支持 NFC 的手机都能把标签内容读出来,也都能复制一份。如果你的业务逻辑依赖标签 ID 做身份校验,那就要格外小心,因为复制一张卡的物理成本几乎为零。

我的处理方式是:标签里只放一个一次性的短链 token,真正的业务校验在服务端完成。短链映射到服务端记录,记录里有场景、有效期、使用次数、是否已核销等字段。这样即使 token 被读取,也只能落到一个已经失效或者已核销的页面。如果场景对安全性要求更高(比如门禁、支付),那就不能只用静态标签,得配合动态加密卡或者服务端下发的挑战响应机制。

运营层面还有个小技巧值得分享:落地页里一定要埋上报点,记录「打开来源」「UA 机型」「是否跳转市场」「是否成功唤起 App」。这些数据能帮你判断标签投放效果。我之前做一个展台项目,埋点之后发现某个机型(某品牌的一个中端系列)的唤起成功率低得离谱,排查下来是它的 NFC 天线位置特别靠下,配合我们的标签贴法基本贴不到,后来单独调整了那个展台的标签高度,成功率就上去了。这类问题如果没埋点,你永远只能听到「有的手机不好使」这种模糊反馈。

我在实际落地这套方案时的体会是,NFC 这块真正的门槛不在代码本身,写的都是几十行的配置和脚本,难的是对两套系统行为差异的准确预期管理。Android 会给你惊喜,也会给你惊吓,它的分发行为跟机型、ROM、GMS 有无强相关;iOS 则是规则明确但边界很硬,很多东西系统不让你做,你只能顺着它的设计走。所以做之前先想清楚:用户到底是「已装 App 的老用户」还是「未装 App 的新用户」为主?如果是后者占大头,那 Android 端把落地页做扎实、iOS 端考虑上 App Clip,会比死磕「全自动唤起」务实得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 1:31:32

南大计院夏令营机试与笔试题型破解:刷题路线与算法模板全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 1:30:58

ESP32隐藏射频通路:寄存器级IQ采样与嵌入式SDR实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 1:30:21

RC电路微分与积分全解析:从原理到工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 1:30:17

TL431不是稳压芯片,而是精密并联基准源

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 1:29:51

PCB布局布线规则全解析:从器件摆放到DRC检查的实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 1:29:40

MOS管体二极管Vf与Trr实测:双脉冲测试平台搭建与判读

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华