移动产业下的 NFC 生态链

1,614 阅读5分钟

cover

相信在当今移动科技高度发达的环境下已经很少有不知道 NFC 是什么的人了,那么我们作为软件工程师往往知道与应该知道的要比普通大众更多,NFC 都能做些什么事情?如何接入 NFC 功能?如何在技术上使 NFC 功能做到移动场景全覆盖?

简介

NFC 全称为 Near Field Communication, 中文全称为近场通讯技术,以 13.56 MHz 频率运行于 10 厘米距离内。其传输速度有 106 kbit/s 、212 kbit/s 以及 424 kbit/s 三种。

NFC 技术是由非接触式射频识别(RFID)及互连互通技术整合演变而来,通过在单一芯片上集成感应式读卡器、感应式卡片和点对点通信等功能,可以让使用了 NFC 技术的设备(比如手机)在彼此靠近的情况下进行数据交换。

对比

当下的近距无线通信技术除 NFC 外,主要还包括射频识别(RFID)、蓝牙(Bluetooth)、紫蜂(ZigBee)、红外、 Wi-Fi 等技术。以上各项技术都有各自的优缺点,下图给出了 NFC 以及其他几种短距离无线通信技术在所列频段上性能的比较。

上图可以看出,NFC 技术具有极高的安全性,在短距离通信中具有性能优势,更重要的是成本较低。

在日常生活中我们接触过的有移动支付、电子票务、移动身份识别、防伪等应用场景。比如我们坐地铁刷卡,出入小区的门禁,再比如人所皆知的茅台、五粮液酒的防伪都使用了 NFC 功能。

如何使用

那么我们今天主要讲在移动网络下,如何使 NFC 功能形成闭环,相信很多同学在开发的时候都碰到过一个问题,那就是安卓开发在使用 NFC 功能时没有问题,但是使用 iOS 接入时,相同的数据和写入方式就无法识别,无论是读还是写,原因在于苹果爸爸的 Core NFC 中对 NFC 的数据格式仅支持 NDEF 格式读写。也就是说,如果想做全生态的 NFC 功能,那么笔者建议使用 NDEF 来作为你们通信的通用格式,以保证相关功能的完整流转,话不多说,先上一张笔者一笔一笔画出来的图,我们看一下都有哪些场景可以使用 NFC 功能。

当我们选择使用 NDEF 格式进行通讯的情况下,那么无论是安卓,还是 iOS 又或者微信小程序,都可以做到数据共享,无论你是否在自身应用内识别还是通过系统进行识别。

下面我们分别来讲解一下芯片的数据写入和各端如何接入。

完整接入教程

假定现在要实现一个扫描芯片后跳转到我们应用内某个指定页面或者网页的需求。

NFC 芯片数据写入

1.了解 NDEF

NFC 芯片和各端数据交换遵循 NDEF 协议,以下是 NDEF 的组成部分。

可以看到消息主要由若干个 Record 组成,Record 中的内容遵循 RTD(Record Type Definition)协议。

RTD_URI 记录内容格式如下表 1-1 所示,其中 Identifier code 为 URI 前缀标识符,其值如表 1-2 所示。

表 1-1 RTD_URI 记录内容

NameOffsetSizeValueDescription
Identifer code01 byteURI Identifier codeURI 前缀如表 1-2 所示
URI field1NUTF-8 StringThe rest of the URI,or the entire URI(If Identifier code == 0x00)

表 2-2 RTD_URI 前缀缩略表(部分)

十进制数(Decimal)十六进制数(Hex)协议(Protocol)
00x00N/A(没有匹配的协议)
10x01http://www.
20x02https://:www.
30x03http://
40x04https://
... ...... ...... ...

2. 写入数据规则

写入格式遵循 RTD_URI(即内容写在 payload 中),规定 Header 中 Type 为 Well-Known Records(TNF Record Type 0x01)。

由于需要支持 iOS、Android 与小程序三端,因此需要以下两条 Record 来供各端识别。

  • HTTP Record

  • Android Application Record

2.1 HTTP Record

schemehttps
hostxxxx.xxxxx.com
path/path

