1

This is a continuation of this post. I have a problem with blocks with an NSOperation, my application crash in the completionblock.

i think that the problem is a retain cycle (i have a warning : Capturing self strongly in this block is likely to lead to a retain cycle), i try this solution (see the code above), the warning is gone but the app still crash.

the application is a simple tableView. the operation apply some modification in the passed image.

this is the first time i create and use block myself, thank you to give me some fundamental explication

code:

TableViewController.m:

in tableView: cellForRowAtIndexPath:

   [[ImageManager sharedManager] modifyImage:[UIImage imageNamed:@"anImage.png"] completionBlock:^(UIImage *image, NSError *error) {        
    [cell.imageView setImage:image];
}];

ImageManager.h:

  #import <Foundation/Foundation.h>
 @interface ImageManager : NSObject
 @property(nonatomic, strong) NSOperationQueue *imageOperationQueu;

 -(void) modifyImage:(UIImage*)image completionBlock:(void(^)(UIImage *image,NSError *error)) completBlock;

 + (id) sharedManager;

 @end

ImageManager.m:

#import "ImageManager.h"
#import "ImageOperations.h"

static ImageManager *MySharedManager = nil;

@implementation ImageManager

@synthesize imageOperationQueu;

+ (id)sharedManager
{
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    if (MySharedManager == nil) MySharedManager = [[self alloc] init];
  });
  return MySharedManager;
}

- (NSOperationQueue *)imageOperationQueu
{
   if (!imageOperationQueu)
 {
    imageOperationQueu = [[NSOperationQueue alloc] init];
    [imageOperationQueu setMaxConcurrentOperationCount:3];
    imageOperationQueu.name = @"imageOperationQueu";
 }
return imageOperationQueu;
}

-(void) modifyImage:(UIImage*)image completionBlock:(void(^)(UIImage *image,NSError *error)) completBlock
{
 ImageOperations *op = [[ImageOperations alloc]initWithImage:image WithCompletionBlock:completBlock];
[self.imageOperationQueu addOperation:op];
}
@end

ImageOperations.h:

 #import <Foundation/Foundation.h>

 typedef void (^CompletionBlock)(UIImage *image,NSError *error);

 @interface ImageOperations : NSOperation

 @property(nonatomic, weak) CompletionBlock completBlock;
 @property(nonatomic, strong) UIImage *imageToTransform;

 -(id)initWithImage:(UIImage *)image WithCompletionBlock:(CompletionBlock) block;
 @end

ImageOperations.m:

 #import "ImageOperations.h"

 @implementation ImageOperations
 @synthesize imageToTransform;
 @synthesize completBlock;

 -(id)initWithImage:(UIImage *)image WithCompletionBlock:(CompletionBlock) block
 {
  if (self = [super init])
  {
    NSLog(@"initWithImage");
    self.imageToTransform = image;
    [self setCompletBlock:block];
  }
 return self;
}

- (void)main
{
  @autoreleasepool
{        
    UIImage *img = [self setRoundedImage:self.imageToTransform];

    __weak ImageOperations *imgOp = self;

    [imgOp setCompletionBlock:^{
        NSLog(@"setCompletionBlock");
        imgOp.completBlock(img,nil);
    }];
}
}

-(UIImage*)setRoundedImage:(UIImage*)image
{
// ..
}

@end
Community
  • 1
  • 1
Red Mak
  • 1,176
  • 2
  • 25
  • 56
  • 1
    A retain cycle won't cause your app to crash. Premature deallocation will. –  Mar 27 '13 at 14:01
  • Put a breakpoint in your completion block and see if `cell` is valid when you're trying to use it. Since it's in a block, it is async and `cell` may or may not be valid at that time. You might want to declare cell with __block (`__block UITableViewCell *cell`) – Jai Govindani Mar 27 '13 at 14:04
  • First of all: Any stacktrace or console output when the crash occurs? – Nick Weaver Mar 27 '13 at 14:11
  • #H2CO3, yes it's right but much leak will cause a crash i think. i modify my code to set completBlock in ImageOperations as strong, now it's work (really really slow, but this is an other problem), but i have again that warning and after i scroll the app crash. #JaiGovindani, after my code modification, the modified images are displayed, so i don't think this is the problem. #NickWeaver, no nothing in log or the debug navigator (in xcode)! i'm testing in a device. – Red Mak Mar 27 '13 at 14:33
  • @RedMak If you're wondering why it's slow, it's because you're calling the completion block from the background queue, but your block is updating the UI. You should dispatch those calls back to the main queue, e.g. `[[NSOperationQueue mainQueue] addOperationWithBlock:^{ ... }` – Rob Mar 27 '13 at 16:18
  • is there any guarantee that blocks will be called in the main thread ? or have i to use into a dispatch_async(dispatch_get_main_queue(), ^{} ? – Red Mak Mar 27 '13 at 16:24
  • @RedMak No, there is no guarantee it will be called from main thread. To the contrary, when you manually call the completion block, it will be called from whatever queue you ran it from. So you should use either `dispatch_async(dispatch_get_main_queue(),...)` or the `[[NSOperationQueue mainQueue] addOperationWithBlock:^{ ... }];` to submit it back to the main queue (as a stylistic matter, when doing GCD I'll do the former, when doing `NSOperation` code, I'll use the latter, but either works fine). – Rob Mar 27 '13 at 16:29

1 Answers1

1

A couple of thoughts:

  1. You have your block defined as a weak property. In the Working With Blocks section of Programming with Objective-C guide they advise that you use copy instead. (Note, we're copying the block, not the objects it uses; you do have to be careful of strong references within that block, though.) The scope of block variables is surprisingly subtle. (See Patterns to Avoid in the Block Programming Topics guide for examples.) And calling a block that was released because it was weak can generate some unpredictable results. Use copy.

  2. Your tableview has a subtle issue, which is what would happen if the cell had scrolled off by the time the completion block was invoked. The cell could have been reused for another row of your table. (As it turns out, corner rounding logic is probably fast enough that this problem is unlikely to manifest itself, but this is critical if you do slower operations such as downloading images or if the images were huge.) Anyway, instead of:

    [[ImageManager sharedManager] modifyImage:[UIImage imageNamed:@"anImage.png"] completionBlock:^(UIImage *image, NSError *error) {        
        [cell.imageView setImage:image];
    }];
    

    you might want to call the UITableView method cellForRowAtIndexPath (not to be confused with the UITableViewController method tableView:cellForRowAtIndexPath:):

    [[ImageManager sharedManager] modifyImage:[UIImage imageNamed:@"anImage.png"] completionBlock:^(UIImage *image, NSError *error) {   
        if (error == nil)
        {     
            UITableViewCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
    
            // if cell has scrolled off screen, this will be `nil`, so let's check
    
            if (updateCell)
                [updateCell.imageView setImage:image];
        }
    }];
    
  3. You might want to be sensitive to the fact that the imageNamed method, itself, can be slow with first retrieving an image. If all of the cells are returning the same imageNamed, this is not an issue, but if (a) using unique imageNamed files; and (b) if dealing with older, slower device, you might even want to push the imageNamed process into the background queue, too. On newer devices (or on the simulator), you'll never see the issue, but if you test on a old, slow device, you might see some stuttering in a fast tableview scroll (but because imageNamed caches, you'll only see this stuttering UI when image is first retrieved...if you're using the same image name for all of them, you might not see this.)

  4. Your call to completBlock is being done in the background queue. Since you're doing UI updates, you might want to make sure to dispatch this back to the main queue:

    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        // call the completBlock here
    }];
    
Rob
  • 415,655
  • 72
  • 787
  • 1,044