DateFormatter性能深度優(yōu)化(譯文)

此文章為本人翻譯的譯文,版權(quán)為原作者所有。
英文原文:Parsing Dates: When Performance Matters

不久前,我在App開發(fā)中遇到了一些性能問題。 這個App需要處理數(shù)千個JSON對象,并將它們存儲在Core Data數(shù)據(jù)庫中 - 這不是一項微不足道的任務(wù),能都理解會有性能問題。 但是多達30秒的處理時間超出了可接受的范圍。

在用Instruments的Time Profiler工具測試之后,我驚奇地發(fā)現(xiàn)大約一半的處理時間用于解析日期,所以我的任務(wù)是提高日期解析性能。

My initial, naive approach

每個JSON對象可能有幾個日期,格式為ISO 8601字符串。 對于每個對象,使用DateFormatter將日期字符串轉(zhuǎn)換為Date對象,然后使用Core Data存儲這些對象。 我正在為每個對象創(chuàng)建一個新的日期格式轉(zhuǎn)換方法,類似于以下:

for dateString in lotsOfDateStrings {

  let formatter = NSDateFormatter()
  formatter.format = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
  let date = formatter.date(from: dateString)
  doStuff(with: date)
}

你可能不認(rèn)為創(chuàng)建一個DateFormatter對象會非常昂貴,但是你錯了:創(chuàng)建一次DateFormatter并重新使用它會帶來很大的性能提升。

首先,它到底有多快? 格式化100,000個日期字符串的簡單測試發(fā)現(xiàn)以下內(nèi)容:

Formatter Creation Time
Naive 10.88 seconds
Once 4.15 seconds

哇 - 這是一個很大的進步。 不足以完全解決性能問題,但足以說明DateFormatter創(chuàng)建成本很高。

Caching date formatters

我建議使用類似以下方式作為創(chuàng)建任何DateFormatter的默認(rèn)方式。 使用此方法創(chuàng)建的DateFormatter都會自動緩存以供日后使用,即使對于UITableView單元重用等更復(fù)雜的情況也是如此。 要使用它,只需調(diào)用DateFormatter.cached(withFormat:“<date format>”) - 沒有比這更容易了。

private var cachedFormatters = [String : DateFormatter]()

extension DateFormatter {

  static func cached(withFormat format: String) -> DateFormatter {
    if let cachedFormatter = cachedFormatters[format] { return cachedFormatter }
    let formatter = DateFormatter()
    formatter.dateFormat = format
    cachedFormatters[format] = formatter
    return formatter
  }
}

Faster, but not fast enough

盡管有很大改進,但速度提升2倍還不夠。經(jīng)過一番研究,我發(fā)現(xiàn)iOS 10有一個新的日期格式化類,ISO8601DateFormatter ......很好!不幸的是iOS 9支持是必須的,但是讓我們看看它與普通的舊DateFormatter相比如何。

使用100,000個日期字符串運行相同的測試出需要4.19秒,這比DateFormatter慢一點,但僅僅是。如果你支持iOS 10+并且性能不是問題,你應(yīng)該仍然可以使用這個新類,盡管速度會有輕微降低 - 它可能會更徹底地處理所有可能的ISO 8601標(biāo)準(zhǔn)。

strptime() - don’t be fooled

對替代日期解析解決方案的更多研究獲得了一個有趣的函數(shù):strptime()。 它是一個舊的C函數(shù),用于低級日期解析,完成我們需要的所有格式化說明符。 它可以直接在Swift中使用,你可以按如下方式使用它。

func parse(dateString: String) -> Date? {

  var time: time_t
  var timeComponents: tm = tm(tm_sec: 0, tm_min: 0, tm_hour:
    0, tm_mday: 0, tm_mon: 0, tm_year: 0, tm_wday: 0, tm_yday:
    0, tm_isdst: 0, tm_gmtoff: 0, tm_zone: nil)
  guard let cDateString = dateString.cString(using: .utf8) else { return nil }
  strptime(cDateString, "%Y-%m-%dT%H:%M:%S%z", &timeComponents)
  return Date(timeIntervalSince1970: Double(mktime(&timeComponents)))
}

