NSOperation in iOS - How to handle loops and nested NSOperation call to get Images

蹲街弑〆低调 提交于 2019-12-12 20:08:09

问题


I need help understanding how to appropriate handle the following use case:

Say I'm writing a Chat app:

  1. Launch App
  2. Ask server (AFHTTPRequestOperation) to give me a list of all new messages
  3. Loop through those messages to see if I need to download any images
  4. If yes, then make another call to the server (AFImageRequestOperation) to get the image

I keep getting crashes where my managed object "message" is no longer in the same context, but I'm only using one managedObjectContext.

Is it because of the way I'm nesting the calls, since they are asynchronous? I am almost positive the message isn't getting deleted elsewhere, because I see it.

One thing to note is that is seems to happen when I change view controllers. I launch the app, and at the root view controller (RVC), it performs step #2 above. If I touch to go the the "MessageListViewController" (MLVC) before all of the images have downloaded, the associated messages for those images that didn't finish downloading suddenly have a nil managedObjectContext.

Below is the relevant code:

    AFHTTPRequestOperation * operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:requestImageInfoURL
     success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {

         NSDictionary * JSONresponse = (NSDictionary *)JSON;

         if( [[[JSONresponse objectForKey:@"status"] uppercaseString] isEqualToString:@"ERROR"] )
         {
             for( id<CommsObserver> observer in _observers )
                 [observer errorOccurred:[JSONresponse objectForKey:@"message"]];
         }
         else
         {
             if( [JSONresponse containsKey:@"convoMessages"] )
             {
                 NSArray * messageList = [JSON objectForKey:@"messages"];

                 for( int i = 0 ; i < messageList.count ; i++ )
                 {
                     __block ConversationMessage * message = [JSONUtility convoMessageFromJSON:[messageList objectAtIndex:i]];

                     if( !message )
                         NSLog( @"Couldn't create the new message..." );
                     else
                     {
                         message.unread = [NSNumber numberWithBool:YES];
                         [[DataController sharedController] saveContext];

                         if( (!message.text || [message.text isEqualToString:@""]) && (message.image || message.imageInfoKey) )
                         {
                             NSString * imageURL = (message.image ? [Comms urlStringForImageInfo:message.image] : [Comms urlStringForImageKey:message.imageInfoKey extension:message.imageInfoExt]);

                             NSURLRequest *requestImageURL = [NSURLRequest requestWithURL:[NSURL URLWithString:imageURL]];
                             AFImageRequestOperation * imageOperation;
                             imageOperation = [AFImageRequestOperation imageRequestOperationWithRequest:requestImageURL
                                   imageProcessingBlock:^UIImage *(UIImage *image) {
                                       return image;
                                   } success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {

                                       if( message.image )
                                       {
                                           NSLog( @"updating imageInfo for message" );

                                           [Utilities updateImageInfo:message.image
                                                            withImage:image
                                                            asPreview:YES
                                                          asThumbnail:YES
                                                         preserveSize:YES];
                                       }
                                       else
                                       {
                                           NSLog( @"creating a new imageInfo for message" );

                                           ImageInfo * imageInfo = [Utilities createImageInfoFromImage:image asPreview:NO asThumbnail:NO preserveSize:YES];

                                           if( imageInfo.managedObjectContext == nil )
                                               NSLog( @"imageInfo MOC is NIL" );
                                           else if( message.managedObjectContext == nil )
                                           {
                                               NSLog( @"message MOC is NIL" );

                                               message = [[DataController sharedController] convoMessageForKey:message.key];

                                               if( !message )
                                                   NSLog( @"message is NIL, meaning it wasn't found in the MOC" );
                                               else if( !message.managedObjectContext )
                                                   NSLog( @"message MOC was STILL NIL" );
                                               else
                                                   NSLog( @"problem solved..." );
                                           }

                                           if( imageInfo )
                                               [[DataController sharedController] associateImageInfo:imageInfo withMessage:message];
                                       }

                                       [[DataController sharedController] saveContext];

                                   } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
                                       NSLog( @"Image DOWNLOAD error... \n%@" , [NSString stringWithFormat:@"%@" , error] );
                                   }];

                             [imageOperation start];
                         }

                         for( id<CommsObserver> observer in _observers )
                             [observer newConvoMessages:@[message.key]];
                     }
                 } // End for loop of messageList

             } // End if JSONresponse

         } // End outer if ERROR statement

     } // End Success 

     failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
         NSLog( @"Error: \n%@" , [NSString stringWithFormat:@"%@" , error] );
     }
];