2.2 Android Application Record

该 Record 在 Android 系统扫描时使用,当 App 未安装支持跳转应用市场。

3. 注意点

Record 写入顺序必须为先 HTTP Record 再 Android Application Record,否则 iOS 会识别不到 App Clip。

4. 代码示例(Android)

写入数据


fun writeNfcData(intent: Intent) {

  val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)

  val NDEF= Ndef.get(tag)

  ndef.connect()

  //添加一条 HTTP Record

  val schemeRecord =

  NfcRecordUtils.getHttpRecord("需要写入的URL")

  //添加 Android Application Record

  val appRecord = NdefRecord.createApplicationRecord("应用包名")


  val maxSize = ndef.maxSize

  Log.e("NFC", "存放的最大值${maxSize}")

  val ndefMessage = NdefMessage(records)

  ndef.writeNdefMessage(ndefMessage)

}

工具类(HttpRecordUtils.lt)

@JvmStatic
fun getHttpRecord(url: String, id: String? = null): NdefRecord {
      var uri = Uri.parse(url)

      uri = uri.normalizeScheme()
      var uriString: String = uri.toString()
      require(uriString.isNotEmpty()) { "uri is empty" }
      var prefix: Byte = 0
      for (i in 1 until URI_PREFIX_MAP.size) {
          if (uriString.startsWith(URI_PREFIX_MAP.get(i))) {
              prefix = i.toByte()
              uriString = uriString.substring(URI_PREFIX_MAP[i].length)
              break
          }
      }
      val uriBytes = uriString.toByteArray(StandardCharsets.UTF_8)
      val recordBytes = ByteArray(uriBytes.size + 1)
      recordBytes[0] = prefix
      System.arraycopy(uriBytes, 0, recordBytes, 1, uriBytes.size)

      return NdefRecord(
          NdefRecord.TNF_WELL_KNOWN,
          NdefRecord.RTD_URI,
          id?.toByteArray(Charsets.UTF_8) ?: ByteArray(0),
          recordBytes
      )
}

敲重点! 接下来我们实践一下如何识别上面写入的数据。

iOS

1.我们先介绍一下应用内接入

首先需要在苹果开发者账号下开通 NFC 功能权限。

打开工程文件会如图所示

配置 info.plist 文件 添加 Privacy - NFC Scan Usage Description

到此为止你的前期配置部分已经完成。

开始编写代码

#import "ViewController.h"
#import <CoreNFC/CoreNFC.h>

@interface ViewController () <NFCNDEFReaderSessionDelegate>
 
@end

@implementation ViewController

// 开始扫描
- (void)startScan {
  /*
   条件:iphone7/7plus运行iOS11及以上
   invalidateAfterFirstRead 属性表示是否需要识别多个NFC标签,
   如果是 YES,则会话会在第一次识别成功后终止。否则会话会持续。
   不过有一种例外情况,就是如果响应了 -readerSession:didInvalidateWithError: 方法,
   则是否为 YES,会话都会被终止
   */

    NFCNDEFReaderSession *session =[[NFCNDEFReaderSession alloc] initWithDelegate:self 
                                                                            queue:nil
                                                         invalidateAfterFirstRead:YES];
  // 开始扫描
  [session beginSession];
}

// 扫描到的回调
-(void)readerSession:(NFCNDEFReaderSession *)session didDetectNDEFs:(NSArray<NFCNDEFMessage *> *)messages{
  /*
   数组 messages 中是 NFCNDEFMessage 对象
   NFCNDEFMessage 对象中有一个 records 数组,这个数组中是 NFCNDEFPayload 对象
   参考 NFCNDEFMessage、NFCNDEFPayload 类
   */
  
  for (NFCNDEFMessage *message in messages) {
    for (NFCNDEFPayload *record in message.records) {
      NSString *dataStr = [[NSString alloc] initWithData:record.payload
                           encoding:NSUTF8StringEncoding];
      NSLog(@"扫描结果 :%@", dataStr);
    }
  }

  // 主动终止会话,调用如下方法即可。
  [session invalidateSession];
}

// 错误回调
- (void)readerSession:(NFCNDEFReaderSession *)session didInvalidateWithError:(NSError *)error{
  // 识别出现 Error 后会话会自动终止,此时就需要程序重新开启会话
  NSLog(@"错误回调 : %@", error);
}

@end

值得注意的是

NFC 最低支持硬件 iPhone 7 或者 iPhone 7 Plus,最低支持系统为 iOS 11

使用 NFC 功能时,需要应用程序完全在前台模式,iPhone X 之后机型,支持后台读取。

此时你已经完成了应用内 NFC 功能的集成,赶快尝试吧。

2.没有自身应用时使用系统识别

此时我们有两个选择,一个是通过 App Clip 来实现我们的功能,另一个是打开我们在芯片中写入的网页地址。

2.1 App Clip 接入

  1. App Clip 接入请参照 官方文档,这里只简述主要步骤。

  2. 创建 App Clip Target。

  3. 创建 App Clip Target 时要关联该 App Clip 所依附的完整 App。

  4. Associated Domains(关联域名)。 。

  5. 类似 Universal links,如果你不了解可参照官方文档,注意 App Clip 和对应的完整 App 都要设置。

  6. 将 App Clip 与网站相关联。

  7. 编辑 apple-app-site-association 文件并放入指定位置。

  8. 获取 NFC-integrated App Clip Code 携带的参数。

  9. 当 App Clip 被唤起后会回调对应方法,并携带调用 url

  10. 需实现以下方法,userActivity 携带了调用 url

 func application(_ application: UIApplication, 

     continue userActivity: NSUserActivity, 

     restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
 }

至此通过 NFC 标签唤起 App Clip 或完整 App 的需求已完成。

2.2 Universal links 与 NFC

当手机靠近写入了 Universal links 链接的 NFC 标签,顶部将弹出识别到 url 的通知。

如果你的 App 配置过 Universal links,点击会唤醒 App。

如果你的 App 没有配置过 Universal links 或者你的手机没安装过对应 App,那么在点击通知后会从默认浏览器打开该页面。

Android

1. 通过手机 NFC 工具打开 App

在 Manifest 中声明 NFC 需要用到的权限。

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

在指定 Activity 中声明 Intent-filter。

<activity android:name=".MainActivity">
 <intent-filter>
   <action android:name="android.intent.action.VIEW"/>
   <category android:name="android.intent.category.DEFAULT"/>
   <category android:name="android.intent.category.BROWSABLE"/>
   <data
     android:host="xxx.xxx.com"
     android:scheme="https"
     android:path="/nfc"/>
 </intent-filter>
 <intent-filter>
   <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
   <category android:name="android.intent.category.DEFAULT"/>
   <data
      android:host="xxx.xxx.com"
      android:scheme="https"
      android:path="/nfc"/>
 </intent-filter>
</activity>

在 MainActivity 中处理 Intent Data。

class MainActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // 指的是 getIntent()
    dispatchIntent(intent)
  }

  override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    dispatchIntent(intent)
  }

  private fun dispatchIntent(intent: Intent) {
    when (intent.action) {
      NfcAdapter.ACTION_NDEF_DISCOVERED -> {
        dispatchNfcLink(intent.data)
      }
    }
  }

  private fun dispatchNfcLink(data: Uri?) {
    if(data != null) {
      // 处理参数
      val param1 = it.getQueryParameter("param1")
      val param2 = it.getQueryParameter("param2")
    }
  }
} 

2.App 内扫描 NFC 标签。

class ScanNfcResultActivity : BaseActivity() {
  companion object {
    internal const val NFC_REQUEST_CODE = 90
  }

