[性能优化]DateFormatter深度优化探索

前言

在iOS开发中,对日期进行格式化处理一般有三个步骤:git

  • 建立DateFormatter对象
  • 设置日期格式
  • 使用DateFormatter对象对日期进行处理

在上篇文章《DateFormatter性能优化》中,咱们经过建立单例对象的方式对建立DateFormatter对象,设置日期格式两个步骤进行了缓存,将方法耗时下降为不缓存的方案的10%左右,可是这种优化方法受制于DateFormatter的几个系统方法的执行效率,自己具备必定的局限性。以前在一些文章中,也看到了使用C语言的github

size_t	 strftime_l(char * __restrict, size_t, const char * __restrict,
		const struct tm * __restrict, locale_t)
		__DARWIN_ALIAS(strftime_l) __strftimelike(3);
复制代码

函数对日期格式化进行处理,因此本文将对如下几种状况的方法耗时进行评测:面试

  • 使用Objective-C,不缓存DateFormatter对象
  • 使用Objective-C,缓存DateFormatter对象
  • 使用Objective-C,调用strftime_l作日期处理
  • 使用Swift,不缓存DateFormatter对象
  • 使用Swift,缓存DateFormatter对象
  • 使用Swift,调用strftime_l作日期处理

Objective-C的三种状况下的代码

