【问题标题】:Is caching a NSDateformatter application-wide good idea?缓存 NSDateformatter 应用程序范围的好主意吗?
【发布时间】:2015-02-03 23:36:12
【问题描述】:

已知创建 NSDateFormatters 是 well 'expensive'

甚至 Apple 的 Data Formatting Guide(2014 年 2 月更新)声明:

创建日期格式化程序并不是一项廉价的操作。如果您可能经常使用格式化程序,则缓存单个实例通常比创建和处置多个实例更有效。一种方法是使用静态变量。

但是该文档似乎与 swift 并没有真正保持同步,而且我在最新的NSDateFormatter Class Reference 中也找不到任何关于缓存格式化程序的信息,所以我只能假设它对于 swift 来说和它一样昂贵目标-c。

很多消息来源建议caching 使用它的类中的格式化程序,例如控制器或视图。

我想知道在项目中添加一个单例类来存储日期选择器是否方便甚至“便宜”,这样您就可以放心,再也不需要再次创建它了。这可以在应用程序的任何地方使用。您还可以创建多个包含多个日期选择器的共享实例。例如,一个用于显示日期的日期选择器和一个用于时间符号的日期选择器:

class DateformatterManager {
    var formatter = NSDateFormatter()

    class var dateFormatManager : DateformatterManager {
        struct Static {
            static let instance : DateformatterManager = DateformatterManager()
        }
        // date shown as date in some tableviews
        Static.instance.formatter.dateFormat = "yyyy-MM-dd"
        return Static.instance
    }

    class var timeFormatManager : DateformatterManager {
        struct Static {
            static let instance : DateformatterManager = DateformatterManager()
        }
        // date shown as time in some tableviews
        Static.instance.formatter.dateFormat = "HH:mm"
        return Static.instance
    }

    // MARK: - Helpers
    func stringFromDate(date: NSDate) -> String {
        return self.formatter.stringFromDate(date)
    }
    func dateFromString(date: String) -> NSDate? {
        return self.formatter.dateFromString(date)!
    }
}

// Usage would be something like: 
DateformatterManager.dateFormatManager.dateFromString("2014-12-05")

另一种类似的方法是只创建一个单例并根据需要切换格式:

class DateformatterManager {
    var formatter = NSDateFormatter()

    var dateFormatter : NSDateFormatter{
        get {
            // date shown as date in some tableviews
            formatter.dateFormat = "yyyy-MM-dd"
            return formatter
        }
    }

    var timeFormatter : NSDateFormatter{
        get {
            // date shown as time in some tableviews
            formatter.dateFormat = "HH:mm"
            return formatter
        }
    }

    class var sharedManager : DateformatterManager {
        struct Static {
            static let instance : DateformatterManager = DateformatterManager()
        }
        return Static.instance
    }

    // MARK: - Helpers
    func dateStringFromDate(date: NSDate) -> String {
        return self.dateFormatter.stringFromDate(date)
    }
    func dateFromDateString(date: String) -> NSDate? {
        return self.dateFormatter.dateFromString(date)!
    }
    func timeStringFromDate(date: NSDate) -> String {
        return self.timeFormatter.stringFromDate(date)
    }
    func dateFromTimeString(date: String) -> NSDate? {
        return self.timeFormatter.dateFromString(date)!
    }
}

// Usage would be something like: 
var DateformatterManager.sharedManager.dateFromDateString("2014-12-05")

其中任何一个是好主意还是坏主意?转换格式也很贵吗?

更新: 正如Hot LicksLorenzo Rossi 指出的那样,切换格式可能不是一个好主意(不是线程安全的,而且与重新创建一样昂贵......)。

【问题讨论】:

  • 请注意,无论 NSDateFormatter 本身是否是“线程安全的”,当格式化程序有可能在另一个线程中使用时,不得在一个线程中更改格式。这就是为什么最好让全局格式化程序具有固定格式而不是在每次使用时更改它的原因之一。
  • 不错,谢谢!在第一个示例中这不是问题吗?如果我做对了,每个实例的格式都不会改变。
  • 我认为对于每种类型的格式,您都可以拥有一个日期格式化程序。
  • 另一个不改变格式的好理由是它会提供no性能改进:正如this WWDC video中所述“设置格式与重新创造”
  • 啊,不知道。谢谢!

标签: ios cocoa caching swift nsdatepicker


【解决方案1】:

我会在这里根据经验给出一个答案。答案是肯定的,在应用程序范围内缓存 NSDateFormatter 是一个好主意,但是,为了增加安全性,您需要为此采取一个步骤。

