views:

199

answers:

2

This is a rather simple example and probably wouldn't make much of a difference anyway, but say I have this drawing code in a view to draw a gradient:

@interface SomeView : UIView
@end

@implementation SomeView

- (void)drawRect:(CGRect)rect
{
    const CGContextRef ctx = UIGraphicsGetCurrentContext();

    // Set fill color to white
    CGContextSetGrayFillColor(ctx, 1.0f, 1.0f);
    CGContextFillRect(ctx, rect);

    // Create a fancy, albeit ugly, orange gradient
    const CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
    const CGFloat components[] = { 1.0, 0.5, 0.4, 1.0,   // Start color
              0.8, 0.8, 0.3, 1.0 }; // End color
    CGGradientRef gloss;
    gloss = CGGradientCreateWithColorComponents(rgbColorSpace, components, NULL, 2);
    CGColorSpaceRelease(rgbColorSpace);

    // Draw the gradient
    const CGPoint endPoint = {rect.origin.x,
                              rect.origin.y + floor(rect.size.height / 2.0f)};
    CGContextDrawLinearGradient(ctx, gloss, rect.origin, endPoint, 0);
    CGGradientRelease(gloss);
}

@end

I realize this is a very negligible example, but you can imagine the concern if I had more complex values to reuse. Is it necessary to cache these, or does Cocoa-Touch essentially do that for you with CALayers?

Here's an example of what I mean by caching:

@interface SomeView : UIView
{
    CGGradientRef gloss;
}
@end

@implementation SomeView

- (id)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
     // Create a fancy, albeit ugly, orange gradient only once here instead
     const CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
     const CGFloat components[] = { 1.0, 0.5, 0.4, 1.0,   // Start color
               0.8, 0.8, 0.3, 1.0 }; // End color
     CGGradientRef gloss;
     gloss = CGGradientCreateWithColorComponents(rgbColorSpace, components, NULL, 2);
     CGColorSpaceRelease(rgbColorSpace);
    }
    return self;
}

- (void)dealloc
{
    CGGradientRelease(gradient);
    [super dealloc];
}

- (void)drawRect:(CGRect)
{
    const CGContextRef ctx = UIGraphicsGetCurrentContext();

    // Set fill color to white
    CGContextSetGrayFillColor(ctx, 1.0f, 1.0f);
    CGContextFillRect(ctx, rect);

    // Draw the gradient
    const CGPoint endPoint = {rect.origin.x,
                              rect.origin.y + floor(rect.size.height / 2.0f)};
    CGContextDrawLinearGradient(ctx, gloss, rect.origin, endPoint, 0);
}

@end

You can obviously see the tradeoff here; especially if I had a lot of these views, it could end up taking more memory with this technique vs. possibly worse drawing performance in the former. However, I'm not even sure if there is much of a tradeoff because I don't know what magic Cocoa is doing behind the scenes. Could anyone explain?

+1  A: 

The only thing "cached" is the result of the drawRect: message. It is cached until invalidated, in which case the message is called again.

Cocoa, and Cocoa-Touch, won't cache objects you use in your methods. You could cache them yourself, as you did in your second example. However, I suggest testing such optimizations using a profiler such as Instruments, just to make sure you're not over-complicating your code for not a lot of benefit..

Aviad Ben Dov
When is it invalidated?
Michael
+1 As to when it's invalidated, many things can invalidate it, and it's possible to invalidate only part of the view (which is why -drawRect: passes you a CGRect to tell you which part was invalidated). You calling -setNeedsDisplay invalidates it of course. When the view comes on the screen. When the "state" of the view changes (highlighting for instance). When the size of the view changes. Sometimes during scrolling (though there are some bitmap cache optimizations there).
Rob Napier
In further commentary, Apple recommends against drawing gradients yourself if you can help it. iPhone isn't always fast enough to do it. Apple's recommendation is to use images. Gradients are often handled with single-pixel-wide images which are then stretched horizontally to fill the desired area.
Rob Napier
A: 

Each UIView has its own offscreen drawing buffer, so moving a UIView around or altering its visibility will not cause the UIView to be redrawn. As already mentioned there are specific instances that will trigger a redraw, but as long as you don't cause those to occur your drawRect routine shouldn't be called. (As an experiment, try starting your drawRect routine with an NSLog call to watch when it's getting invoked.) That'll get you one step closer in making drawing "fast" - by limiting the number of times drawing actually takes place.

You should also be able to use performance measurement tools like Shark to see how long your drawing routines are taking, and what's taking the most time within them. This analysis in particular is useful to keep from caching something you think might be expensive when really it isn't, causing you time and complexity for very little performance.

fbrereto