DateFormatter性能深度优化(译文)

此文章为本人翻译的译文,版权为原作者所有。 英文原文:Parsing Dates: When Performance Matters

简书同步地址 www.jianshu.com/p/6fdb4631b…

不久前,我在App开发中遇到了一些性能问题。 这个App需要处理数千个JSON对象,并将它们存储在Core Data数据库中 - 这不是一项微不足道的任务,能都理解会有性能问题。 但是多达30秒的处理时间超出了可接受的范围。

在用Instruments的Time Profiler工具测试之后,我惊奇地发现大约一半的处理时间用于解析日期,所以我的任务是提高日期解析性能。

My initial, naive approach

每个JSON对象可能有几个日期,格式为ISO 8601字符串。 对于每个对象,使用DateFormatter将日期字符串转换为Date对象,然后使用Core Data存储这些对象。 我正在为每个对象创建一个新的日期格式转换方法,类似于以下:

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)
}
复制代码

你可能不认为创建一个DateFormatter对象会非常昂贵,但是你错了:创建一次DateFormatter并重新使用它会带来很大的性能提升。

首先,它到底有多快? 格式化100,000个日期字符串的简单测试发现以下内容:

Formatter CreationTime
Naive10.88 seconds
Once4.15 seconds

哇 - 这是一个很大的进步。 不足以完全解决性能问题,但足以说明DateFormatter创建成本很高。

Caching date formatters

我建议使用类似以下方式作为创建任何DateFormatter的默认方式。 使用此方法创建的DateFormatter都会自动缓存以供日后使用,即使对于UITableView单元重用等更复杂的情况也是如此。 要使用它,只需调用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倍还不够。经过一番研究,我发现iOS 10有一个新的日期格式化类,ISO8601DateFormatter ......很好!不幸的是iOS 9支持是必须的,但是让我们看看它与普通的旧DateFormatter相比如何。

使用100,000个日期字符串运行相同的测试出需要4.19秒,这比DateFormatter慢一点,但仅仅是。如果你支持iOS 10+并且性能不是问题,你应该仍然可以使用这个新类,尽管速度会有轻微降低 - 它可能会更彻底地处理所有可能的ISO 8601标准。

strptime() - don’t be fooled

对替代日期解析解决方案的更多研究获得了一个有趣的函数:strptime()。 它是一个旧的C函数,用于低级日期解析,完成我们需要的所有格式化说明符。 它可以直接在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)))
}

复制代码

看起来很完美,对吧? 嗯,我起初也这么认为......长话短说:不要用它。 strptime()的Mac / iOS实现不能正确支持ISO 8601日期偏移所需的%z格式说明符,并且它在夏令时方面存在问题。 这很快,但是对mktime()的调用减慢了一点 - 上面的代码最终速度是以前的两倍。 在纠正时区偏移后,此代码实际上已进入App Store,直到开始出现夏令时问题。 你可以通过手动校正当前时间和给定时区之间的夏令时差异来使用它...唉,有更好,更快的方式,所以不需要这样做。

vsscanf()

最终的解决方案使用另一个从sscanf()派生的C函数vsscanf()

vsscanf()速度很快,但我花了一些时间搞清楚如何将其转换为Date而不会影响性能。 让我们直截了当:

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秒内解析100,000个日期字符串 - 几乎比原始方法快20倍,比使用缓存DateFormatter快6倍。

补充

另外我也看到两篇DateFormatter性能探讨的文章,可以配合着看
[性能优化]DateFormatter轻度优化探索
[性能优化]DateFormatter深度优化探索

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值