23

I am trying to use CAShapeLayer to mask a CALayer in my iOS app as it takes a fraction of the CPU time to mask an image vs manually masking one in a bitmap context;

When I have several dozen or more images layered over each other, the CAShapeLayer masked UIImageView is slow to move to my touch.

Here is some example code:

UIImage *image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle]pathForResource:@"SomeImage.jpg" ofType:nil]];
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddEllipseInRect(path, NULL, CGRectMake(0.f, 0.f, image.size.width * .25, image.size.height * .25));

for (int i = 0; i < 200; i++) {

    SLTUIImageView *imageView = [[SLTUIImageView alloc]initWithImage:image];
    imageView.frame = CGRectMake(arc4random_uniform(CGRectGetWidth(self.view.bounds)), arc4random_uniform(CGRectGetHeight(self.view.bounds)), image.size.width * .25, image.size.height * .25);

    CAShapeLayer *shape = [CAShapeLayer layer];
    shape.path = path;
    imageView.layer.mask = shape;

    [self.view addSubview:imageView];
    [imageView release];

}
CGPathRelease(path);

With the above code, imageView is very laggy. However, it reacts instantly if I mask it manually in a bitmap context:

UIImage *image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle]pathForResource:@"3.0-Pad-Classic0.jpg" ofType:nil]];
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddEllipseInRect(path, NULL, CGRectMake(0.f, 0.f, image.size.width * .25, image.size.height * .25));


for (int i = 0; i < 200; i++) {

    UIGraphicsBeginImageContextWithOptions(CGSizeMake(image.size.width * .25, image.size.height * .25), NO, [[UIScreen mainScreen]scale]);
    CGContextRef ctx = UIGraphicsGetCurrentContext();

    CGContextAddPath(ctx, path);
    CGContextClip(ctx);

    [image drawInRect:CGRectMake(-(image.size.width * .25), -(image.size.height * .25), image.size.width, image.size.height)];
    UIImage *finalImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    SLTUIImageView *imageView = [[SLTUIImageView alloc]initWithImage:finalImage];
    imageView.frame = CGRectMake(arc4random_uniform(CGRectGetWidth(self.view.bounds)), arc4random_uniform(CGRectGetHeight(self.view.bounds)), finalImage.size.width, finalImage.size.height);

    [self.view addSubview:imageView];
    [imageView release];

}
CGPathRelease(path);

By the way, here is the code to SLTUIImageView, it's just a simple subclass of UIImageView that responds to touches (for anyone who was wondering):

-(id)initWithImage:(UIImage *)image{

    self = [super initWithImage:image];
    if (self) {
        self.userInteractionEnabled = YES;
    }
    return self;
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    [self.superview bringSubviewToFront:self];
}

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

    UITouch *touch = [touches anyObject];
    self.center = [touch locationInView:self.superview];
}

Is it possible to somehow optimize how the CAShapeLayer is masking the UIImageView so that the performance is improved? I have tried to find out where the bottle-neck is using the Time Profiler in Instruments, but I can't tell exactly what is causing it.

I have tried setting shouldRasterize to YES on both layer and on layer.mask but neither seem to have any effect. I'm not sure what to do.

Edit:

I have done more testing and find that if I use just a regular CALayer to mask another CALayer (layer.mask = someOtherLayer) I have the same performance issues. It seems that the problem isn't specific to CAShapeLayer—rather it is specific to the mask property of CALayer.

Edit 2:

So after learning more about using the Core Animation tool in Instruments, I learned that the view is being rendered offscreen each time it moves. Setting shouldRaster to YES when the touch begins and turning it off when the touch ends makes the view stay green (thus keeping the cache) in instruments, but performance is still terrible. I believe this is because even though the view is being cached, if it isn't opaque, than it still has to be re-rendered with each frame.

One thing to emphasize is that if there are only a few views being masked (say even around ten) the performance is pretty good. However, when you increase that to 100 or more, the performance lags. I imagine this is because when one moves over the others, they all have to be re-rendered.

My conclusion is this, I have one of two options.

First, there must be someway to permanently mask a view (render it once and call it good). I know this can be done via the graphic or bitmap context route as I show in my example code, but when a layer masks its view, it happens instantly. When I do it in a bitmap context as shown, it is quite slow (as in it almost can't even be compared how much slower it is).

Second, there must be some faster way to do it via the bitmap context route. If there is an expert in masking images or views, their help would be very much appreciated.

Ketan Parmar
  • 27,092
  • 9
  • 50
  • 75
daveMac
  • 3,041
  • 3
  • 34
  • 59
  • Your best bet is indeed to compress your 200 images you added in your for loop in 1 big image using something like the code mentioned here: http://stackoverflow.com/questions/4334233/how-to-capture-uiview-to-uiimage-without-loss-of-quality-on-retina-display You could also try use instruments, and use the time profiler to check where the bottleneck of your code is. – Sander Saelmans May 02 '16 at 18:40
  • Have you tried rendering the images ahead of time into their final needed state before using them in the app so that the app doesn't need to do all of this graphics processing, particularly since you have so many images to perform this processing on and they are being moved around so it keeps having to re-render all of them again constantly. I'm not sure if that's a viable option for you and if you tried it yet. It is also difficult to grasp what you're trying to do with the images without a picture, in this case it is important, if you attach a screenshot it would help. – Dmitry Samuylov Oct 12 '16 at 19:11
  • 1
    This is due to offscreen rendering as you already pointed. I don't think there is a away to optimize performance about the layer, but as you already seen you can optimize the process. One step further could be subclass the a UIView override the drawRect to achieve what you need. The drawRect is called only if your view is marked as dirty. Do not subclass UIImageView bacuse drawRect is never called. – Andrea Jan 30 '17 at 08:44

2 Answers2

1

You've gotten pretty far along and I believe are almost to a solution. What I would do is simply an extension of what you've already tried. Since you say many of these layers are "ending up" in final positions that remain constant relative to the other layers, and the mask.. So simply render all those "finished" layers to a single bitmap context. That way, every time you write out a layer to that single context, you'll have one less layer to worry about that is slowing down the animation/rendering process.

Alex Gray
  • 16,007
  • 9
  • 96
  • 118
0

Quartz (drawRect:) is slower than CoreAnimation for many reasons: CALayer vs CGContext drawRect vs CALayer. But it is necessary to use it correctly.

In the documentation you can see some advices. ImprovingCoreAnimationPerformance

If you want a hight performance, maybe you can try using AsyncDisplayKit. This framework allows to create smooth and responsive apps.

93sauu
  • 3,770
  • 3
  • 27
  • 43