此文章為本人翻譯的譯文,版權(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)化探索