[operation start];

回答1:


You need to ensure that the execution context where you invoke methods associated to a managed object context is appropriate (namely, is the same) as for the managed object context.

That is, when you invoke

 [[DataController sharedController] saveContext];

the thread (or dispatch queue) where the method save: will be executed (eventually) MUST be the same where the managed object context is associated to.

Here in this case, we can immediately conclude, that this will only work IFF a) the completion handler of AFN will execute on the main thread AND b) the managed object context is associated to the main thread, too, OR you take care of this within the implementation of saveContext and use performBlock: or performBlockAndWait:.

Otherwise since the execution context of a managed object context is private, the execution context of any completion handler will never match this one. Hence, you violate the concurrency rules for Core Data.

Whenever you send a message to a managed object or a managed object context you need to ensure that the current execution context will be the correct one. That is, you need to use performBlock: or performBlockAndWait: and wrap accesses into the block:

[[DataController sharedController].managedObjectContext performBlock:^{
    assert(message.managedObjectContext == [DataController sharedController].managedObjectContext);
    message.unread = [NSNumber numberWithBool:YES];
    [[DataController sharedController] saveContext];
    ...

}];

Note: you have to wrap all those accesses into either performBlock: or performBlockAndWait:, with the exception of property objectID of a managed object.

The objectID can be obtained from any thread. Thus this can be used to fetch any managed object into any context as long as you have the objectID.


A few other hints:

Using a Managed Object

You need to ensure that when you use a managed object (that is, send a message to it) that this will be executed on the same execution context which is associated to the managed object's managed object context.

That is, in order to ensure that, you use performBlock: or performBlockAndWait: as follows:

NSManagedObjectContext* context =  [[NSManagedObjectContext alloc] 
    initWithConcurrencyType:NSPrivateQueueConcurrencyType];

Note: context uses a private queue.

__block NSManagedObject* obj;
[context performBlockAndWait:^{
    obj = [context objectRegisteredForID:theObjectID];
}];

Assuming, the following statement will be executed on an arbitrary thread, this is unsafe:

NSString* name = obj.name;

"Unsafe", unless you know obj's managed object context has been associated to the main thread AND the above statement will also execute on the main thread. If the context uses a private queue, this will be never true unless you use performBlock: or performBlockAndWait::

Safe:

__block NSString* name;
[obj.managedObjectContext performBlockAndWait:^{
    name = obj.name;
}];

Obtaining the objectID is always safe from any thread:

NSManagedObjectID* moid = obj.objectID;  // safe from any thread

"Move" a managed object from one context to another:

You cannot use a managed object associated to context A, in context B. In order to "move" that object into context B you first need the objectID and then "fetch" this object in context B:

NSManagedObjectID* moid = obj.objectID

NSManagedObjectContext* otherContext = [[NSManagedObjectContext alloc] 
    initWithConcurrencyType:NSPrivateQueueConcurrencyType];

[otherContext performBlock:^{
    NSManagedObject* obj = [otherContext objectWithID: moid];
    ...
}];

Error Parameters

Be carefully with error parameters.

Error parameters are always autoreleased. performBlockAndWait: doesn't use an autorelease pool internally. So, you can have a __block variable error outside the block:

__block NSManagedObject* obj;
__block NSError* error;
[context performBlockAndWait:^{
    obj = [context existingObjectWithID:theObjectID error:&error];
}];
if (obj==nil) {
    NSLog(@"Error:%@", error);
}

However performBlock: will use an autorelease pool internally! This has consequences:

If you use the asynchronous version performBlock:, you need to handle errors within the block:

__block NSManagedObject* obj;
[context performBlock:^{
    NSError* error;
    obj = [context existingObjectWithID:theObjectID error:&error];
    if (obj==nil) {
        NSLog(@"Error:%@", error);
    }
}];


来源:https://stackoverflow.com/questions/20171875/nsoperation-in-ios-how-to-handle-loops-and-nested-nsoperation-call-to-get-imag

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!