DateFormatter性能深度优化(译文)
此文章为本人翻译的译文,版权为原作者所有。
英文原文:Parsing Dates: When Performance Matters
不久前,我在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 Creation | Time |
---|---|
Naive | 10.88 seconds |
Once | 4.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深度优化探索