为什么分配或初始化NSDateFormatter被认为是“昂贵”的?

47

当人们说这很贵时,他们是什么意思?我创建了许多瞬态对象的实例,只是为了中间存储(NSString和NSDate是典型的)。如何知道我的程序是否过度使用了NSDateFormatter?

直到现在,我倾向于创建一个类似单例的东西,但我更喜欢将其封装到与之相关的其他对象中,以便我可以使用self引用。

除了运行性能测试外,我正在寻找更好的“经验法则”来理解我应该或不应该这样做的原因。


你可以对你的应用进行性能分析,以确定哪种方式更适合你的需要。我认为开发者想要指出的是,即使像NSDateFormatter这样听起来无害的东西也可能有一些相当复杂的初始化代码,所以文档中提供了一个提示,尽可能重复使用已初始化的对象。 - Tim Reddy
2个回答

40

当某些操作被称为代价昂贵时,并不意味着你永远都不能这样做,而是应该在需要尽快退出方法的情况下避免这样做。例如,在 iPhone 3G 是最新设备的时候,我正在编写一个使用 UITableView 的应用程序,它会在每个单元格中格式化数字以进行显示(我必须补充一句,那时我刚开始学 iOS 开发)。我的第一次尝试如下:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    NSString *reuseIdentifier = @"cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier forIndexPath:indexPath];

    MyManagedObject *managedObject = [self.managedObjects objectAtIndex:indexPath.row];
    NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
    [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];

    [cell.textLabel setText:[managedObject title]];
    [cell.detailTextLabel setText:[numberFormatter stringFromNumber:[managedObject amount]]];

    return cell;
}

这段代码的滚动性能非常差。由于我每次点击tableView:cellForRowAtIndexPath:时都会分配一个新的NSNumberFormatter,导致帧速率降至大约15 FPS。

我通过将代码更改为以下内容来修复它:

- (NSNumberFormatter *)numberFormatter {

    if (_numberFormatter != nil) {
        return _numberFormatter;
    }

    _numberFormatter = [[NSNumberFormatter alloc] init];
    [_numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];

    return _numberFormatter;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    NSString *reuseIdentifier = @"cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier forIndexPath:indexPath];

    MyManagedObject *managedObject = [self.managedObjects objectAtIndex:indexPath.row];
    NSNumberFormatter *numberFormatter = [self numberFormatter];

    [cell.textLabel setText:[managedObject title]];
    [cell.detailTextLabel setText:[numberFormatter stringFromNumber:[managedObject amount]]];

    return cell;
}

这里的区别在于我已经将NSNumberFormatter懒加载为一个实例变量,这样每次运行tableView:cellForRowAtIndexPath:不再分配新实例。这个简单的改变将滚动性能提高到了大约60 FPS。

虽然最新的芯片能够处理分配而不影响滚动性能,但尽可能高效总是更好的。


1
这种情况的危险在于,您必须确保您的numberFormatter不会从多个线程中调用。它不支持并发。 - Greg Maletic
1
@GregMaletic 这只是一个非常简单的例子,“tableView:cellForRowAtIndexPath:” 永远不会被除了主线程之外的任何其他线程调用。 - Ell Neal
1
@Eli 确实如此,但是如果您的numberFormatter方法在代码中的另一个方法中被调用,并且该方法在另一个线程上被调用,那么您可能会遇到问题。您只需要记住仅从主线程调用您的numberFormatter方法即可。 - Greg Maletic
太棒了,我之前不知道这个,但在面试中,当我做了和cellForRow类似的事情时,它被提了出来。现在我知道该怎么回答更好的方法了! - romero-ios

8

我曾经有过同样的问题。我在一些应用程序上运行了Instruments,并发现之前的开发人员为每个自定义日志创建了一个新的NSDateFormatter。由于他们用于记录每个屏幕大约3行,因此该应用程序只花费了约1秒钟来创建NSDateFormatters。

简单的解决方案是将日期格式化程序实例保留在您的类中作为属性或其他内容,并将其重用于每个日志行。

经过一些琐碎的思考,我想到了一个“工厂”,以根据所需的格式和区域设置重复使用NSDateFormatters。我请求某种格式和语言环境的日期格式化程序,我的类会将已加载的格式化程序给我。优秀的性能调整,你应该尝试。

PS:也许有人想测试它,所以我将其公开了:https://github.com/DougFischer/DFDateFormatterFactory/blob/master/README.md


1
你有收到任何反馈吗? - Dejell
1
我同意。简单方便。我的当前应用程序从使用它中获得了很多好处。非常感谢。 - rmvz3
1
最初,我担心线程安全问题,但事实证明这种信息已经过时:https://developer.apple.com/reference/foundation/dateformatter在iOS 7及以上版本中,NSDateFormatter是线程安全的。 - Pat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接