Core NFC框架詳細解析 (二) —— CoreNFC使用簡單示例(一)

版本號 時間
V1.0 2020.06.07 星期日

前言

今天翻閱蘋果的API文檔,發(fā)現(xiàn)多了一個框架Core NFC,看了下才看見是iOS11.0新添加的框架,這里我們就一起來看一下框架Core NFC。感興趣的看下面幾篇文章。
1. Core NFC框架詳細解析 (一) —— 基本概覽(一)

開始

首先看下主要內(nèi)容:

在本教程中,您將學習如何使用CoreNFC無線連接到其他設備或NFC標簽。內(nèi)容來自翻譯

下面看下寫作環(huán)境:

Swift 5, iOS 13, Xcode 11

Near Field Communication(NFC)是一種用于短距離無線設備與其他設備共享數(shù)據(jù)或觸發(fā)這些設備上的動作的技術(shù)。它使用射頻場(radio frequency field)進行構(gòu)建,它可以使沒有電源的設備存儲小塊數(shù)據(jù),同時還使其他有源設備可以讀取該數(shù)據(jù)。

iOSwatchOS設備已經(jīng)內(nèi)置了NFC硬件已有幾年了。實際上,Apple Pay使用此技術(shù)與商店中的支付終端進行交互。但是,開發(fā)人員要到iOS 11才能使用NFC硬件。

蘋果通過引入Core NFC提升了iOS 13中的NFC游戲。借助這項新技術(shù),您可以對iOS設備進行編程,使其以新方式與周圍的互聯(lián)世界互動。本教程將向您展示一些使用該技術(shù)的方法。在此過程中,您將學習如何:

  • 將標準信息寫入NFC tag標簽。
  • 閱讀該信息。
  • 將自定義信息保存到標簽tag。
  • 修改標簽上已經(jīng)找到的數(shù)據(jù)。

重要說明:要執(zhí)行本教程中的所有步驟,您需要滿足以下條件:

  • 物理iOS設備
  • 蘋果開發(fā)者帳戶
  • 您可以讀取和寫入的NFC硬件。許多在線零售商都以合理的價格攜帶NFC tag。通常,您可以以大約10美元的價格獲得一包NFC標簽。在說明中尋找表明它是可編程的或列出其存儲容量(通常為300500字節(jié))的內(nèi)容。具有該近似容量的任何設備都超出了本教程的范圍。

starter文件夾中打開starter項目。 使用項目應用程序,您將學習如何:

  • NFC tag設置為“l(fā)ocation”。
  • 掃描location tag以查看其名稱和訪客日志。
  • 將訪問者(visitor)添加到位置標簽(location tag)。

構(gòu)建并運行。 您會看到以下內(nèi)容:


Writing to Your First Tag

首先,在Project navigator中選擇NeatoCache項目。 然后,轉(zhuǎn)到Signing & Capability,然后選擇+ Capability。 從列表中選擇Near Field Communication Tag Reading。

這將確保您的應用程序的配置文件(provisioning profile)設置為使用NFC。

接下來,打開您的Info.plist并添加以下條目:

  • KeyPrivacy – NFC Scan Usage Description
  • Value:使用NFC讀取和寫入數(shù)據(jù)

您需要此條目來向用戶傳達您正在使用NFC功能的用途,并符合Apple關(guān)于在應用程序中使用NFC的要求。

接下來,您將添加一個函數(shù),該函數(shù)可以執(zhí)行您的應用將處理的各種NFC任務。 打開NFCUtility.swift并將以下導入和類型別名添加到文件頂部:

import CoreNFC

typealias NFCReadingCompletion = (Result<NFCNDEFMessage?, Error>) -> Void
typealias LocationReadingCompletion = (Result<Location, Error>) -> Void

您需要導入CoreNFC才能使用NFC。 類型別名(type aliases)提供以下功能:

  • NFCReadingCompletion用于完成通用標簽讀取任務。
  • LocationReadingCompletion,用于讀取配置為位置的標簽

接下來,將以下屬性和方法添加到NFCUtility

// 1
private var session: NFCNDEFReaderSession?
private var completion: LocationReadingCompletion?