看起來很完美,對吧? 嗯,我起初也這么認(rèn)為......長話短說:不要用它。 strptime()的Mac / iOS實現(xiàn)不能正確支持ISO 8601日期偏移所需的%z格式說明符,并且它在夏令時方面存在問題。 這很快,但是對mktime()的調(diào)用減慢了一點 - 上面的代碼最終速度是以前的兩倍。 在糾正時區(qū)偏移后,此代碼實際上已進入App Store,直到開始出現(xiàn)夏令時問題。 你可以通過手動校正當(dāng)前時間和給定時區(qū)之間的夏令時差異來使用它...唉,有更好,更快的方式,所以不需要這樣做。

vsscanf()

最終的解決方案使用另一個從sscanf()派生的C函數(shù)vsscanf()。

vsscanf()速度很快,但我花了一些時間搞清楚如何將其轉(zhuǎn)換為Date而不會影響性能。 讓我們直截了當(dāng):

class ISO8601DateParser {

  private static var calendarCache = [Int : Calendar]()
  private static var components = DateComponents()

  private static let year = UnsafeMutablePointer<Int>.allocate(capacity: 1)
  private static let month = UnsafeMutablePointer<Int>.allocate(capacity: 1)
  private static let day = UnsafeMutablePointer<Int>.allocate(capacity: 1)
  private static let hour = UnsafeMutablePointer<Int>.allocate(capacity: 1)
  private static let minute = UnsafeMutablePointer<Int>.allocate(capacity: 1)
  private static let second = UnsafeMutablePointer<Float>.allocate(capacity: 1)
  private static let hourOffset = UnsafeMutablePointer<Int>.allocate(capacity: 1)
  private static let minuteOffset = UnsafeMutablePointer<Int>.allocate(capacity: 1)

  static func parse(_ dateString: String) -> Date? {

    let parseCount = withVaList([year, month, day, hour, minute,
      second, hourOffset, minuteOffset], { pointer in
        vsscanf(dateString, "%d-%d-%dT%d:%d:%f%d:%dZ", pointer)
    })

    components.year = year.pointee
    components.minute = minute.pointee
    components.day = day.pointee
    components.hour = hour.pointee
    components.month = month.pointee
    components.second = Int(second.pointee)

    // Work out the timezone offset

    if hourOffset.pointee < 0 {
      minuteOffset.pointee = -minuteOffset.pointee
    }

    let offset = parseCount <= 6 ? 0 :
      hourOffset.pointee * 3600 + minuteOffset.pointee * 60

    // Cache calendars per timezone
    // (setting it each date conversion is not performant)

    if let calendar = calendarCache[offset] {
      return calendar.date(from: components)
    }

    var calendar = Calendar(identifier: .gregorian)
    guard let timeZone = TimeZone(secondsFromGMT: offset) else { return nil }
    calendar.timeZone =  timeZone
    calendarCache[offset] = calendar
    return calendar.date(from: components)

  }

}

這可以在0.67秒內(nèi)解析100,000個日期字符串 - 幾乎比原始方法快20倍,比使用緩存DateFormatter快6倍。

補充

另外我也看到兩篇DateFormatter性能探討的文章,可以配合著看
[性能優(yōu)化]DateFormatter輕度優(yōu)化探索
[性能優(yōu)化]DateFormatter深度優(yōu)化探索

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

相關(guān)閱讀更多精彩內(nèi)容

  • SwiftDate概況 從Swift發(fā)布起,我們就沒有放棄使用Swift。 當(dāng)然,我們希望在項目能夠輕松自如地管理...
    Mee_Leo閱讀 10,336評論 1 13
  • 第5章 引用類型(返回首頁) 本章內(nèi)容 使用對象 創(chuàng)建并操作數(shù)組 理解基本的JavaScript類型 使用基本類型...
    大學(xué)一百閱讀 3,679評論 0 4
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,658評論 1 32
  • 處理日期的常見情景 NSDate -> String & String -> NSDate 日期比較 日期計算(基...
    KAKA_move閱讀 914評論 0 0
  • 荒蕪中一個盹醒來 貓的腳蹼踩入清晨 鈴鐺晃著我,就像整整六年 單車駛?cè)雺艟车挠乃{ 注視著背影剔透 讓白膠和那個風(fēng)塵...
    長舒影閱讀 428評論 2 11

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