Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Large custom UIView - CABackingStoreUpdate Performance

Why, on iOS 4.3.5, do 'large' (960 x 1380) custom UIView's perform CABackingStoreUpdate so inefficiently and how can I improve the performance of drawing operations?

Not entirely sure what I mean? Read on...

Note:

As my understanding of this problem has evolved, so has this question. As a result the question itself is similar but the code example and underlying details/reasoning in the following body of text have changed significantly since the question was first asked.

Context

I have an incredibly basic application (code at the bottom) that draws a single elipses in the drawRect: method of a custom UIView. The application demonstrates the difference in performance when the size of the elipses being drawn remains the same but the size of the custom UIView gets larger:

Small UIView

Large UIView

I ran the application on both an iPod 4th Gen running iOS 4.3.5 and an iPad 1st Gen running iOS 5.1.1 a series of times using custom UIViews of different sizes.
The following table displays the results taken from the time profiler instrument:

Time profiler results

The following instrument traces display the details of the two extremes for each device:

iOS 5.1.1 - (Custom UIView size 320 x 460)

iOS 5.1.1 - Custom UIView size 320 x 460

iOS 5.1.1 - (Custom UIView size 960 x 1380)

iOS 5.1.1 - Custom UIView size 960 x 1380

iOS 4.3.5 - (Custom UIView size 320 x 460)

iOS 4.3.5 - Custom UIView size 320 x 460

iOS 4.3.5 - (Custom UIView size 960 x 1380)

iOS 4.3.5 - Custom UIView size 960 x 1380

As you can (hopefully) see in 3 out of the 4 cases we get what we'd expect: the majority of time was spent performing the custom UIViews drawRect: method and each held 10fps.
But the forth case shows a plumet in performance with the application struggling to hold 7fps while only drawing a single shape. The majority of time was spent copying memory during the UIView's CALayer's display method, specifically:

[CALayer display] >
[CALayer _display] >
CABackingStoreUpdate >
CA::Render::ShmemBitmap::copy_pixels(CA::Render::ShmemBitmap const*, CGSRegionObject*) >
memcpy$VARIANT$CortexA8

Now it doesn't take a genius to see from the figures that something seriously wrong here. With a custom UIView of size 960 x 1380, iOS 4.3.5 spends over 4 times the amount of time copying memory around than it does drawing the entire view's contents.

Question

Now, given the context, I ask my question again:

Why, on iOS 4.3.5, do 'large' (960 x 1380) custom UIView's perform CABackingStoreUpdate so inefficiently and how can I improve the performance of drawing operations?

Any help is very much appreciated.

I have also posted this question on the Apple Developer forums.

The Real Deal

Now, obviously, I've reduced my real problem to the simplest reproducible case for the sake of this question. I'm actually attempting to animate a portion of a 960 x 1380 custom UIView that sits inside a UIScrollView.

Whilst I appreciate the temptation to steer anyone towards OpenGL ES when they're not achieving the level of performance they want through Quartz 2D I ask that anyone that takes that route at least offer an explanation as to why Quartz 2D is struggling to perform even the most basic of drawing operations on iOS 4.3.5 where iOS 5.1.1 has no problem. As you can imagine I'm not thrilled about the idea of re-writing everything for this cornerstone case. This also applies for people suggesting using Core Animation. Although I've used an elipses changing colour (a task perfectly suited for Core Animation) in the demo for the sake of simplicity, the drawing operations I'd actual like to perform are a large quantity of lines expanding over time, a drawing task Quartz 2D is ideal for (when it is performant!). Plus, again, this would require a re-write and doesn't help explain this odd performance problem.

Code

TViewController.m (Implementation of a standard view controller)

#import "TViewController.h"
#import "TCustomView.h"

// VERSION 1 features the custom UIView the same size as the screen.
// VERSION 2 features the custom UIView nine times the size of the screen.
#define VERSION 2