为什么好?表现。事实证明,创建 NSDateFormatters 实际上很慢。我开发了一个高度本地化的应用程序,并使用了很多 NSDateFormatters 和 NSNumberFormatters。有时我们会在方法中贪婪地动态创建它们,并且让类拥有自己所需的格式化程序副本。此外,我们还有额外的负担,在某些情况下,我们还可以在同一屏幕上显示针对不同区域设置的本地化字符串。我们注意到我们的应用程序在某些情况下运行缓慢,在运行 Instruments 之后,我们意识到这是格式化程序的创建。例如,当滚动包含大量单元格的表格视图时,我们看到性能下降。所以我们最终通过创建一个出售适当格式化程序的单例对象来缓存它们。

调用看起来像:

NSDateFormatter *dateFormatter = [[FormatterVender sharedInstance] shortDate];

注意,这是 Obj-C,但可以在 Swift 中制作等价物。只是碰巧我们的在 Obj-C 中。

从 iOS 7 开始,NSDateFormatters 和 NSNumberFormatters 是“线程安全的”,但是正如 Hot Licks 所提到的,如果另一个线程正在使用它,您可能不想四处修改格式。缓存它们的另一个 +1。

我刚刚想到的另一个好处是代码可维护性。尤其是如果您拥有像我们这样的大型团队。因为所有开发人员都知道有一个出售格式化程序的集中对象,所以他们可以简单地查看他们需要的格式化程序是否已经存在。如果没有,它会被添加。这通常与功能相关,因此通常意味着其他地方也需要新的格式化程序。这也有助于减少错误,因为如果格式化程序中碰巧有错误,您可以在一个地方修复它。但我们通常会在新格式化程序的单元测试中发现这一点。

为了安全起见,如果您愿意,还可以再添加一个元素。您可以使用 NSThread 的 threadDictionary 来存储格式化程序。换句话说,当您调用将出售格式化程序的单例时,该类会检查当前线程的 threadDictionary 以查看该格式化程序是否存在。如果它存在,那么它只是返回它。如果没有,它会创建它然后返回它。这增加了一定程度的安全性,因此如果出于某种原因您想要修改格式化程序,您可以这样做,而不必担心格式化程序正在被另一个线程修改。

我们最终使用的是单例,它出售特定的格式化程序(NSDateFormatter 和 NSNumberFormatter),确保每个线程本身都有自己的特定格式化程序的副本(请注意,该应用程序是在 iOS 7 之前创建的,它使它成为必不可少的事情)。这样做提高了我们的应用程序性能,并消除了由于线程安全和格式化程序而导致的一些令人讨厌的副作用。由于我们有 threadDictionary 部分,我从未测试过它是否在没有它的情况下在 iOS7+ 上存在任何问题(即它们已经真正成为线程安全的)。这就是为什么我在上面添加了“如果你愿意”。

【讨论】:

  • 首先,很好的答案!我在下面添加了一个答案,说明了您的观点。只是出于兴趣,你是这样做的吗?天真地,我过去曾遇到过问题,忽略了跨线程访问会导致问题的事实(确实如此,否则我不会在这里)。尽管我正在运行 >iOS7,但您的解决方案似乎已经缓解了我过去遇到的任何问题。你知道为什么会这样吗(尽管 NSDateFormatter 应该是线程安全的)?如果是这样的话,我不会对共享实例有任何问题,对吧?
  • 其实我做的方式不一样。我创建了一个格式化程序工厂,出售我需要的特定格式化程序。例如,我需要不同样式的日期格式化程序(例如,一个简短的变体和一个较长的版本)。每个格式化程序都有一个明确的名称,并以这种方式引用。例如:DATE_FORMATTER_SHORT。
  • 关于您的问题,您遇到了哪些类型的问题?即使您有一个共享实例,根据您的处理方式,即使事情是线程安全的,仍然可能会出现某些类型的问题。
  • 如果您为每个线程缓存格式化程序,您可以只使用一个 DateFormatter 和一个 NumberFormatter 并在每次需要以不同方式使用它们时更改它们的属性以配置它们,对吗?我想知道这是更有效还是更少?
  • 感谢您的意见。我认为不需要使用 NSThread 字典,因为 DateFormatter 是线程安全的,并且线程本地不能解决突变问题。 DateFormat 的变异 dateFormat/timeZone 仍然可以在同一个线程中发生。 NSThread 字典仅用于最小化此类范围。唯一的解决方案是将 DateFormatter 设计为不可变类,但这超出了开发人员的控制范围。可行的方法是在团队内部强加硬性规则,一旦 DateFormatter 初始化就不要改变它们。
