Pergunta

In my app I create a grid view by subclassing UIView. If the user touches a cell of the grid, it toggles between a number of different states e.g. 'ON' or 'OFF' and changes color accordingly. So when a cell is touched I call setNeedsDisplay to refresh the view.

Works great on my new iPhone 5s but horrible performance on iPhone 4 - my whole app freezes, audio stutters etc. Is there a better way to do this? How can I improve performance?

 /**
 * Draw rect
 */
-(void)drawRect:(CGRect)rect
{
    _cellWidth = _frameWidth / _gridSizeX;
    _cellHeight = _frameHeight / _gridSizeY;

    int firstCellY = _frameHeight - _cellHeight;

    for(int x = 0; x < _gridSizeX; x++) {
        for(int y = 0; y < _gridSizeY; y++) {

            CGRect rect = CGRectMake(x * _cellWidth, firstCellY - (y * _cellHeight), _cellWidth, _cellHeight);

            UIColor *strokeColor = [UIColor grid];
            UIColor *fillColor;

            if(currentBeat != x) {
                switch(mCells[x][y]) {
                    case OFF:
                        //bar lines
                        if(x % 4 == 0) {
                            fillColor = [UIColor bgBar];
                        }
                        //octave lines
                        else if(scaleLength > 0 && (y % scaleLength == 0 || y == _gridSizeY-1)) {
                            fillColor = [UIColor bgOctave];
                        }
                        else {
                            fillColor = [UIColor bgMain];
                        }
                        break;
                    case ON:
                        fillColor = [UIColor cellOn];
                        break;
                    case ON_EXTEND:
                        fillColor = [UIColor cellOnExtend];
                        break;
                    case LOCKED:
                        fillColor = [UIColor cellLocked];
                        break;
                    case LOCKED_EXTEND:
                        fillColor = [UIColor cellLockedExtend];
                        break;
                    default:
                        break;
                }
            }
            else {
                switch (mCells[x][y]) {
                    case OFF:
                        if(_playing) {
                            fillColor = [UIColor bgMainHL];
                        }
                        else {
                            fillColor = [UIColor bgBar];
                        }
                        break;
                    case ON:
                        if(_playing) {
                            fillColor = [UIColor cellOnHL];
                        }
                        else {
                            fillColor = [UIColor cellOn];
                        }
                        break;
                    case ON_EXTEND:
                        fillColor = [UIColor cellOnExtendHL];
                        break;
                    case LOCKED:
                        if(_playing) {
                            fillColor = [UIColor cellLockedHL];
                        }
                        else {
                            fillColor = [UIColor cellLocked];
                        }
                        break;
                    case LOCKED_EXTEND:
                        fillColor = [UIColor cellLockedExtendHL];
                        break;
                    default:
                        break;
                }
            }

            CGContextRef context = UIGraphicsGetCurrentContext();
            CGContextSetFillColorWithColor(context, fillColor.CGColor);
            CGContextSetStrokeColorWithColor(context, strokeColor.CGColor);
            CGContextFillRect(context, rect);
            CGContextStrokeRect(context, rect);
        }
    }
}


/**
 * Touch events
 */
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self handleTouches:touches withEvent:event];
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self handleTouches:touches withEvent:event];
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self handleTouches:touches withEvent:event];
}
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self handleTouches:touches withEvent:event];
}


/**
 * Handle touches
 */
-(void)handleTouches:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [[event allTouches] anyObject];
    CGPoint location = [touch locationInView:self];

    //Find column and row
    int col = (int) (location.x / _cellWidth);
    int row = (int) _gridSizeY - (location.y / _cellHeight);

    //Only redraw view when row and column changed, so doesn't redraw every touch that's registered
    if([touch phase] == UITouchPhaseBegan || (touchBeganCol != col || touchBeganRow != row)) {
        [self.delegate mViewTouchEventWithTouch:touch andRow:row andColumn:col];
        [self setNeedsDisplay];
        touchBeganCol = col;
        touchBeganRow = row;
        NSLog(@"redraw");
    }
    else if([touch phase] == UITouchPhaseEnded || [touch phase] == UITouchPhaseCancelled) {
        [self.delegate mViewTouchEventWithTouch:touch andRow:row andColumn:col];
        [self setNeedsDisplay];
        touchBeganCol = 99999;
        touchBeganRow = 99999;
        NSLog(@"redraw");
    }
}
Foi útil?

Solução

I followed some advice on another post and used CALayers instead of drawRect.

/**
 * Draw layers
 */
-(void)drawLayers
{
    for(CellLayer *layer in layerArray) {
        [layer removeFromSuperlayer];
    }
    [layerArray removeAllObjects];

    _cellWidth = _frameWidth / _gridSizeX;
    _cellHeight = _frameHeight / _gridSizeY;

    int firstCellY = _frameHeight - _cellHeight;

    UIColor *strokeColor = [UIColor grid];

    for(int x = 0; x < _gridSizeX; x++) {
        for(int y = 0; y < _gridSizeY; y++) {

            CellLayer *cellLayer = [CellLayer layer];
            cellLayer.frame = CGRectMake(x * _cellWidth, firstCellY - (y * _cellHeight), _cellWidth, _cellHeight);

            cellLayer.backgroundColor = [self getColourForState:mCells[x][y] andX:x andY:y].CGColor;
            cellLayer.borderColor = strokeColor.CGColor;
            cellLayer.borderWidth = 0.5;
            cellLayer.col = x;
            cellLayer.row = y;
            cellLayer.state = mCells[x][y];

            [layerArray addObject:cellLayer];
            [self.layer addSublayer:cellLayer];
        }
    }
}

Then as advised here, following the touch events I only change the properties of cells which need to be updated:

- (void)redrawChangedCells
{
    [CATransaction setDisableActions:YES];
    [[layerArray copy] enumerateObjectsUsingBlock:^(CellLayer *layer, NSUInteger idx, BOOL *stop) {
        if(mCells[layer.col][layer.row] != layer.state) {
            layer.backgroundColor = [self getColourForState:mCells[layer.col][layer.row] andX:layer.col andY:layer.row].CGColor;
            layer.state = mCells[layer.col][layer.row];
            [self updateLayerArrayWithLayer:layer andIndex:idx];
        }
    }];
}

Outras dicas

There are a few things that could help:
As a rule of thumb you should avoid as much as possible object allocations inside drawing code, since this is very expensive. In your code the next lines could be moved outside the double-for loop, and just reuse the variables.

       UIColor *strokeColor = [UIColor grid];
       UIColor *fillColor;

To extend the previous comment, and this is something I'm not sure of, it's possible that even calls like [UIColor bgBar] instantiate objects. I usually have class variables, such as _bgBarColor, already set with the apropriate color.

Another useful hint is just invalidate the rect that needs to be redrawn (there must be something like setNeedsDisplay:Rect to replace setNeedsDisplay). If you want to take this one further, in your draw code, you could skip drawing the cells that are outside the rect to be drawn.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top