// 2
static func performAction(
  _ action: NFCAction,
  completion: LocationReadingCompletion? = nil
) {
  // 3
  guard NFCNDEFReaderSession.readingAvailable else {
    completion?(.failure(NFCError.unavailable))
    print("NFC is not available on this device")
    return
  }

  shared.action = action
  shared.completion = completion
  // 4
  shared.session = NFCNDEFReaderSession(
    delegate: shared.self,
    queue: nil,
    invalidateAfterFirstRead: false)
  // 5
  shared.session?.alertMessage = action.alertMessage
  // 6
  shared.session?.begin()
}

如果您由于不符合NFCNDEFReaderSessionDelegate而此時遇到編譯錯誤,請不要擔心,您將立即修復此問題。

這是您剛剛做的:

  • 1) 您添加sessioncompletion屬性以存儲活動的NFC reading session及其完成塊(completion block)。
  • 2) 添加靜態(tài)函數(shù)作為NFC讀寫任務的入口點。通常,您將使用單例樣式訪問此函數(shù)和NFCUtility。
  • 3) 確保設備支持NFC讀取。否則,請返回errorcomplete。
  • 4) 創(chuàng)建一個NFCNDEFReaderSession,它代表活動的閱讀會話。您還可以設置代表以通知NFC閱讀會話的各種事件。
  • 5) 您可以在會話上設置alertMessage屬性,以使其在NFC模式下向用戶顯示該文本。
  • 6) 開始閱讀會話。調(diào)用時,模態(tài)將向用戶呈現(xiàn)您在上一步中設置的所有指令。

1. Understanding NDEF

請注意,上面的代碼引入了另一個首字母縮寫詞NDEF,代表NFC Data Exchange Format。這是用于寫入或讀取NFC設備的標準格式。您將使用兩種NDEF:

  • NDEF Record:其中包含您的有效載荷(payload)值,例如字符串,URL或自定義數(shù)據(jù)。它還包含有關(guān)該有效負載值的信息,例如長度和類型。此信息是CoreNFC中的NFCNDEFPayload。
  • NDEF Message:這是保存NDEF記錄的數(shù)據(jù)結(jié)構(gòu)。 NDEF消息中可以有一個或多個NDEF記錄。

2. Detecting Tags

現(xiàn)在,您已經(jīng)設置了NFCReaderSession,現(xiàn)在遵循NFCUtility成為代理了,這樣就可以通知您在讀取會話期間發(fā)生的各種事件。

將以下代碼添加到NFCUtility.swift的底部:

// MARK: - NFC NDEF Reader Session Delegate
extension NFCUtility: NFCNDEFReaderSessionDelegate {
  func readerSession(
    _ session: NFCNDEFReaderSession,
    didDetectNDEFs messages: [NFCNDEFMessage]
  ) {
    // Not used
  }
}

您將在一秒鐘內(nèi)向此擴展添加更多內(nèi)容,但請注意,在本教程中,您將不會對readerSession(_:didDetectNDEFs :)進行任何操作。 您僅在此處添加它,因為必須遵守委托協(xié)議。

NFC技術(shù)的互動越多,您越會發(fā)現(xiàn)在讀寫過程的各個階段遇到錯誤的可能性。 將以下方法添加到新擴展中以捕獲這些錯誤:

private func handleError(_ error: Error) {
  session?.alertMessage = error.localizedDescription
  session?.invalidate()
}

代碼的第一行應該看起來很熟悉。 它將在NFC模式視圖中向用戶顯示錯誤消息。 如果發(fā)生錯誤,您還將使會話無效以終止會話并允許用戶再次與該應用進行交互。

接下來,將以下方法添加到擴展中以處理NFC讀取會話中的錯誤:

func readerSession(
  _ session: NFCNDEFReaderSession,
  didInvalidateWithError error: Error
) {
  if let error = error as? NFCReaderError,
    error.code != .readerSessionInvalidationErrorFirstNDEFTagRead &&
      error.code != .readerSessionInvalidationErrorUserCanceled {
    completion?(.failure(NFCError.invalidated(message: 
      error.localizedDescription)))
  }

  self.session = nil
  completion = nil
}

添加此委托方法將清除到目前為止您遇到的所有編譯錯誤。