  private var mNFCAdapter: NfcAdapter? = null
  private lateinit var mNFCFilter: Array<IntentFilter>
  private lateinit var mTechList: Array<Array<String>>
  private lateinit var mPendingIntent: PendingIntent
  override val layoutId: Int = R.layout.activity_nfc_result
  override fun initActivity(savedInstanceState: Bundle?) {
    StatusBarCompat.setStatusBarColor(this, Color.WHITE)
    
    isSupportNFC()
    val intent = Intent(this,ScanNfcResultActivity::class.java)
    
    // 私有的请求码
    mPendingIntent = PendingIntent.getActivity(
      this,
      NFC_REQUEST_CODE,
      intent,
      0
    )

    mNFCFilter = arrayOf(
      IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED),
      IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED),
      IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED)
    )
    // 只针对 ACTION_TECH_DISCOVERED
    mTechList = arrayOf(
      arrayOf(IsoDep::class.java.name), arrayOf(
        NfcA::class.java.name
      ), arrayOf(NfcB::class.java.name), arrayOf(
        NfcV::class.java.name
      ), arrayOf(NfcF::class.java.name), arrayOf(
        Ndef::class.java.name
      )
    )
  }

  /**
   判断是否支持 NFC
   */
  private fun isSupportNFC() {
    mNFCAdapter = NfcAdapter.getDefaultAdapter(this)
    mNFCAdapter?.let { adapter ->
      if (!adapter.isEnabled) {
        val setNfc = Intent(Settings.ACTION_NFC_SETTINGS)
        startActivity(setNfc)
      }
    }
  }

  override fun onResume() {
    super.onResume()
    mNFCAdapter?.enableForegroundDispatch(
      this,
      mPendingIntent,
      mNFCFilter,
      mTechList
    )
  }

  override fun onPause() {
    super.onPause()
    mNFCAdapter?.disableForegroundDispatch(this)
  }

  override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    try {
      val rawmsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
      if (rawmsgs != null) {
        val msg = rawmsgs[0] as NdefMessage
        val records = msg.records
        parseNFCData(records)
      }
    } catch (e: UnsupportedEncodingException) {
      e.printStackTrace()
    } catch (e: FormatException) {
      e.printStackTrace()
    } catch (e: IOException) {
      e.printStackTrace()
    }
  }

  private fun parseNFCData(recordList: Array<NdefRecord>) {
    recordList.forEach { record ->
      val payload = String(record.payload)
      val id = String(record.id)
      when (id) {
        "1" -> {
          val uri = Uri.parse(payload)
          val param1 = uri.getQueryParameter("param1")
          val param2 = uri.getQueryParameter("param2")
        }
      }
    }
  }
}

微信小程序

startNFC() {
  // 初始化
  const NFCAdapter = wx.getNFCAdapter();

  // 开启识别监听
  NFCAdapter.startDiscovery({
    success: (res) => {
      wx.showToast({
        title: '请将设备靠近要识别的 NFC 芯片',
        icon: 'none',
        duration: 2000,
      });
    },
    fail: (error) => {
      console.error('刷新重试');
    },
    complete: (res) => {
      //
    },
  });

  /**
    格式化得到 aid 值
   * @param {Object} buffer
   */
  const ab2hex = function (buffer) {
    const hexArr = Array.prototype.map.call(new Uint8Array(buffer), (bit) => (`00${bit.toString(16)}`).slice(-2));
    return hexArr.join('');
  };

  // 识别成功回调
  const discoveredCallBack = (callBack) => {
    wx.showToast({
      title: '已成功获取到信息',
      icon: 'none',
      duration: 500,
    });

    const read = {};
    const aid = parseInt(ab2hex(callBack.id), 16);
    if (callBack.messages) {
      const cordsArray = callBack.messages[0].records[0];
      // 一般情况,所存储的信息会写入在 payload 字段
      read.payload = Base64.decode(wx.arrayBufferToBase64(cordsArray.payload));

      // read 对象就是所存储的信息,拿到信息你可以处理具体业务逻辑了

      // 取消识别监听
      NFCAdapter.offDiscovered(discoveredCallBack);
      NFCAdapter.stopDiscovery();
      console.log('off');
    }
  };

  // 识别成功
  NFCAdapter.onDiscovered(discoveredCallBack);
}

小结

至此,我们就完成了所有 NFC 功能的相关集成,通过以上的介绍你可以做到绝大多数的业务场景覆盖,产品的线下推广,实物的防伪溯源,门禁的无卡开启等等。大胆的去尝试吧,朋友们!