configurePersistentStoreCoordinator not called for saveAs NSPersistentDocument

后端 未结 4 1489
醉梦人生
醉梦人生 2021-01-21 11:18

I experience an odd behaviour regarding saving an NSPersistentDocument. I can create a new document which is autosaved without an issue. But when I save it write(to: ofTyp

相关标签:
4条回答
  • 2021-01-21 11:34

    Good news and bad news

    I have succeeded to reproduce the exact same error with Xcode 9.2 running on MacOS 10.13.2.

    1. The version 1 of document file is still in a Autosave directory.
    2. Document.xcdatamodel is updated to version 2 (Document 2.xcdatamodel)
    3. When the application starts, the file of version 1 in the Autosave directory is read with NSBinaryStoreSecureDecodingClasses option.
    4. The app works well.
    5. File > Save... triggers NSPersistentStoreCoordinator .migratePersistentStore() aka -[NSPersistentStoreCoordinator migratePersistentStore:toURL:options:withType:error:]:
    6. During the course of migratePersistentStore, NSBinaryStoreSecureDecodingClasses option is not set for both writing to and reading from a temporary file.
    7. The error occurs.

    Somewhat strange for me. Why the migration is executed upon saving, instead of upon opening a file? The lightweight migration has been already done at the step 3?

    EDIT:

    It seems that the name of migratePersistentStore does not mean migration from old version to new version. It seems to mean migrate from an old persistent store with an old URL aka filename and/or file format to a new persistent store with a new URL and/or file format. So it is called when Save As... or Saving a new document which is already saved in an Autosave directory.

    0 讨论(0)
  • 2021-01-21 11:44

    Another, simpler workaround:

    The following code has a bug. Please refer to the bug fixed version cited below.

    extension NSPersistentStoreCoordinator {
    
      @objc func x_migratePersistentStore(_ store: NSPersistentStore, to URL: URL, options: [AnyHashable : Any]? = nil, withType storeType: String) throws -> NSPersistentStore {
        var opt: [AnyHashable : Any] = options ?? [:]
    
        if #available(OSX 10.13, *) {
          opt[NSBinaryStoreSecureDecodingClasses] = NSSet(array: [ NSColor.self ])
        }
    
        return try x_migratePersistentStore(store, to: URL, options: opt, withType: storeType)
      }
    
    }
    
    class Document: NSPersistentDocument {
    
      override init() {
        super.init()
    
        let s1 = #selector(NSPersistentStoreCoordinator.migratePersistentStore(_:to:options:withType:))
        let s2 = #selector(NSPersistentStoreCoordinator.x_migratePersistentStore(_:to:options:withType:))
        let m1 = class_getInstanceMethod(NSPersistentStoreCoordinator.self, s1)!
        let m2 = class_getInstanceMethod(NSPersistentStoreCoordinator.self, s2)!
        method_exchangeImplementations(m1, m2)
      }
    
      override func configurePersistentStoreCoordinator(for url: URL, ofType fileType: String, modelConfiguration configuration: String?, storeOptions: [String : Any]? = nil) throws {
        // keep yours
      }
    
    }
    

    Added:

    This workaround is going to be a practical solution until they enhance NSPersistentDocument.write(to...) to take into account of options or they implement other means to handle options. Those options will be given to NSPersistentStoreCoordinator.migratePersistentStore(...) and then be used to instantiate NSPersistentStore.

    Bug Fixed Version:

    There was a bug in the preceding workaround. Opening document files, creating new files, making changes, waiting for thirty second to the document being auto-saved, closing them, and/or save-as-ing them randomly would cause the initial error. Reported by Wizard of Kneup.

    Here is a fixed version using singleton to make sure swizzling is applied only once.

    extension NSPersistentStoreCoordinator {
    
      @objc func x_migratePersistentStore(_ store: NSPersistentStore, to URL: URL, options: [AnyHashable : Any]? = nil, withType storeType: String) throws -> NSPersistentStore {
        var opt: [AnyHashable : Any] = options ?? [:]
    
        if #available(OSX 10.13, *) {
          opt[NSBinaryStoreSecureDecodingClasses] = NSSet(array: [ NSColor.self ])
        }
    
        return try x_migratePersistentStore(store, to: URL, options: opt, withType: storeType)
      }
    
      class MigratePersistentStoreInitializer {
        init() {
          let s1 = #selector(NSPersistentStoreCoordinator.migratePersistentStore(_:to:options:withType:))
          let s2 = #selector(NSPersistentStoreCoordinator.x_migratePersistentStore(_:to:options:withType:))
          let m1 = class_getInstanceMethod(NSPersistentStoreCoordinator.self, s1)!
          let m2 = class_getInstanceMethod(NSPersistentStoreCoordinator.self, s2)!
          method_exchangeImplementations(m1, m2)
        }
    
        static let singlton = MigratePersistentStoreInitializer()  // Lazy Stored Property
      }
    
    }
    
    class Document: NSPersistentDocument {
    
      override init() {
        super.init()
        let _ = NSPersistentStoreCoordinator.MigratePersistentStoreInitializer.singlton
      }
    
      override func configurePersistentStoreCoordinator(for url: URL, ofType fileType: String, modelConfiguration configuration: String?, storeOptions: [String : Any]? = nil) throws {
        // keep yours
      }
    
    }
    

    References:

    • Lazy Stored Properties
    0 讨论(0)
  • 2021-01-21 11:46

    Could you try this nasty workaround?

    class CustomPersistentStoreCoordinator : NSPersistentStoreCoordinator {
    
      static var m1 : Method? = nil
      static var m2 : Method? = nil
    
      override func migratePersistentStore(_ store: NSPersistentStore, to URL: URL, options: [AnyHashable : Any]? = nil, withType storeType: String) throws -> NSPersistentStore {
        var opt: [AnyHashable : Any] = options ?? [:]
    
        if #available(OSX 10.13, *) {
          opt[NSBinaryStoreSecureDecodingClasses] = NSSet(array: [ NSColor.self ])
        }
    
        let m1 = CustomPersistentStoreCoordinator.m1!
        let m2 = CustomPersistentStoreCoordinator.m2!
        method_exchangeImplementations(m2, m1)
    
        let x = try self.migratePersistentStore(store, to: URL, options: opt, withType: storeType)
    
        method_exchangeImplementations(m1, m2)
        return x
      }
    }
    
    class Document: NSPersistentDocument {
    
      override init() {
        super.init()
    
        let s1 = #selector(NSPersistentStoreCoordinator.migratePersistentStore(_:to:options:withType:))
        let s2 = #selector(CustomPersistentStoreCoordinator.migratePersistentStore(_:to:options:withType:))
        let m1 = class_getInstanceMethod(NSPersistentStoreCoordinator.self, s1)!
        let m2 = class_getInstanceMethod(CustomPersistentStoreCoordinator.self, s2)!
        CustomPersistentStoreCoordinator.m1 = m1
        CustomPersistentStoreCoordinator.m2 = m2
        method_exchangeImplementations(m1, m2)
      }
    
      override func configurePersistentStoreCoordinator(for url: URL, ofType fileType: String, modelConfiguration configuration: String?, storeOptions: [String : Any]? = nil) throws {
        // keep yours
      }
    
    }
    
    0 讨论(0)
  • 2021-01-21 11:48

    Thank you @Wizard of Kneup!

    That clearly shows the application certainly read a document file and then encounter the error.

    Let's start investigation.

    (1) What file does the app attempt to read?

    (lldb) breakpoint set -n '-[NSBinaryObjectStoreFile readFromFile:error:]'    
    Breakpoint 3: # locations.
    

    Run the app again to reproduce the problem. Once the breakpoint hits, type the following commands to the lldb prompt:

    po $rdi
    p (SEL)$rsi
    po $rdx
    po $rcx
    po $r8 
    po $r9
    

    The filename would be shown. Disable the breakpoint. Use the number # which was returned at breakpoint set before. e.g. 3

    (lldb) breakpoint disable 3
    

    (2) What object does the app try to decode?

    (lldb) breakpoint set -n '-[NSKeyedUnarchiver _decodeArrayOfObjectsForKey:]'
    Breakpoint 4: # locations.
    

    Same as above. Use a set of po commands to get some information. It seems an array of something.

    (lldb) breakpoint disable 4
    

    (3) What class does the app reject to decode?

    (lldb) breakpoint set -n '-[NSCoder _validateAllowedClass:forKey:allowingInvocations:]'
    

    I wrote the breakpoints one by one and disable each time. But, no need to do that. Alternatively, set all breakpoints at a time and do hit-and-investigate-then-continue.

    The questions are why the app reads the file. That might be for migration.

    -[NSPersistentStoreCoordinator migratePersistentStore:toURL:options:withType:error:]
    

    But why override func configurePersistentStoreCoordinator() is not called? I have no knowledge on that now.

    I hope you would have some hints for getting closer to a solution.

    Added:

    To set a breakpoint, sometime we need to stop at the very beginning of execution of application.

    For instance, set a breakpoint at init() of AppDelegate to get a chance of manually setting breakpoints.

    class AppDelegate: NSObject, NSApplicationDelegate {
    
      override init() {
    =>  super.init()
      }
    
    }
    

    Appended:

    I had misunderstood. The correction:

    The func configurePersistentStoreCoordinator seems to be called when a class NSPersistentStore is instantiated with a desired URL, i.e. filename. So the timing is not related to the action of reading from nor writing to a document file. Instead, it is the first time when the internal document is about to be connected to the URL.

    • Opening an existing file -> the func configurePersistentStoreCoordinator is called.
    • File > New and then Save... -> the func is called.

    The fact we had seen that func configurePersistentStoreCoordinator was not called upon SaveAs seems to be normal, if the document has been loaded from an existing file in advance. The options for the NSPersistentStore we provided in that func are already there, I think.

    Tips for setting a breakpoint

    0 讨论(0)
提交回复
热议问题