最后,將最后一種方法添加到您的擴展中,以處理可能的NFC標簽檢測:

func readerSession(
  _ session: NFCNDEFReaderSession,
  didDetect tags: [NFCNDEFTag]
) {
  guard 
    let tag = tags.first,
    tags.count == 1 
    else {
      session.alertMessage = """
        There are too many tags present. Remove all and then try again.
        """
      DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(500)) {
        session.restartPolling()
      }
      return
  }
}

在這里,您可以實現(xiàn)在會話檢測到您掃描標簽時將調(diào)用的方法。

通常,您希望用戶只有一個標簽離手機足夠近,但是您應該考慮多個標簽。 如果檢測到此情況,則將停止掃描并alert給用戶。 顯示該消息后,您將重新啟動閱讀會話,并讓您的用戶再試一次。

3. Handling the Tag

知道有一個標簽后,您可能想對它做些事情。 在readerSession(_:didDetect :)中的guard聲明之后添加以下代碼:

// 1
session.connect(to: tag) { error in
  if let error = error {
    self.handleError(error)
    return
  }

  // 2
  tag.queryNDEFStatus { status, _, error in
    if let error = error {
      self.handleError(error)
      return
    }

    // 3
    switch (status, self.action) {
    case (.notSupported, _):
      session.alertMessage = "Unsupported tag."
      session.invalidate()
    case (.readOnly, _):
      session.alertMessage = "Unable to write to tag."
      session.invalidate()
    case (.readWrite, .setupLocation(let locationName)):
      self.createLocation(name: locationName, with: tag)
    case (.readWrite, .readLocation):
      return
    default:
      return
    }
  }
}

您在上面的代碼中正在做的事情是:

  • 1) 使用當前的NCFNDEFReaderSession連接到檢測到的標簽。您需要執(zhí)行此步驟以執(zhí)行對標簽的任何讀取或?qū)懭搿_B接后,它將調(diào)用其完成處理程序,并可能發(fā)生任何錯誤。
  • 2) 在標簽中查詢其NDEF狀態(tài),以查看是否支持NFC設備。就您的NeatoCache應用而言,狀態(tài)必須為readWrite
  • 3) 切換狀態(tài)和NFC操作,并根據(jù)其值確定應執(zhí)行的操作。在這里,您嘗試使用createLocation(name:with :)將標簽設置為具有位置名稱,該名稱尚不存在,因此會遇到編譯錯誤。不用擔心,您稍后會添加它。同樣,readLocation操作也尚未處理。

4. Creating the Payload

到目前為止,您已經(jīng)在尋找標簽,連接標簽并查詢其狀態(tài)。要完成對標簽的寫入設置,請在NFCUtility.swift的末尾添加以下代碼塊:

// MARK: - Utilities
extension NFCUtility {
  func createLocation(name: String, with tag: NFCNDEFTag) {
    // 1
    guard let payload = NFCNDEFPayload
      .wellKnownTypeTextPayload(string: name, locale: Locale.current) 
      else {
        handleError(NFCError.invalidated(message: "Could not create payload"))
        return
    }

    // 2
    let message = NFCNDEFMessage(records: [payload])

    // 3
    tag.writeNDEF(message) { error in
      if let error = error {
        self.handleError(error)
        return
      }

      self.session?.alertMessage = "Wrote location data."
      self.session?.invalidate()
      self.completion?(.success(Location(name: name)))
    }
  }
}

您在上面的代碼中正在做的事情是:

  • 1) 創(chuàng)建文本NFCNDEFPayload。如前所述,這類似于NDEF記錄。
  • 2) 使用有效負載創(chuàng)建新的NFCNDEFMessage,以便可以將其保存到NFC設備。
  • 3) 最后,將消息寫入標簽。

5. Using NDEF Payload Types

NFCNDEFPayload支持幾種不同類型的數(shù)據(jù)。在此示例中,您使用的是wellKnownTypeTextPayload(string:locale :)。這是一種非常簡單的數(shù)據(jù)類型,它使用字符串和設備的當前語言環(huán)境。其他一些數(shù)據(jù)類型包含更復雜的信息。完整清單如下:

  • Empty
  • Well-Known
  • MIME media-type
  • Absolute URI
  • External
  • Unknown
  • Unchanged
  • Reserved