【解决方案2】:

由于 Swift 使用 dispatch once 方法来创建静态属性,因此以这种方式创建 DateFormatter 非常快速且安全。

extension DateFormatter {
    static let shortFormatDateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd"
        formatter.locale = Locale(identifier: "en_US_POSIX")
        return formatter
    }()
}

不仅仅是写

date = DateFormatter.shortFormatDateFormatter.string(from: json["date"])

【讨论】:

  • 共享可变状态有竞争条件的风险。
  • @ScottyBlades 为什么它是可变的?你认为DF内部有某种状态吗?我们可以通过使用 NSThread 的信息字典将它们分隔在不同的线程上来添加另一层。
  • 把这个扔到操场上:DateFormatter.shortFormatDateFormatter.dateFormat = "MM-dd-yyyy HH:mm" print("your shortDate formatter is now medium length format: ", DateFormatter.shortFormatDateFormatter.dateFormat!),类是通过引用传递的。与通过副本传递的结构不同,类实例是相同的,但 values on that instance 可以更改,创建共享的可变状态、竞争条件和意外值,这些很难调试。
  • @ScottyBlades 我关于安全的观点与创作本身有关。我们不期望客户端会更改该对象中的任何内容。显然,不知道的客户端可以通过在不同线程上使用相同的日期格式化程序来创建竞争条件情况。这就是我写 NSThread 及其字典的原因。如果您需要在物理上消除该竞争条件的可能性,那么您唯一的选择是创建另一个抽象级别(通过引入新函数或新类/结构)。越来越多的创建日期格式化程序会影响性能 - 您在性能/安全/重量之间取得平衡。
  • 最佳答案的作者非常出色地指出了该解决方案的优缺点。没有必要再讨论这些事情了。 :)
【解决方案3】:

我认为缓存NSDateFormatter 是一个好主意,如果您的应用广泛或在整个应用中使用它,它将提高您的应用的性能。如果您在 1 或 2 个地方需要它,那将不是一个好主意。但是,更改日期格式并不是一个好主意,它可能会导致您陷入不希望的情况。 (每次使用前都需要跟踪当前格式)

在我的一个应用程序中,我使用了一个带有三个日期格式对象(所有三个都包含三种不同格式)作为属性的单例。以及每个NSDateFormatter的自定义getter

+ (instancetype)defaultDateManager
{
    static DateManager *dateManager = nil;
    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{
        dateManager                = [[DateManager alloc] init];
    });

    return dateManager;
}

// Custom Getter for short date
- (NSDateFormatter *)shortDate
{
    if (!_shortDateFormatter)
    {
        _shortDateFormatter = [[NSDateFormatter alloc] init];
        [_shortDateFormatter setDateFormat:@"yyyy-MM-dd"]
    }
    return _shortDateFormatter
}

像这样,我也为其他两个实现了自定义 getter。

为什么我实现了自定义 getter ?为什么我在单例初始化期间没有分配NSDateFormatter

这是因为我不想在一开始就分配它们。我需要在第一次需要时分配它(按需)。在我的应用程序中,所有三个 NSDateFormatters 都没有被广泛使用,这就是为什么我选择这样的模式来实现它。 (我的应用在 Objective C 中,这就是我在这里使用 Objective C 代码的原因)

【讨论】:

  • 我应该为每个 dateFormat 创建不同的格式化程序吗?例如:一个用于“yyyy-MM-dd” 一个用于“yyyy”等等?
  • @SisterRay:IMO,是的,你应该这样做。
【解决方案4】:

不要使用单例,而是使用依赖注入。请记住遵循 0、1、无限规则。

http://en.wikipedia.org/wiki/Zero_one_infinity_rule

在这里,我们显然不能有 0,虽然 1 听起来不错,但如果你要从多个线程中使用它,那么你不能只有一个没有它挂起。所以,无穷大。

解决这个问题的一个好方法是小心你生成的数量,为你打开的每个线程保留一个,并确保在你完成使用它们后立即清理它们。

为了帮助您,请查看这个其他 stackoverflow 链接 - 恐怕我的答案会与他们的一致(将 NSDateformatters 的数量保持在最低限度)。但是,他们可能有一些本线程未涵盖的推理(没有双关语!)

How to minimize the costs for allocating and initializing an NSDateFormatter?

另外,如果我可能会问 - 如果您遇到这个问题,也许您的程序中的某个地方可以改进流程以避免甚至需要这么多格式化程序?

