Skip to content

Instantly share code, notes, and snippets.

@Tricertops
Created December 18, 2017 10:36
Show Gist options
  • Save Tricertops/354e443283d50497912cd7a1e8c884a1 to your computer and use it in GitHub Desktop.
Save Tricertops/354e443283d50497912cd7a1e8c884a1 to your computer and use it in GitHub Desktop.
NSKeyedUnarchiver cannot decode class clusters
#import <Foundation/Foundation.h>
@interface Node : NSObject <NSCoding>
@property NSString *name;
@property NSMutableArray<Node *> *targets;
@end
@implementation Node
- (instancetype)initWithCoder:(NSCoder *)decoder {
self = [super init];
self.name = [decoder decodeObjectForKey:@"name"];
self.targets = [decoder decodeObjectForKey:@"targets"];
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:self.name forKey:@"name"];
[encoder encodeObject:self.targets forKey:@"targets"];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Node *A = [Node new];
Node *B = [Node new];
A.name = @"A";
B.name = @"B";
// This object graph contains multiple references to `targets` mutable array, which in turn holds the same
// objects again. This creates cyclic reference chain, which should be OK in archive.
NSMutableArray<Node *> *targets = [NSMutableArray arrayWithArray:@[A, B]];
A.targets = targets;
B.targets = targets;
// References to the `targets` array will NOT be properly preserved. The references will point to
// uninitialized array instance, which was returned from [NSMutableArray alloc], but since
// [NSMutableArray initWithCoder:] replaced this with another instance, the references were not updated.
NSArray<Node *> *decodedTargets = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:targets]];
NSLog(@"Before: %@", targets[0].targets[0]);
NSLog(@"After: %@", decodedTargets[0].targets[0]); // This crashes:
// *** -[NSMutableArray objectAtIndex:]: method sent to an uninitialized mutable array object
// Conclusion:
// It is not safe for -initWithCoder: to return anything else than the original receiver. If this occurs in
// cyclic reference chain, the other end of this chain will point to original receiver of -initWithCoder:
// no matter what object that method returned. Same rule applies to -awakeAfterUsingCoder: method.
// I think this problem has no other solution, since I consider it impossible to update all existing
// references of an object to a new object. Class clusters are the place, where change need to happen.
// The address of final object must be know to NSCoder before the contents of NSArray are decoded.
}
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment