记一次关于null_resettable的性能优化

最近在进行项目优化的时候遇到了关于null_resettable的坑,记录一下,由于之前代码不在了,简单模拟一下当时的情况.

最近项目要正式上线,需要进行一定的性能方面的测试,由于之前的数据加载的方案几经变化,都没有出现严重的性能问题,也没有在意,结果一测试,结果让我大跌眼镜:性能简直差到天边d(・`ω´・d*)!!!简单log一下看看那里耗时严重.

测试环境

硬件:iPhone5C,系统,iOS9.0.2(越狱)
网络:辣鸡WiFi…
没有模拟器,之前引入第三方蓝牙库,只有真机包,坑…,要不然直接Instruments查看了,不过当时预测不是大问题,就直接log查看了.

主要过程

思路:由于之前是采取分段加载数据,之后采取一次性加载数据,同时进行4个网络请求,可能在网络方面有耗时操作,包括请求数据,解析数据;另一个就是存在频繁调用方法的低性能,导致运行慢.
测试数据是血糖数据,数据时间跨度为两年,模拟数据3600条,主要包括空腹血糖数据,以及数据记录时间等,用于绘制曲线图,曲线图以四小时为单位进行绘制,可知共有365 * 2 * 6 = 4380个点需要绘制,其中包括大量的时间比较,因为在同一时间区间,比如4:00-8:00只允许有一个数据进行绘制,因此这里还有一个数据去重操作.大概过程理清之后,打印一下时间:

定位问题

简单看了一下时间分布,总共有18s,网络部分,网络请求7s,其中有接近1s的数据解析耗时;绘图部分,空腹血糖数据生成耗时10s.
耗时代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DHLog(@"-----------fbg数据开始------------");
__block NSMutableArray *fbgVals = [NSMutableArray array];
__block NSInteger didAddIndex = -1; // 标记是否被添加过
NSInteger listCount = (NSInteger)self.weekRecords.count;
for (NSInteger i = listCount-1; i >= 0; i--) {
// 从远到近取出时间
RecordBG *bg = self.weekRecords[i];
NSDate *currentDate = [bg.happenTime dotString2Date];
NSInteger xPosition = [DHChartTool getXAxisPointWithHour:currentDate.hour];
NSInteger index = [NSDate daysWithinEraFromDate:from toDate:currentDate] * 6 + xPosition + 6;
if (didAddIndex != index) {
didAddIndex = index;
[fbgVals addObject:[[ChartDataEntry alloc] initWithValue:bg.fbg xIndex:index data:bg]];
}
}
DHLog(@"-----------fbg数据OK------------");

按照以上思路,继续打印时间,最后定位问题代码

1
NSInteger index = [NSDate daysWithinEraFromDate:from toDate:currentDate] * 6 + xPosition + 6;

进入看一下这行代码的功能,主要用于计算两个日期之间的整数天.代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
+ (NSInteger)daysWithinEraFromDate:(NSDate *) startDate toDate:(NSDate *) endDate
{
NSCalendar *cal = [NSCalendar currentCalendar];
NSDate *s = [[startDate dateStringWithFormatString:@"yyyy-MM-dd"] string2DateWithFormat:@"yyyy-MM-dd"];
NSDate *e = [[endDate dateStringWithFormatString:@"yyyy-MM-dd"] string2DateWithFormat:@"yyyy-MM-dd"];
NSInteger startDay=[cal ordinalityOfUnit:NSDayCalendarUnit
inUnit: NSEraCalendarUnit forDate:s];
NSInteger endDay=[cal ordinalityOfUnit:NSDayCalendarUnit
inUnit: NSEraCalendarUnit forDate:e];
return (endDay-startDay);
}

看了一下这几行代码,[endDate dateStringWithFormatString:@"yyyy-MM-dd"]主要用于去掉日期的时分秒时间,[cal ordinalityOfUnit:NSDayCalendarUnit inUnit:NSEraCalendarUnit forDate:s]是系统提供的方法,进行单独测试,没发现问题.上面的方法实现如下:

1
2
3
4
5
- (NSString *)dateStringWithFormatString:(NSString *)formatString {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:formatString];
return [dateFormatter stringFromDate:self];
}

这里只是对系统方法进行简单调用,单看代码没问题,每个方法进入头文件看一下,其中一个关键字引起注意,想起了之前听说过NSDate有性能问题,当时没注意,现在看到这个关键字,猜测是这个属性引起的性能问题:

NSDateFormatter.h
@property (null_resettable, copy) NSString *dateFormat;
使用null_resettable修饰的属性,字面意义,不可重置的,官方默认使用这个关键字,就是告诉开发者尽量不要重置这个属性的值,因为重置需要重写set和get,防止为空的情况下没有默认值,好了,就是这个坑.

解决方案

定位到问题代码,优化考虑从两方面入手,一是避免调用这个方法,二是替换这个方法的实现,换用更好性能的实现.在原先的代码中,有很多地方调用[NSDate daysWithinEraFromDate: toDate:],还有很多地方调用日期转字符串的方法.
首先,把简单调用日期转字符串的方法改为字符串截取方法,比如,只需要获取年月日的地方可以这样调用:

1
NSString *dateStr = [[[[date dateByAddingDays:i] description] substringToIndex:10] stringByReplacingOccurrencesOfString:@"-" withString:@"/"];

这样就可以把日期转换为yyyy-MM-dd格式,注意,需要保证date的description返回标准格式,防止他人重写description带来隐患,
其次,把需要进行计算日期差的方法改为下面的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
+ (NSInteger)dh_daysWithinEraFromDate:(NSString *)startDate toDate:(NSString *)endDate
{
NSCalendar *cal = [NSCalendar currentCalendar];
// 截取年月日
NSString *start = [startDate substringToIndex:10];
NSString *end = [endDate substringToIndex:10];
static NSDateFormatter *formatter;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyy-MM-dd"];
[formatter setLocale:[NSLocale currentLocale]];
});
// 转成去掉时分秒的日期
NSDate *s = [formatter dateFromString:start];
NSDate *e = [formatter dateFromString:end];
NSInteger startDay=[cal ordinalityOfUnit:NSDayCalendarUnit
inUnit: NSEraCalendarUnit forDate:s];
NSInteger endDay=[cal ordinalityOfUnit:NSDayCalendarUnit
inUnit: NSEraCalendarUnit forDate:e];
return (endDay-startDay);
}

经过这一番修改,绘制时间缩短到了1.5s,但是网络请求时间太久,接下来就是进行数据本地缓存,网络分段加载数据等方面网络部分的优化了.