touchesMoved绘图在CAShapeLayer慢/滞后

时间:2014-03-04 16:44:32

标签: ios uiview calayer uibezierpath cashapelayer

正如在之前的StackOverflow问题中向我建议的那样,我正在尝试改进我的绘图方法,让我的用户将线条/点绘制成UIView。我现在正在尝试使用CAShapeLayer而不是dispatch_async进行绘制。这一切都正常,但是,当触摸移动变得缓慢而且路径滞后时,不断地进入CAShapeLayer,而我的旧(效率低下我被告知)代码工作得非常流畅和快速。您可以在下面看到我的旧代码。

有没有办法改善我想做的表现?也许我正在过度思考。

我很感激提供任何帮助。

代码:

@property (nonatomic, assign) NSInteger center;
@property (nonatomic, strong) CAShapeLayer *drawLayer;
@property (nonatomic, strong) UIBezierPath *drawPath;
@property (nonatomic, strong) UIView *drawView;
@property (nonatomic, strong) UIImageView *drawingImageView;
CGPoint points[4];

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];

    self.center = 0;
    points[0] = [touch locationInView:self.drawView];

    if (!self.drawLayer)
    {
        CAShapeLayer *layer = [CAShapeLayer layer];
        layer.lineWidth = 3.0;
        layer.lineCap = kCALineCapRound;
        layer.strokeColor = self.inkColor.CGColor;
        layer.fillColor = [[UIColor clearColor] CGColor];
        [self.drawView.layer addSublayer:layer];
        self.drawView.layer.masksToBounds = YES;
        self.drawLayer = layer;
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
   UITouch *touch = [touches anyObject];

   self.center++;
   points[self.center] = [touch locationInView:self.drawView];

   if (self.center == 3)
   {
       UIBezierPath *path = [UIBezierPath bezierPath];

       points[2] = CGPointMake((points[1].x + points[3].x)/2.0, (points[1].y + points[3].y)/2.0);
       [path moveToPoint:points[0]];
       [path addQuadCurveToPoint:points[2] controlPoint:points[1]];
       points[0] = points[2];
       points[1] = points[3];
       self.center = 1;

       [self drawWithPath:path];
    }
}

- (void)drawWithPath:(UIBezierPath *)path
{
    if (!self.drawPath)
    {
        self.drawPath = [UIBezierPath bezierPath];
    }

    [self.drawPath appendPath:path];

    self.drawLayer.path = self.drawPath.CGPath;

    [self.drawLayer setNeedsDisplay];

    // Below code worked faster and didn't lag behind at all really

    /*

    dispatch_async(dispatch_get_main_queue(),
    ^{
        UIGraphicsBeginImageContextWithOptions(self.drawingImageView.bounds.size, NO, 0.0);

        [self.drawingImageView.image drawAtPoint:CGPointZero];

        [self.inkColor setStroke];
        [path stroke];

        self.drawingImageView.image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
    });

    */
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (self.center == 0)
    {
        UIBezierPath *path = [UIBezierPath bezierPath];
        [path moveToPoint:points[0]];
        [path addLineToPoint:points[0]];

        [self drawWithPath:path];
    }

    self.drawLayer = nil;
    self.drawPath = nil;
}

1 个答案:

答案 0 :(得分:2)

这个问题引起了我的兴趣,因为我总是发现UIBezierPath / shapeLayer速度相对较快。

重要的是要注意,在上面的代码中,您继续向drawPath添加点。随着这种情况的增加,appendPath方法成为真正的资源负担。同样,一遍又一遍地连续渲染相同的点是没有意义的。

作为旁注,增加lineWidth和添加lineCap(无论方法如何)都会有明显的性能差异。为了比较Apples和Apples,在下面的测试中,我将两者都保留为默认值。

我拿了你上面的代码并稍微改了一下。我使用的技术是在将当前渲染提交到图像之前,将触摸点添加到BezierPath,直到达到每个确定的数量。这与您的原始方法类似,但是,考虑到每个touchEvent都没有发生这种情况。它的CPU密集程度要低得多。我在最慢的设备(iPhone 4S)上测试了这两种方法,并注意到初始实现时的CPU利用率在绘制时始终保持在75-80%左右。使用修改后的/ CAShapeLayer方法,CPU利用率始终保持在10-15%左右。使用第二种方法,内存使用率仍然很小。

以下是我使用的代码;

@interface MDtestView () // a simple UIView Subclass
@property (nonatomic, assign) NSInteger cPos;
@property (nonatomic, strong) CAShapeLayer *drawLayer;
@property (nonatomic, strong) UIBezierPath *drawPath;
@property (nonatomic, strong) NSMutableArray *bezierPoints;
@property (nonatomic, assign) NSInteger pointCount;
@property (nonatomic, strong) UIImageView *drawingImageView;
@end


@implementation MDtestView
CGPoint points[4];

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
    // Initialization code
    }
    return self;
}

-(id)initWithCoder:(NSCoder *)aDecoder{
    self = [super initWithCoder:aDecoder];
    if (self) {
    //
    }
    return self;
 }



- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];

    self.cPos = 0;
    points[0] = [touch locationInView:self];

    if (!self.drawLayer)
    {
        // this should be elsewhere but kept it here to follow your code
        self.drawLayer = [CAShapeLayer layer];
        self.drawLayer.backgroundColor = [UIColor clearColor].CGColor;
        self.drawLayer.anchorPoint = CGPointZero;
        self.drawLayer.frame = self.frame;
        //self.drawLayer.lineWidth = 3.0;
       // self.drawLayer.lineCap = kCALineCapRound;
        self.drawLayer.strokeColor = [UIColor redColor].CGColor;
        self.drawLayer.fillColor = [[UIColor clearColor] CGColor];
        [self.layer  insertSublayer:self.drawLayer above:self.layer ];

        self.drawingImageView = [UIImageView new];
        self.drawingImageView.frame = self.frame;
        [self addSubview:self.drawingImageView];
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{

    UITouch *touch = [touches anyObject];
    if (!self.drawPath)
    {
        self.drawPath = [UIBezierPath bezierPath];
      //  self.drawPath.lineWidth = 3.0;
      //  self.drawPath.lineCapStyle = kCGLineCapRound;
    }

    // grab the current time for testing Path creation and appending
    CFAbsoluteTime cTime = CFAbsoluteTimeGetCurrent();

    self.cPos++;
    //points[self.cPos] = [touch locationInView:self.drawView];
    points[self.cPos] = [touch locationInView:self];
    if (self.cPos == 3)
    {


    /* uncomment this block to test old method


       UIBezierPath *path = [UIBezierPath bezierPath];

       [path moveToPoint:points[0]];
       points[2] = CGPointMake((points[1].x + points[3].x)/2.0, (points[1].y + points[3].y)/2.0);
       [path addQuadCurveToPoint:points[2] controlPoint:points[1]];
        points[0] = points[2];
        points[1] = points[3];
        self.cPos = 1;
        dispatch_async(dispatch_get_main_queue(),
                       ^{
                           UIGraphicsBeginImageContextWithOptions(self.drawingImageView.bounds.size, NO, 0.0);

                           [self.drawingImageView.image drawAtPoint:CGPointZero];
                          // path.lineWidth = 3.0;
                         //  path.lineCapStyle = kCGLineCapRound;
                           [[UIColor redColor] setStroke];
                           [path stroke];

                           self.drawingImageView.image = UIGraphicsGetImageFromCurrentImageContext();
                           UIGraphicsEndImageContext();
                           NSLog(@"it took %.2fms to draw via dispatchAsync", 1000.0*(CFAbsoluteTimeGetCurrent() - cTime));
                   });
   */

    // I've kept the original structure in place, whilst comparing apples for apples. we really don't need to create
    // a new bezier path and append it. We can simply add the points to the global drawPath, and zero it at an
    // appropriate point. This would also eliviate the need for appendPath
    // /*
        [self.drawPath moveToPoint:points[0]];
        points[2] = CGPointMake((points[1].x + points[3].x)/2.0, (points[1].y + points[3].y)/2.0);
        [self.drawPath addQuadCurveToPoint:points[2] controlPoint:points[1]];

        points[0] = points[2];
        points[1] = points[3];
        self.cPos = 1;
        self.drawLayer.path = self.drawPath.CGPath;

         NSLog(@"it took %.2fms to render %i bezier points", 1000.0*(CFAbsoluteTimeGetCurrent() - cTime), self.pointCount);

       // 1 point for MoveToPoint, and 2 points for addQuadCurve
        self.pointCount += 3;

         if (self.pointCount > 100) {
            self.pointCount = 0;
            [self commitCurrentRendering];
        }

  //  */
    }
}

- (void)commitCurrentRendering{
    CFAbsoluteTime cTime = CFAbsoluteTimeGetCurrent();
    @synchronized(self){
        CGRect paintLayerBounds = self.drawLayer.frame;
        UIGraphicsBeginImageContextWithOptions(paintLayerBounds.size, NO, [[UIScreen mainScreen]scale]);
        CGContextRef context = UIGraphicsGetCurrentContext();
        CGContextSetBlendMode(context, kCGBlendModeCopy);
        [self.layer renderInContext:context];
        CGContextSetBlendMode(context, kCGBlendModeNormal);
        [self.drawLayer renderInContext:context];
        UIImage *previousPaint = UIGraphicsGetImageFromCurrentImageContext();

        self.layer.contents = (__bridge id)(previousPaint.CGImage);
        UIGraphicsEndImageContext();
        [self.drawPath removeAllPoints];
    }
    NSLog(@"it took %.2fms to save the context", 1000.0*(CFAbsoluteTimeGetCurrent() - cTime));
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (self.cPos == 0)
    {
      /* //not needed
        UIBezierPath *path = [UIBezierPath bezierPath];
        [path moveToPoint:points[0]];
        [path addLineToPoint:points[0]];
        [self drawWithPath:path];
     */
    }
    if (self.cPos == 2) {
        [self commitCurrentRendering];
      }

   // self.drawLayer = nil;
    [self.drawPath removeAllPoints];
}

@end