【讨论】:

    【解决方案5】:

    这些是好主意还是坏主意?

    引入单例(当您需要格式化程序的新变体时)不是一个好的解决方案。创建格式化程序并不那么昂贵。

    相反,请设计一些方法来重用和共享格式化程序的实例,并在程序中传递它们,就像常规变量一样。这种方法很容易引入现有程序。

    仪器将帮助您确定您的程序在哪里创建了许多格式化程序,并且您可以考虑如何根据这些数据重用格式化程序。

    而且转换格式也很昂贵?

    不要费心修改您共享的格式化程序,除非在非常特定/本地的上下文中使用(例如特定的视图集合)。这将更容易实现预期的结果。相反,如果您需要共享格式化程序的变体,copy 然后进行变异。

    【讨论】:

      【解决方案6】:

      在对象中:

      考虑到 NSDateFormatter 被认为是线程不安全的,在您的应用程序中使用单个格式化程序可能不是最好的主意。每个线程有一个格式化程序可能是一种方法。或者,您可以考虑将格式化程序包装在线程安全类中。

      来自 Apple 文档: https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html

      在 Swift 中:

      如果 swift 提供类的线程安全性,那么拥有一个实例应该没有问题。

      【讨论】:

      • 我认为“让”让它线程安全? stackoverflow.com/questions/24024549/…
      • @Tieme,如果您可以验证实例是真正的线程安全的,那么对于应用程序使用一个对象应该没问题。
      • @Tieme 变量是以线程安全的方式创建的,但对象本身不是线程安全的。 let 在这种情况下意味着不会出现初始化变量的竞争条件(如使用 dispatch_once)。 let 不会将通常线程不安全的对象提升为线程安全的对象。
      • DateFormatter 并不是真正的线程安全的。我不建议尝试跨不同线程更改 dateFormat。 :)
      【解决方案7】:

      Swift 示例

      基于@Mobile Ben 的回答:这是一个简单的 Swift 单例示例。

      class YourDateFormatter {
      
          // MARK: - Properties
      
          static let sharedFormatter = YourDateFormatter()
          /// only date format
          private let dateFormatter: NSDateFormatter
          /// only time format
          private let timeFormatter: NSDateFormatter
      
          // MARK: - private init
      
          private init() {
      
              // init the formatters
      
              dateFormatter = NSDateFormatter()
              timeFormatter = NSDateFormatter()
      
              // config the format
      
              dateFormatter.dateStyle = .MediumStyle
              dateFormatter.timeStyle = .NoStyle
              dateFormatter.doesRelativeDateFormatting = true
      
              timeFormatter.dateStyle = .NoStyle
              timeFormatter.timeStyle = .MediumStyle
      
          }
      
          // MARK: - Public 
      
          func dateFormat(date: NSDate) -> String {
              return dateFormatter.stringFromDate(date)
          }
      
          func timeFormat(date: NSDate) -> String {
              return timeFormatter.stringFromDate(date)
          }
      
          func dateTimeFormat(date: NSDate) -> String {
              let dateFormat = self.dateFormat(date)
              let timeFormat = self.timeFormat(date)
      
              return dateFormat + " - " + timeFormat
          }
      }
      

      【讨论】:

        【解决方案8】:

        我还想通过提供一个示例来扩展 Mobile Ben 的答案:

        import Foundation
        
        public class DateFormatter : NSDateFormatter{
        
          public class func sharedFormatter() -> NSDateFormatter {
            // current thread's hash
            let threadHash = NSThread.currentThread().hash
            // check if a date formatter has already been created for this thread
            if let existingFormatter = NSThread.currentThread().threadDictionary[threadHash] as? NSDateFormatter{
              // a date formatter has already been created, return that
              return existingFormatter
            }else{
              // otherwise, create a new date formatter 
              let dateFormatter = NSDateFormatter()
              // and store it in the threadDictionary (so that we can access it later on in the current thread)
              NSThread.currentThread().threadDictionary[threadHash] = dateFormatter
              return dateFormatter
        
            }
        
          }
        
        }
        

        这是在库中使用的,这就是为什么您可以在整个过程中看到 public 修饰符的原因。

        【讨论】:

        • 抱歉这么晚了。我把我的 cmets 放在上面了。
        • 你可能不应该把这个类命名为DateFormatter:github.com/apple/swift-corelibs-foundation/blob/…
        • 您不应在应用程序中为所有 dateFormats 重复使用单个 DateFormatter。这就是 Apple 工程师在 WWDC 上所说的。
        猜你喜欢
        • 1970-01-01
        • 2017-01-13
        • 2018-09-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-08-19
        • 2020-05-02
        相关资源
        最近更新 更多