//不缓存DateFormatter对象
-(void)testDateFormatterInOCWithoutCache:(NSInteger)times {
    NSString *string = @"";
    NSDate *date;
    CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
    for (int i=0; i<times; i++) {
        NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
        [dateFormatter setDateFormat:@"yyyy年MM月dd日HH时mm分ss秒"];
        date = [NSDate dateWithTimeIntervalSince1970:(1545308405 + i)];
        string = [dateFormatter stringFromDate:date];
    }
    CFAbsoluteTime duration = (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0;
    NSLog(@"\n不缓存DateFormatter对象的方案:\n计算%ld次\n耗时%f ms\n", (long)times, duration);
}

//缓存DateFormatter对象
-(void)testDateFormatterInOCWithCache:(NSInteger)times {
    NSString *string = @"";
    NSDate *date;
    CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
    for (int i=0; i<times; i++) {
        date = [NSDate dateWithTimeIntervalSince1970:(1545308405 + i)];
        string = [[DateFormatterCache shareInstance].formatterOne stringFromDate:date];
    }
    CFAbsoluteTime duration = (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0;
    NSLog(@"\n缓存DateFormatter对象的方案:\n计算%ld次\n耗时%f ms\n", (long)times, duration);
}

//使用C语言来作日期处理
-(void)testDateFormatterInC:(NSInteger)times {
    NSString *string = @"";
    NSDate *date;
    time_t timeInterval;
    char buffer[80];
    CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
    for (int i=0; i<times; i++) {
        date = [NSDate dateWithTimeIntervalSince1970:(1545308405 + i)];
        timeInterval = [date timeIntervalSince1970];
        strftime(buffer, sizeof(buffer), "%Y年%m月%d日%H时%M分%S秒", localtime(&timeInterval));
        string = [NSString stringWithCString:buffer encoding:NSUTF8StringEncoding];
    }
    CFAbsoluteTime duration = (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0;
    NSLog(@"==%@", string);
    NSLog(@"\n使用C语言的方案:\n计算%ld次\n耗时%f ms\n", (long)times, duration);
}

复制代码

这里对于我们iOS开发的同窗来讲比较陌生的就是 strftime_l(buffer, sizeof(buffer), "%Y年%m月%d日%H时%M分%S秒", localtime(&timeInterval), NULL);这这行代码的调用,strftime_l函数接受四个参数,第一个参数buffer是C语言中字符数组用于存储日期格式化后的字符串,第二个参数是写入buffer数组的最大值,若是格式化的字符串大于这个值,那么只会取字符串的的一部分,第三个参数"%Y年%m月%d日%H时%M分%S秒"是日期格式,第四个参数localtime(&timeInterval)是指向使用当地时区对时间戳处理获得tm类型结构体的指针swift

附上tm结构体:数组

struct tm {
	int	tm_sec;		/* seconds after the minute [0-60] */
	int	tm_min;		/* minutes after the hour [0-59] */
	int	tm_hour;	/* hours since midnight [0-23] */
	int	tm_mday;	/* day of the month [1-31] */
	int	tm_mon;		/* months since January [0-11] */
	int	tm_year;	/* years since 1900 */
	int	tm_wday;	/* days since Sunday [0-6] */
	int	tm_yday;	/* days since January 1 [0-365] */
	int	tm_isdst;	/* Daylight Savings Time flag */
	long	tm_gmtoff;	/* offset from UTC in seconds */
	char	*tm_zone;	/* timezone abbreviation */
};
复制代码

Swift三种状况下的代码

//不进行缓存
    func testInOldWay(_ times: Int) {
        var string = ""
        var date = Date.init()
        let startTime = CFAbsoluteTimeGetCurrent();
        for i in 0..<times {
            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy年MM月dd日HH时mm分ss秒"
            date = Date.init(timeIntervalSince1970: TimeInterval(1545308405 + i))
            string = formatter.string(from: date)
        }
        let duration = (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0;
        print("使用oldWay计算\n\(times)次,总耗时\n\(duration) ms\n")
    }
    //进行缓存
    func testInNewWay(_ times: Int) {
        var string = ""
        var date = Date.init()
        let startTime = CFAbsoluteTimeGetCurrent();
        for i in 0..<times {
            date = Date.init(timeIntervalSince1970: TimeInterval(1545308405 + i))
            string = DateFormatterCache.shared.dateFormatterOne.string(from: date)
        }
        let duration = (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0;
        print("使用缓存Formatter的方案计算\n\(times)次,总耗时\n\(duration) ms\n")
    }
    
    //使用C语言来作日期处理
    func testFormatterInC(_ times: Int) {
        var date = Date.init()
        var dateString = ""
        var buffer = [Int8](repeating: 0, count: 100)
        var time = time_t(date.timeIntervalSince1970)
        let format = "%Y年%m月%d日%H时%M分%S秒"
        let startTime = CFAbsoluteTimeGetCurrent();
        for i in 0..<times {
            date = Date.init(timeIntervalSince1970: TimeInterval(1545308405 + i))
            time = time_t(date.timeIntervalSince1970)
            strftime(&buffer, buffer.count, format, localtime(&time))
            dateString = String.init(cString: buffer, encoding: String.Encoding.utf8) ?? ""
        }
        let duration = (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0;
        print("使用C语言的方案计算\n\(times)次,总耗时\n\(duration) ms\n")
        print(dateString)
    }
复制代码

iOS 12.1 iPhone 7缓存

测试结果:性能优化

测试结果:bash

在Objective-C中, 不使用缓存,使用缓存,使用C语言函数处理的耗时比约为100:10.7:3.5微信

在Swift中, 不使用缓存,使用缓存,使用C语言函数处理的耗时比约为100:11.7:6.6app

Swift在使用DateFormatter进行处理时,不管是缓存的方案仍是不缓存的方案,跟使用Objective-C的耗时基本一致,而在Swift中使用C语言的函数来作日期处理时,时间约为使用Objective-C的两倍,并且当只作一第二天期处理时,因为涉及到一些初始资源的初始化,因此看上去比后面执行10次的时间还多

最后

若是项目是Objective-C的项目,我以为能够采用这种C语言的strftime来作日期处理,能将时间下降为缓存NSDateFormatter的方案的33%左右,若是是Swift项目,调用C语言函数的效率没有在Objective-C项目中那么高,虽然能将时间下降为缓存NSDateFormatter的方案的56%左右,可是在Swift中使用C语言的函数存在必定的风险,在这里风险之一就是time = time_t(date.timeIntervalSince1970)这行代码返回的值是time_t类型,time_t类型的定义以下:

public typealias time_t = __darwin_time_t
public typealias __darwin_time_t = Int /* time() */
复制代码

time_t其实就是Int,当Swift项目运行在32位设备(也就是iphone 5,iphone 5C)上时,Int类型是32位的,最大值为2147483647,若是这是一个时间戳的值,转换为正常时间是2038-01-19 11:14:07,也就是处理的时间是将来的日期,2038年之后的话,会出现数值溢出。

Demo在这里: github.com/577528249/S…

参考资料:

forums.developer.apple.com/thread/2905… stackoverflow.com/questions/2…

PS:

最近加了一些iOS开发相关的QQ群和微信群,可是感受都比较水,里面对于技术的讨论比较少,因此本身建了一个iOS开发进阶讨论群,欢迎对技术有热情的同窗扫码加入,加入之后你能够获得:

1.技术方案的讨论,会有在大厂工做的高级开发工程师尽量抽出时间给你们解答问题

2.每周按期会写一些文章,而且转发到群里,你们一块儿讨论,也鼓励加入的同窗积极得写技术文章,提高本身的技术

3.若是有想进大厂的同窗,里面的高级开发工程师也能够给你们内推,而且针对性得给出一些面试建议

群已经满100人了,想要加群的小伙伴们能够扫码加这个微信,备注:“加群+昵称”,拉你进群,谢谢了