vir us
vir us

Reputation: 10715

UIView with drawRect/update image performance issue on Retina displays

The problem in short:

LooksLike drawRect itself (even empty) leads to a significant performance bottleneck depending on device's resolution - the bigger screen is the worse things are. Is there a way to speed up redrawing of view's content?

In more details:

I'm creating a small drawing app on iOS - user moves his finger over a display to draw a line.

The idea behind this is quite simple - while user moves his finger touchesMoved accumulates the changes into offscreen buffer image and invalidates the view to merge the offscreen buffer with the view's content.

The simple code snippet may look like this:

@interface CanvasView : UIView
...
end;


@implementation CanvasView{
    UIImage *canvasContentImage;
    UIImage *bufferImage
    CGContextRef drawingContext;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    // prepare drawing to start
    UIGraphicsBeginImageContext(canvasSize);
    drawingContext = UIGraphicsGetCurrentContext();
    ...

}

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
    // draw to bufferImage
    CGContextMoveToPoint(drawingContext, prevPoint.x, prevPoint.y);
    CGContextAddLineToPoint(drawingContext, point.x, point.y);
    CGContextStrokePath(drawingContext);
    ...
    [self setNeedDisplay];
}

-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
    //finish drawing
    UIGraphicsEndImageContext();
    //merge canvasContentImage with bufferImage
    ...
}

-(void)drawRect:(CGRect)rect{
    // draw bufferImage - merge it with current view's content

    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextDrawImage(context, self.bounds, canvasContentImage.CGImage);
    CGContextDrawImage(context, imageRect, bufferImage.CGImage);
    ...

}

I've also implemented a small helper class to calculate fps rate.

So the approach above works rather good on non-retina screens producing nearly 60fps. However fps rate dramatically drops on retina-screens. For example on iPad Retina it is about 15-20 fps which is too slow.

The first obvious reason I thought is that setNeedsDisplay causes to redraw a full screen which is a big wast of resources. So I moved to setNeedsDisplayInRect to update only a dirty region. Surprisingly it didn't change anything regarding to performance (at lest nothing noticeable according to measurements and visually).

So I've started to try different approaches to figure out the bottleneck. When I've commented out all the drawing logic the fps rate still stayed at 15-20 - looks like the problem lies outside of drawing logic. Finally when I've fully commented out the drawRect method the fps rises to 60. Not that I removed only the implementation but even a declaration. Not sure of my terminology so here is the results:

//    -(void)drawRect:(CGRect)rect{
//        // draw bufferImage - merge it with current view's content
//        ...
//    }

What is more interesting when I moved all the drawing code from drawRect method to touchMoved method it doesn't impact the performance, however the same amount of drawing/processing logic still remains comparing to the version with drawRect method - even updating the entire view every time still gives me 60fps. One problem is that without drawRect I'm not able to visualize that changes.

So I've came to what pregenerated drawRect method warns about:

"Only override drawRect: if you perform custom drawing. An empty implementation adversely affects performance during animation."

My guess is that the system creates and destroys graphics context every time custom drawRect is triggered leading to "dversely affects performance"

So the questions are:

  1. Is there any way to speed up drawRect calls, like make the system reuse resources from call to call of drawRect or something?

  2. If this is a dead end, what other approaches available to update view's content? Moving to OpenGL is not an option at the moment as there are a lot of code/logic already implemented and it will take a lot of effort to port it.

I'll be glad to provide any additional information needed. And Thanks in advance for any suggestions!

EDIT:

After some more investigation and experiments I've came to using UIImageView and it's image property to update view's content dynamically. It gave a small performance improvements (the drawing is more stable at 19-22 fps). However it is still far from target 50-60fps. One think I've noticed is that updating only a dirty part of offscreen buffer image really makes sense - without forcing the view's content update, pure logic of redrawing offscreen buffer gives around 60 fps. But once I'm trying to attach the updated image to UIImageView.image property to update it's content the fps drops to mentioned 19-22. This is reasonable as assigning the property forces whole image to be redrawn on view's side.

So the question still remains - is there any way to updated only a specified portion of view's (UIImageView's) displaying content?

Upvotes: 4

Views: 1439

Answers (1)

vir us
vir us

Reputation: 10715

After spending several days I've came to unexpected (at least for myself) results. I was able to achieve 30fps on retina iPad which is acceptable result for now.

The trick that worked for me was:

  1. Subclass UIImageView
  2. Use UIImageView.image property to update content - it gives a better results comparing to ordinary UIView with setNeedsDisplay/setNeedsDisplayInRect methods.
  3. Use [self performSelector:@selector(setImage:) withObject:img afterDelay:0]; instead of just UIImageView.image = img to set the updated image to UIImageView.

The last point is a kind of spell for me now however it gives the minimal necessary frame rate (even if the original image is redrawn fully each frame not only dirty regions).

My guess if why performSelector helped to gain fps for updating view in my case is that scheduling setImage on view's queue optimizes possible internal idles which may occur during touch event processing. This is only my guess and if anyone can provide relevant explanation I'll be glad to post it here or accept that answer.

Upvotes: 1

Related Questions