Pergunta

As I understand, the use of @try/@catch blocks is discouraged, because exceptions should only be thrown at unrecoverable, catastrophic errors (refer to this discussion with a nice answer by @bbum: Exception Handeling in iOS).

So I looked through my code and found a @try/@catch block that I don't know how to get rid of:

NSData *fileData = [NSData dataWithContentsOfFile: ....];

NSDictionary *dictionary;

@try {
   dictionary = [NSKeyedUnarchiver unarchiveObjectWithData: fileData];
}
@catch (NSException *exception) {
   //....
}
@finally {
  //...
}

The problem is that (as stated in the documentation) +unarchiveObjectWithData: raises an NSInvalidArchiveOperationException if the NSData doesn't contain a valid archive.

Since the data is provided by a file the user chose, it is not guaranteed that it contains a valid archive, and thus the application would crash if a incorrect file was chosen.

Now two questions:

  1. Why doesnt +unarchiveObjectWithData: just return nil (Edit: and an NSError**) if the archive is not valid (this doesn't seem to qualify as a catastrophic or unrecoverable error).
  2. Is the pattern above correct (using @try)? I have found no method that lets us check if the data contains a valid archive beforehand and have found no possibility to handle this case using the delegate protocol. Antyhing I overlooked?

Note that the code above of course works, I just wonder if its the best practice.

Foi útil?

Solução 2

NSKeyedArchiver is built by Apple. They control the code that is performed while unarchiveObjectWithData: executes so they also control resource management during exception handling (which is the source of trouble behind exceptions in Objective-C).

If they can guarantee that in between your call to unarchiveObjectWithData: and the point in code where they raise the exception is no foreign code (neither third party nor your app's code) it is in theory possible to safely use an exception, as long as calling code takes care of cleaning up correctly.

The problem is that this assumption might not be the case: It is common to use NSKeyedArchiver to serialize custom objects. Usually the custom class implements initWithCoder: to read the classes data (by using the archiver's methods like decodeObjectForKey:).

If the archiver throws an exception in one of these methods there's no way to fix resource handling for the archiver. The exception will be thrown through the custom object's initWithCoder:. The archiver does not know if there's more stuff to clean up than the deserialized objects. So in this scenario the occurrence of the exception means that the process is in a dangerous state and unwanted behavior may result.

Regarding your questions:

Why doesn't [NSKeyedArchiver use proper Cocoa error handling]?

Only the Apple engineers who built the archiver know. My guess is that exception handling and keyed archiving were built at roughly the same time (around 2001) and at that point it wasn't yet clear that exception handling would never be a first class citizen in Objective-C.

Is the @try pattern correct?

With the limitation of the caveats described above it is correct. If Apple's code handles the exception cases properly and your own serialization code does the same the @try pattern might be correct.

It is very difficult to achieve full correctness, though. You'd have to make sure all executed code is aware of the exceptions and does cleanup correctly.

ARC, for instance, does no exception cleanup for local variables and temporary objects by default (you would have to enable -fobjc-arc-exceptions to do this).

Also, there's no documentation on exception safety of the accessors of @synthesized properties (when atomic they might leak a lock).

Conclusion:

There are a myriad of subtle ways of how exceptions can break stuff. It is difficult and requires in depth knowledge of the implementation of all involved parts to build exception safe code in Objective-C.

All this leads to the conclusion. If you want to handle errors gracefully while loading possibly corrupted archives and continue normal execution afterwards: Do not use NSKeyedArchiver.

Outras dicas

There was a new method added in iOS 9 to NSKeyedUnarchiver that now returns an error:

Swift:

public class func unarchiveTopLevelObjectWithData(data: NSData) throws -> AnyObject?

Objective-C:

+ (nullable id)unarchiveTopLevelObjectWithData:(NSData *)data error:(NSError **)error;

However, this is not backwards compatible with previous versions of iOS, so you will need to check for framework availability.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top