Search code examples
iosswiftthread-safetynskeyedarchiver

Crash while archiving data by NSKeyedArchiver


My app uses the following function to serialise a set of custom objects (only relevant code is shown):

class func serializeShoppingLocations(buyLocations: Set<ShoppingLocation>) -> Data {
    #if os(iOS)
        NSKeyedArchiver.setClassName("ShoppingLocation", for: ShoppingListIOS.ShoppingLocation.self)
    #elseif os(watchOS)
        NSKeyedArchiver.setClassName("ShoppingLocation", for: ShoppingListWatch.ShoppingLocation.self)
    #endif  
    let data = NSKeyedArchiver.archivedData(withRootObject: buyLocations)
    return data
}  

The conditional compilation for the iOS and watchOS targets is required in order to allow both targets to dearchive the serialised data later, even if they have been archived by the other target.

ShoppingLocation adopts the NSCoding protocol, and inherits from NSObject.

This code crashes at the let data = … instruction with EXC_BAD_ACCESS (code=1, address=0x0).

I know that the error probably indicates that a non-existent object is referred. However, the variable buyLocations is existent, and I can print the value in the debugger, including all values of the objects in the set.

The stack trace is:
enter image description here

The stack trace indicates that the archiver starts to archive the NSSet, and tries to archive one of the elements of the set, but setObjectForKey in the internal mutable dictionary fails.

What could be the reason?

EDIT:

During further testing I realised that the crash happens always when at least 2 threads of the app are active.
Particularly, I found a situation where one thread crashed (red in Xcode) at an instruction different from the one mentioned above:

let dataBlob = NSKeyedArchiver.archivedData(withRootObject: shoppingItem.buyLocations)  

and the other thread stopped at the instruction mentioned above (green in Xcode):

let data = NSKeyedArchiver.archivedData(withRootObject: buyLocations)

Might it be that NSKeyedArchiver is not thread-safe?


Solution

  • My problem is indeed, as suspected, caused by unsynchronised accesses by multiple threads:

    An NSArchiver walks through an object graph, starting at the root object, and archives on the way every object it encounters.
    In order to create a correct archive, it is therefore required that the object graph is not mutated until the archiver terminates.

    In a single-threaded application, this is guaranteed automatically.
    In a multi-threaded application where more than 1 thread can mutate the object graph, the programmer is responsible to ensure this, because the archiver has no way to prevent other threads to mutate the root object. (If, e.g., the archiver would first make a deep copy of the object graph, and then archive the copy, the object graph could be mutated during the deep copy).

    It is therefore required to serialise all get and set accesses to the object graph, e.g. by execute them in a serial access queue.