Hi!
I need to paint an image using some data and show it on my iphone application. Since the painting takes significant time (2-3 seconds on device), I want to perform painting on a different thread. Also, I want to be able to cancel painting, change something in data and start it again. So it's best for me to use NSOperation.
Now, when I do the drawing on the main thread, everything looks fine.
When I do exactly the same thing using NSOperation subclass, everything looks fine, but only 95% of the time. Sometimes it doesnt draw the full picture. Sometimes it doesnt draw text. Sometimes it uses different colors, there might be red/green/blue dots scattered all over the image etc etc etc.
I made a very short example to illustrate this: First, we do all the painting on a main thread in a regular method:
//setting up bitmap context
size_t width = 400;
size_t height = 400;
size_t bitsPerComponent = 8;
size_t bytesPerRow = 4 * width;
void* imageData = malloc(bytesPerRow * height);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(imageData, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast);
CFRelease(colorSpace);
//transforming it to usual coordinate system
CGRect mapRect = CGRectMake(0, 0, width, height);
UIGraphicsPushContext(context);
CGContextTranslateCTM(context, 0, mapRect.size.height);
CGContextScaleCTM(context, 1, -1);
//actull drawing - nothing complicated here, 2 lines and 3 text strings on white background
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextFillRect(context, mapRect);
CGContextSetLineWidth(context, 3);
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGContextMoveToPoint(context, 10, 10);
CGContextAddLineToPoint(context, 20, 20);
CGContextStrokePath(context);
CGContextMoveToPoint(context, 20, 20);
CGContextAddLineToPoint(context, 100, 100);
CGContextStrokePath(context);
[UIColor blackColor].set;
[[NSString stringWithString:@"tag1"] drawInRect:CGRectMake(10, 10, 40, 15) withFont:[UIFont systemFontOfSize:15]];
[[NSString stringWithString:@"tag2"] drawInRect:CGRectMake(20, 20, 40, 15) withFont:[UIFont systemFontOfSize:15]];
[[NSString stringWithString:@"tag3"] drawInRect:CGRectMake(100, 100, 40, 15) withFont:[UIFont systemFontOfSize:15]];
//getting UIImage from bitmap context
CGImageRef _trueMap = CGBitmapContextCreateImage(context);
if (_trueMap) {
UIImage* _map = [UIImage imageWithCGImage:_trueMap];
CFRelease(_trueMap);
//displaying what we got
//self.map leads to UIImageView
self.map = _map;
}
//releasing context and memmory
UIGraphicsPopContext();
CFRelease(context);
free(imageData);
No errors here. Always works.
Now, I'll subclass NSOperation and copy-paste this code there: Interface:
@interface Painter : NSOperation {
//The controller which contains UIImageView we will use to display image
MapViewController* mapViewController;
CGContextRef context;
void* imageData;
}
@property (nonatomic, assign) MapViewController* mapViewController;
- (id) initWithRootController:(MapViewController*)mvc__;
@end
Now the methods:
- (id) initWithRootController:(MapViewController*)mvc__ {
if (self = [super init]) {
self.mapViewController = mvc__;
size_t width = 400;
size_t height = 400;
size_t bitsPerComponent = 8;
size_t bytesPerRow = 4 * width;
imageData = malloc(bytesPerRow * height);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
context = CGBitmapContextCreate(imageData, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast);
CFRelease(colorSpace);
}
return self;
}
- (void) main {
size_t width = 400;
size_t height = 400;
//transforming it to usual coordinate system
CGRect mapRect = CGRectMake(0, 0, width, height);
UIGraphicsPushContext(context);
CGContextTranslateCTM(context, 0, mapRect.size.height);
CGContextScaleCTM(context, 1, -1);
//actull drawing - nothing complicated here, 2 lines and 3 text strings on white background
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextFillRect(context, mapRect);
CGContextSetLineWidth(context, 3);
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGContextMoveToPoint(context, 10, 10);
CGContextAddLineToPoint(context, 20, 20);
CGContextStrokePath(context);
CGContextMoveToPoint(context, 20, 20);
CGContextAddLineToPoint(context, 100, 100);
CGContextStrokePath(context);
[UIColor blackColor].set;
[[NSString stringWithString:@"tag1"] drawInRect:CGRectMake(10, 10, 40, 15) withFont:[UIFont systemFontOfSize:15]];
[[NSString stringWithString:@"tag2"] drawInRect:CGRectMake(20, 20, 40, 15) withFont:[UIFont systemFontOfSize:15]];
[[NSString stringWithString:@"tag3"] drawInRect:CGRectMake(100, 100, 40, 15) withFont:[UIFont systemFontOfSize:15]];
//getting UIImage from bitmap context
CGImageRef _trueMap = CGBitmapContextCreateImage(context);
if (_trueMap) {
UIImage* _map = [UIImage imageWithCGImage:_trueMap];
CFRelease(_trueMap);
//displaying what we got
[mapViewController performSelectorOnMainThread:@selector(setMap:) withObject:_map waitUntilDone:YES];
}
//releasing context and memmory
UIGraphicsPopContext();
CFRelease(context);
free(imageData);
}
Again, no significant code changes between this 2 pieces of code. And when I start this operation like this:
NSOperationQueue* repaintQueue = [[NSOperationQueue alloc] init];
repaintQueue.maxConcurrentOperationCount = 1;
[repaintQueue addOperation:[[[Painter alloc] initWithRootController:self] autorelease]];
It will work. But not always, sometimes image will contain artifacts.
I've also made few screenshots to illustrate the issue, but couldn't post them =( Anyways, there is a screenshot which shows red line and 3 text lines (which is fine) and a screenshot which shows red line, no text lines and "tag2" written upside down on tab bar controller.
So what's the problem? I cant use Quartz with NSOperation? Is there some kind of restriction on drawing on separate threads? Is there a way to bypass those restrictions if so? If anyone ever seen this problem, please reply.