【问题标题】:touchesMoved drawing in CAShapeLayer slow/laggytouchesMoved 在 CAShapeLayer 中缓慢/滞后的绘图
【发布时间】:2014-04-06 08:04:33
【问题描述】:

正如在之前的 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;
}

【问题讨论】:

  • 前几天我在这里发布了一个 WWDC 视频的链接。可能对你有用吗? stackoverflow.com/questions/22011115/…
  • 感谢您的链接,它有帮助。我能够使用分析器确定 86% 的 CPU 正在执行 [self.drawPath appendPath:path];但不知道如何解决。

标签: ios uiview calayer uibezierpath cashapelayer


【解决方案1】:

这个问题引起了我的兴趣,因为我一直发现 UIBezierPath/shapeLayer 的速度非常快。

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

附带说明,增加 lineWidth 和添加 lineCap 时存在明显的性能差异(无论采用何种方法)。为了比较 Apple 和 Apple,在下面的测试中,我将两者都保留为默认值。

我采用了您上面的代码并对其进行了一些更改。我使用的技术是在将当前渲染提交到图像之前,将接触点添加到 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

【讨论】:

  • 我现在有没有办法拥有 3.0 的 lineWidth 和 kCALineCapRound 的 lineCap?无论性能如何,这都是我需要的。
  • 当然,添加它们。我只是删除了它们,因为在您的原始实现中您没有它们,我希望比较是相同的。
猜你喜欢
  • 2017-06-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-11-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多