注意:本教程涵蓋了Well-KnownUnknown。要了解其他類型,請查看本教程末尾列出的鏈接。

另請注意,類型可以具有子類型。例如,Well-known的具有TextURI的子類型。

您真的很接近!剩下的就是將用戶界面連接到新代碼。轉(zhuǎn)到AdminView.swift并替換以下代碼:

Button(action: {
}) {
  Text("Save Location…")
}
.disabled(locationName.isEmpty)

使用

Button(action: {
  NFCUtility.performAction(.setupLocation(locationName: self.locationName)) { _ in
    self.locationName = ""
  }
}) {
  Text("Save Location…")
}
.disabled(locationName.isEmpty)

這將進行調(diào)用,以使用在text field中找到的文本來設置您的位置。

構(gòu)建并運行,切換到應用程序的Admin選項卡,輸入名稱并選擇Save Location…。

您會看到以下內(nèi)容:

注意:請記住,您需要使用物理設備并具有支持寫入功能的NFC標簽。

將手機放在NFC標簽上后,您會看到一條消息,說明您的位置已成功保存。

6. Reading the Tag

很好! 現(xiàn)在,您已經(jīng)有了一個可以在標簽中寫入字符串的應用程序,您就可以為讀取標簽提供支持。 返回NFCUtility.swift并在readerSession(_:didDetect :)中找到以下代碼。

case (.readWrite, .readLocation):
  return

現(xiàn)在,替換它使用下面:

case (.readWrite, .readLocation):
  self.readLocation(from: tag)

是時候?qū)崿F(xiàn)該readLocation(from :)方法了。 將以下內(nèi)容添加到包含createLocation(name:with :)Utilities擴展中:

func readLocation(from tag: NFCNDEFTag) {
  // 1
  tag.readNDEF { message, error in
    if let error = error {
      self.handleError(error)
      return
    }
    // 2
    guard 
      let message = message,
      let location = Location(message: message) 
      else {
        self.session?.alertMessage = "Could not read tag data."
        self.session?.invalidate()
        return
    }
    self.completion?(.success(location))
    self.session?.alertMessage = "Read tag."
    self.session?.invalidate()
  }
}

您對添加的內(nèi)容應該有點熟悉,因為它與您寫入標簽的方式非常相似。

  • 1) 首先,您開始讀取標簽。 如果可以讀取,它將返回找到的所有消息。
  • 2) 接下來,如果有,嘗試從消息數(shù)據(jù)中創(chuàng)建一個Location。 這使用了一個接受NFCNDEFMessage并將其命名的自定義初始化程序。 如果您感到好奇,可以在LocationModel.swift中找到該初始化程序。

最后,打開VisitorView.swift,并在scanSection中,替換以下代碼:

Button(action: {
}) {
  Text("Scan Location Tag…")
}

使用下面

Button(action: {
  NFCUtility.performAction(.readLocation) { location in
    self.locationModel = try? location.get()
  }
}) {
  Text("Scan Location Tag…")
}

您已經(jīng)準備好從標簽中讀取數(shù)據(jù)。 構(gòu)建并運行。

Visitors選項卡上,點擊Scan Location Tag…。 您會在用戶界面中看到以下內(nèi)容以及您的位置名稱:


Writing Different Data Types

盡管在某些情況下寫字符串可能會完美地工作,但您可能會發(fā)現(xiàn)想要將其他類型的數(shù)據(jù)寫到標簽中。

為此,請在Utilities擴展中的NFCUtility.swift中添加以下內(nèi)容:

private func read(
  tag: NFCNDEFTag,
  alertMessage: String = "Tag Read",
  readCompletion: NFCReadingCompletion? = nil
) {
  tag.readNDEF { message, error in
    if let error = error {
      self.handleError(error)
      return
    }

    // 1
    if let readCompletion = readCompletion,
       let message = message {
      readCompletion(.success(message))
    } else if 
      let message = message,
      let record = message.records.first,
      let location = try? JSONDecoder()
        .decode(Location.self, from: record.payload) {
      // 2
      self.completion?(.success(location))
      self.session?.alertMessage = alertMessage
      self.session?.invalidate()
    } else {
      self.session?.alertMessage = "Could not decode tag data."
      self.session?.invalidate()
    }
  }
}

