问题
I needed to draw a shadow around photos of users. I drew those circle photos by drawing a circle and then clipping the context. Here's the snippet of my code:
+ (UIImage*)roundImage:(UIImage*)img imageView:(UIImageView*)imageView withShadow:(BOOL)shadow
{
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, [UIScreen mainScreen].scale);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextAddEllipseInRect(context, CGRectMake(0,0, imageView.width, imageView.height));
CGContextSaveGState(context);
CGContextClip(context);
[img drawInRect:imageView.bounds];
CGContextRestoreGState(context);
if (shadow) {
CGContextSetShadowWithColor(context, CGSizeMake(0, 0), 5, [kAppColor lighterColor].CGColor);
}
CGContextDrawPath(context, kCGPathFill);
UIImage* roundImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return roundImage;
}
But after clipping the area I couldn't draw the shadow beneath. So I had to draw another circle with a shadow behind photos.
+ (UIImage *)circleShadowFromRect:(CGRect)rect circleDiameter:(CGFloat)circleDiameter shadowColor:(UIColor*)color
{
UIGraphicsBeginImageContextWithOptions(rect.size, NO, [UIScreen mainScreen].scale);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
CGFloat circleStartPointX = CGRectGetMidX(rect) - circleDiameter * 0.5;
CGFloat circleStartPointY = CGRectGetMidY(rect) - circleDiameter * 0.5;
CGContextAddEllipseInRect(context, CGRectMake(circleStartPointX,circleStartPointY, circleDiameter, circleDiameter));
CGContextSetShadowWithColor(context, CGSizeMake(0, 0), 5, color.CGColor);
CGContextDrawPath(context, kCGPathFill);
UIImage *circle = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return circle;
}
The problem with this approach is obviously it hits the performance of my app - drawing twice more circles plus it's the tableview. My question is how can I avoid drawing second circle and draw shadow after clipping the context? I did save and restore the state but it didn't help, I might be doing it wrong. I also assume that drawInRect closes the current path, that is why shadow doesn't know where to draw itself. Should I make another call to CGContextAddEllipseInRect and then draw the shadow?
回答1:
Regarding performance
Performance is interesting but often very unintuitive.
Developers, myself included, often have a sense of what's faster or more efficient, but it's seldom that simple. In practicality it's a tradeoff between straining the CPU, the GPU, consuming memory, code complexity, and more.
Any "poorly performing code" will have a bottleneck in one of the above, and any improvement that isn't targeting that bottleneck won't "improve performance" of that code. Which one ends up being the bottleneck will vary from case to case and may even vary from device to device. Even with extensive experience it's hard to predict what factor is the bottleneck. The only way to be sure is to measure (read: use Instruments) across real devices both before and after each change.
Drawing the shadow in the same image
That said, you can change the above code to draw the shadow in the same image as the rounded image, but it has to happen before the clipping.
Below is a Swift version of your code that does just that.
func roundedImage(for image: UIImage, bounds: CGRect, withShadow shadow: Bool) -> UIImage {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
defer {
UIGraphicsEndImageContext()
}
let context = UIGraphicsGetCurrentContext()
let circle = CGPathCreateWithEllipseInRect(bounds, nil)
if shadow {
// draw an elliptical shadow
CGContextSaveGState(context)
CGContextSetShadowWithColor(context, .zero, 5.0, UIColor.blackColor().CGColor)
CGContextAddPath(context, circle)
CGContextFillPath(context)
CGContextRestoreGState(context)
}
// clip to an elliptical shape, and draw the image
CGContextAddPath(context, circle)
CGContextClip(context)
image.drawInRect(bounds)
return UIGraphicsGetImageFromCurrentImageContext()
}
A few things to note:
- Passing 0.0 for the scale factor results in the scale factor of the device's main screen.
- The context is saved and restored when drawing the shadow, so that the shadow isn't calculated again when drawing the image.
This code doesn't extend the size of the image to account for the shadow. You'd either want to only extend the size when drawing the image (resulting in different image dimension with and without shadows) or always extend the size (resulting in empty space around images without shadows). It's up to you to pick which of those behaviors suite you best.
Alternatives to consider
This is more of a fun speculation regarding possible alternatives and their hypothetical performance differences. These suggestions are not meant to be taken strictly, but more to illustrate that there is no single "correct" solution.
The shadow being drawn is always the same, so you could hypothetically trade CPU cycles for memory by only drawing the shadow image once draw and then reusing it. Going further, you could even include an asset (at the cost of a larger bundle and the time to read from disk) for the shadow so that you wouldn't even have to draw it the first time.
The image with the shadow is still transparent, meaning that it will have to blend with the background (Note: blending is almost never a problem with todays hardware. This is more hypothetical.). You could pass a background color parameter to the function and have it generate an opaque image.
There is a cost to clipping the image. If the resulting image was opaque, it could include an asset with a background, circle, and circular cut-out pre-rendered in it (below rendered on top of an orange background). This way, the profile image could be drawn into the image context without clipping and the shadow image would be drawn above it.
The blending is still happening on the CPU. By adding a second layer with the pre-rendered shadow and background cut-out above, then work of blending can be moved from the CPU to the GPU.
And so on...
Another direction to go with this is to layer configuration. You can round the image with the layer's corner radius, and draw a shadow using the various shadow properties. As long as you remember to specify an explicit shadowPath
the performance difference should be small.
You can verify that last statement by profiling both alternatives with a release build on real devices. ;)
来源:https://stackoverflow.com/questions/36764268/cant-draw-shadow-after-clipping