@interface TViewController ()
@property (strong, nonatomic) TCustomView *customView;
@property (strong, nonatomic) NSTimer *animationTimer;
@end


@implementation TViewController

- (void)viewDidLoad
{
    // Custom subview.
    TCustomView *customView = [[TCustomView alloc] init];
    customView.backgroundColor = [UIColor whiteColor];
#if VERSION == 1
    customView.frame = CGRectMake(0.0f, 0.0f, 320.0f, 460.0f);
#else
    customView.frame = CGRectMake(0.0f, 0.0f, 960.0f, 1380.0f);
#endif

    [self.view addSubview:customView];

    UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
    [customView addGestureRecognizer:singleTap];

    self.customView = customView;
}

#pragma mark - Timer Loop

- (void)handleTap:(UITapGestureRecognizer *)tapGesture
{
    self.customView.value = 0.0f;

    if (!self.animationTimer  || !self.animationTimer.isValid) {
        self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(animationLoop) userInfo:nil repeats:YES];
    }
}

#pragma mark - Timer Loop

- (void)animationLoop
{
    // Update model here. For simplicity, increment a single value.
    self.customView.value += 0.01f;

    if (self.customView.value >= 1.0f)
    {
        self.customView.value = 1.0f;
        [self.animationTimer invalidate];
    }

    [self.customView setNeedsDisplayInRect:CGRectMake(0.0f, 0.0f, 320.0f, 460.0f)];
}

@end

-

TCustomView.h (Custom view header)

#import <UIKit/UIKit.h>

@interface TCustomView : UIView
@property (assign) CGFloat value;
@end

-

TCustomView.m (Custom view implementation)

#import "TCustomView.h"

@implementation TCustomView

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

    // Draw ellipses.
    CGContextSetRGBFillColor(context, self.value, self.value, self.value, 1.0f);
    CGContextFillEllipseInRect(context, rect);

    // Draw value itself.
    [[UIColor redColor] set];
    NSString *value = [NSString stringWithFormat:@"%f", self.value];
    [value drawAtPoint:rect.origin withFont:[UIFont fontWithName:@"Arial" size:15.0f]];
}

@end
like image 815
Elliott James Perry Avatar asked Mar 03 '13 00:03

Elliott James Perry


1 Answers

Since both the iPod Touch 4th Gen and iPad 1st Gen have similar hardware (same amount of memory / same GPU) it suggests the problem you are seeing is due to an un-optimized code path in iOS4.

If you look at the size of the views that cause the (negative) performance spike on iOS4 they both have one side longer than 1024. Originally 1024x1024 was the maximum size a UIView could be, and whilst this restriction has since been lifted it is entirely likely that views larger than this only became efficient in iOS5 and later.

I'd conjecture that the excess memory copying you are seeing in iOS4 is due to UIKit using a full size memory buffer for the large UIView, but then having to copy appropriate sized tiles of it before they can be composited; and that in iOS5 and later they've either removed any restriction on the size of the tiles that can be composited, or changed the way UIKit renders for such large UIViews.

In terms of working around this bottleneck on iOS4 you can try tiling the area you want to cover with smaller UIViews. If you structure it as:

  Parent View - contains drawing and event related code
    Tile View 1 - contains drawRect
    ...
    Tile View n - contains drawRect

In each tile view, you can ask the parent view to render its contents after adjusting the graphics context's transform appropriately. This means you don't have to change the drawing code, it will just be invoked multiple times (there is a small overhead for this, but remember each invocation will be drawing only a portion of the whole view).

Note that its important that the parent view does not have a drawRect method - otherwise UIKit will think you want to draw into it directly and it will create a backing store thus putting you back in the same situation.

There is also CATiledLayer that you could look into - this does the tiling for you but asynchronously; meaning that your drawing code and such has to handle being executed from one or more background threads.

like image 98
runrevmark Avatar answered Nov 15 '22 19:11

runrevmark