從現(xiàn)在開始,這種讀取標簽的新方法將成為您大多數(shù)活動的切入點。 如您所見,它仍然像以前一樣讀取標簽。 但是,一旦讀取標簽,它將執(zhí)行以下兩項操作之一:

  • 1) 調(diào)用completion handler并將消息傳遞給它。 這對于將多個NFC任務鏈接在一起非常有用。
  • 2) 解碼有效負載(payload),以便您可以解析標簽的記錄。 您將稍后再講到這一點。

1. Writing Custom Data Instead of Strings

此時,您已經(jīng)準備好將應用程序從編寫字符串到標簽轉(zhuǎn)換為將自定義數(shù)據(jù)寫入標簽。 將以下內(nèi)容添加到Utilities擴展中:

private func createLocation(_ location: Location, tag: NFCNDEFTag) {
  read(tag: tag) { _ in
    self.updateLocation(location, tag: tag)
  }
}

這是用于創(chuàng)建帶有位置標簽的新函數(shù)。 您可以看到它使用新的read(tag:alsertMessage:readCompletion :)啟動該過程,并調(diào)用了一個新函數(shù)來更新標簽上的位置,還調(diào)用了一個新的暫時實施updateLocation(_:tag :)方法。

由于您要替換將位置信息寫入標簽的方式,因此請刪除NFCUtility擴展程序開頭的createLocation(name:with :),因為不再需要它。 另外,從以下代碼在readerSession(_:didDetect :)中更新代碼:

case (.readWrite, .setupLocation(let locationName)):
  self.createLocation(name: locationName, with: tag)

到下面

case (.readWrite, .setupLocation(let locationName)):
  self.createLocation(Location(name: locationName), tag: tag)

createLocation(_:tag:)后面添加這個方法:

private func updateLocation(
  _ location: Location,
  withVisitor visitor: Visitor? = nil,
  tag: NFCNDEFTag
) {
  // 1
  var alertMessage = "Successfully setup location."
  var tempLocation = location
  
  // 2
  let jsonEncoder = JSONEncoder()
  guard let customData = try? jsonEncoder.encode(tempLocation) else {
    self.handleError(NFCError.invalidated(message: "Bad data"))
    return
  }
  // 3
  let payload = NFCNDEFPayload(
    format: .unknown,
    type: Data(),
    identifier: Data(),
    payload: customData)
  // 4
  let message = NFCNDEFMessage(records: [payload])
}

您在上面的代碼中正在做的事情是:

  • 1) 創(chuàng)建默認alert消息和臨時位置。 稍后您將回到這些內(nèi)容。
  • 2) 對傳遞給函數(shù)的Location結(jié)構(gòu)進行編碼。 這會將模型轉(zhuǎn)換為原始數(shù)據(jù)(Data)。 這很重要,因為這是將任何自定義類型寫入NFC標簽的方式。
  • 3) 創(chuàng)建可以處理數(shù)據(jù)的有效負載(payload)。 但是,您現(xiàn)在使用unknown作為格式。 這樣做時,必須將typeidentifier設置為空數(shù)據(jù)Data,而有效負載(payload)參數(shù)將承載實際的解碼模型。
  • 4) 將有效負載添加到新創(chuàng)建的消息中。

總體而言,這似乎與將字符串保存到標簽中時沒什么不同,只是增加了一個步驟,將Swift數(shù)據(jù)類型轉(zhuǎn)換為標簽可理解的內(nèi)容。

2. Checking Tag Capacity

要完成將數(shù)據(jù)寫入標簽,請在updateLocation(_:withVisitor:tag)中添加下一個代碼塊:

tag.queryNDEFStatus { _, capacity, _ in
  // 1
  guard message.length <= capacity else {
    self.handleError(NFCError.invalidPayloadSize)
    return
  }

  // 2
  tag.writeNDEF(message) { error in
    if let error = error {
      self.handleError(error)
      return
    }
    
    if self.completion != nil {
      self.read(tag: tag, alertMessage: alertMessage)
    }
  }
}

上面的閉包嘗試查詢當前NDEF狀態(tài),然后:

  • 1) 確保設備有足夠的存儲空間來存儲位置。 請記住,與您可能熟悉的設備相比,NFC標簽通常具有極其有限的存儲容量。
  • 2) 將消息寫入標簽。

與上面一樣,構(gòu)建并運行并設置位置。 如果愿意,可以使用之前的相同標簽,因為新代碼將覆蓋以前保存的所有數(shù)據(jù)。

3. Reading Your Custom Data

此時,如果您嘗試讀取標簽,將會收到錯誤消息。 保存的數(shù)據(jù)不再是眾所周知的類型。 要解決此問題,請在readerSession(_:didDetect :)中替換以下代碼:

case (.readWrite, .readLocation):
  self.readLocation(from: tag)

使用

case (.readWrite, .readLocation):
  self.read(tag: tag)

構(gòu)建并運行并掃描標簽。 因為您在沒有任何completion塊的情況下調(diào)用read(tag:alertMessage:readCompletion :),所以它將解碼消息的第一條記錄中找到的數(shù)據(jù)。


Modifying Content

此應用程序的最后一項要求是保存訪問此位置的人員的日志。 您的應用具有UI中已經(jīng)存在的未使用功能,該功能允許用戶輸入其名稱并將其添加到標簽中。 到目前為止,您所做的工作將使其余的設置變得微不足道。 您已經(jīng)可以將數(shù)據(jù)讀取和寫入標簽,因此對其進行修改應該很容易。

在創(chuàng)建tempLocation之后,在NFCUtility.swift中,將此代碼添加到updateLocation(_:withVisitor:tag :)

if let visitor = visitor {
  tempLocation.visitors.append(visitor)
  alertMessage = "Successfully added visitor."
}

在上面的代碼中,您檢查是否提供了visitor。 如果是這樣,則將其添加到該位置的Visitors數(shù)組中。

接下來,將以下方法添加到Utilities擴展中:

private func addVisitor(_ visitor: Visitor, tag: NFCNDEFTag) {
  read(tag: tag) { message in
    guard 
      let message = try? message.get(),
      let record = message.records.first,
      let location = try? JSONDecoder()
        .decode(Location.self, from: record.payload) 
      else {
        return
    }

    self.updateLocation(location, withVisitor: visitor, tag: tag)
  }
}

這個新方法將讀取一個標簽,從中獲取消息,并嘗試對標簽上的Location進行解碼。

接下來,在readerSession(_:didDetect :)中,向switch語句添加新的case

case (.readWrite, .addVisitor(let visitorName)):
  self.addVisitor(Visitor(name: visitorName), tag: tag)

如果用戶明確想要添加visitor,則將調(diào)用在上一步中添加的函數(shù)。

剩下的就是更新VisitorView.swift。 在visitorSection中,替換以下代碼:

Button(action: {
}) {
  Text("Add To Tag…")
}
.disabled(visitorName.isEmpty)

使用

Button(action: {
  NFCUtility
    .performAction(.addVisitor(visitorName: self.visitorName)) { location in
      self.locationModel = try? location.get()
      self.visitorName = ""
    }
}) {
  Text("Add To Tag…")
}
.disabled(visitorName.isEmpty)

Build并運行,然后轉(zhuǎn)到Visitors選項卡。 輸入您的姓名,然后選擇Add To Tag…。 掃描后,您將看到更新的位置以及在標記上找到的訪問者列表。

現(xiàn)在,您應該熟悉Core NFC的基礎知識。 框架中還有很多事情沒有在本教程中提到。 例如,您可以添加標簽的背景閱讀,這可以為用戶提供一種無需打開應用即可與您的應用進行交互的方式。 如果您使用Apple的Shortcuts應用程序來自動化您的智能家居設備,這對您來說應該很熟悉。 您可以在此處找到有關(guān)此操作的更多信息:Adding Support for Background Tag Reading。

要了解更多信息,請查看以下一些重要資源:

Apple's Core NFC Documentation是NFC規(guī)范中Apple支持的所有內(nèi)容的首選資源。

NFC Forum Homepage是放置有關(guān)NFC所需的所有一般信息及其定義的規(guī)范的地方。

后記

本篇主要講述了CoreNFC使用簡單示例,感興趣的給個贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容