Abstract

Off-screen rendering is when the GPU does not render the current screen buffer and creates a new buffer.

Why is there an off-screen rendering?

This is because the GPU prints layer by layer onto the canvas, but in some cases, you have to complete the output before you know what to do with the existing layers.

Typical scenario: Rounded corners.

Rounded corners

Personally, for rounded corners, there is only one layer, if it can be clipped in one pass, it will not trigger the off-screen rendering.

Beyond 1 level, such as adding child views, you can’t crop without masksToBounds, which triggers off-screen rendering.

So let’s try it out.

Rendered in the Color off-screen of the Simulator (Instruments is unavailable, only Xcode and the Simulator can be set)

CornerRadius > 0 & masksToBounds = true

Only 1 layer, set rounded corners, no off-screen rendering.

Rounded corners work with or without masksToBounds.

class ViewController: UIViewController {

    override func viewDidLoad(a) {
        super.viewDidLoad()
        let wh: CGFloat = 80
        let v = UIView()
        v.frame = .init(x: 50, y: 100, width: wh, height: wh);
        v.layer.cornerRadius = wh/2;
        v.layer.masksToBounds = true;
        // Setting the background color alone will not cause off-screen rendering, even if the background color is not solid.
        v.backgroundColor = .gray;
        view.addSubview(v)
    }
}
Copy the code

Beyond 1 layer, there will be an off-screen rendering

You must set masksToBounds for rounded corners to take effect.

// Add an SV to the original code
let sv = UIView(a)let p: CGFloat = 5
sv.frame = .init(x: p, y: p, width: wh-p*2, height: wh-p*2)
sv.backgroundColor = .white
v.addSubview(sv)
Copy the code

Analysis of the

For more than 1 layer, for example, the child view needs to be clipped because the parent view has rounded corners, so it can only be clipped when all the child views are rendered.

Therefore, THE GPU needs to output complete output in another area, then do clipping, then transfer to the cache area, and finally display.

This process can be represented as follows:

To sum up, it can be simply considered that:

If rounded corners require masksToBounds, off-screen rendering is triggered. Otherwise, it will not trigger even if it is set.

Specifically, after iOS9 UIImageView uses PNG and even cornerRadius > 0 & masksToBounds = true will not trigger off-screen rendering.

I’m curious, how does the system do that? If you know, please inform us. Thank you very much!

The solution

Setting rounded corners will cause off-screen rendering, and the GPU will waste a lot of time switching between two buffers.

So how do you reduce off-screen rendering?

  1. When off-screen rendering is unavoidable, caching can be used to reduce the frequency.
  2. Images can be cropped to avoid off-screen rendering.

The cache

Caching rendering results, although not to avoid off-screen rendering, but can reduce the number of times.

Note that render results have size limits and are out of date.

v.layer.shouldRasterize    = YES; // Cache view renders the content. If the view does not change, the cache can be directly displayed when drawing next time
v.layer.rasterizationScale = label.layer.contentsScale;
Copy the code

For example, if the four rounded corners are inconsistent, the layer + mask method can not avoid off-screen rendering, so it can be cached.

Example code for adding different rounded corners:

public extension UIView {
    func addRoundedCorners(_ corner: UIRectCorner.raddi: CGSize.fillColor: UIColor) {
        let maskLayer = self.mask(for: corner, raddi: raddi, fillColor: fillColor)
        self.layer.mask = maskLayer
    }

    func mask(for corner: UIRectCorner.raddi: CGSize.fillColor: UIColor) -> CALayer {
        let maskLayer = CAShapeLayer()
        maskLayer.frame = self.bounds
        let path = UIBezierPath(roundedRect: maskLayer.bounds, byRoundingCorners: corner, cornerRadii: raddi)
        maskLayer.fillColor = fillColor.cgColor
        maskLayer.backgroundColor = UIColor.clear.cgColor
        maskLayer.path = path.cgPath
        return maskLayer
    }
}
Copy the code

Images rounded corners

For the rounded images, show pictures before use UIGraphicsBeginImageContextWithOptions for tailoring rounded corners.

Sample code:

@implementation UIImage (Corner)

- (UIImage *)my_imageByRoundCornerRadius:(CGFloat)radius
                                  resize:(CGSize)resize
                                 corners:(UIRectCorner)corners
                             borderWidth:(CGFloat)borderWidth
                             borderColor:(UIColor *)borderColor
                          borderLineJoin:(CGLineJoin)borderLineJoin {
    if (resize.width <= 0 || resize.height <= 0 || isnan(resize.width) || isnan(resize.height) ) {
        return self;
    }
    if(corners ! =UIRectCornerAllCorners) {
        UIRectCorner tmp = 0;
        if (corners & UIRectCornerTopLeft) tmp |= UIRectCornerBottomLeft;
        if (corners & UIRectCornerTopRight) tmp |= UIRectCornerBottomRight;
        if (corners & UIRectCornerBottomLeft) tmp |= UIRectCornerTopLeft;
        if (corners & UIRectCornerBottomRight) tmp |= UIRectCornerTopRight;
        corners = tmp;
    }

    UIGraphicsBeginImageContextWithOptions(resize, NO[UIScreen mainScreen].scale);
    CGContextRef context = UIGraphicsGetCurrentContext(a);CGRect rect = CGRectMake(0.0, resize.width, resize.height);
    CGContextScaleCTM(context, 1.- 1);
    CGContextTranslateCTM(context, 0, -rect.size.height);

    CGFloat minSize = MIN(resize.width, resize.height);
    if (borderWidth < minSize / 2) {
        UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners cornerRadii:CGSizeMake(radius, borderWidth)];
        [path closePath];

        CGContextSaveGState(context);
        [path addClip];
        CGContextDrawImage(context, rect, self.CGImage);
        CGContextRestoreGState(context);
    }

    if (borderColor && borderWidth < minSize / 2 && borderWidth> 0) {
        CGFloat strokeInset = (floor(borderWidth * self.scale) + 0.5) / self.scale;
        CGRect strokeRect = CGRectInset(rect, strokeInset, strokeInset);
        CGFloat strokeRadius = radius > self.scale / 2 ? radius - self.scale / 2 : 0;
        UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:strokeRect byRoundingCorners:corners cornerRadii:CGSizeMake(strokeRadius, borderWidth)];
        [path closePath];

        path.lineWidth = borderWidth;
        path.lineJoinStyle = borderLineJoin;
        [borderColor setStroke];
        [path stroke];
    }

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext(a);UIGraphicsEndImageContext(a);return image;
}
Copy the code

Examples of Swift versions:

func my_drawRectWithRoundedCorner(radius radius: CGFloat.borderWidth: CGFloat.backgroundColor: UIColor.borderColor: UIColor) -> UIImage {
    UIGraphicsBeginImageContextWithOptions(sizeToFit, false.UIScreen.mainScreen().scale)
    let context = UIGraphicsGetCurrentContext(a)CGContextMoveToPoint(context, start position);// Start from the right of the start coordinate
    CGContextAddArcToPoint(context, x1, y1, x2, y2, radius);  // Repeat this type of code four times

    CGContextDrawPath(UIGraphicsGetCurrentContext().FillStroke)
    let output = UIGraphicsGetImageFromCurrentImageContext(a);UIGraphicsEndImageContext(a);return output
}
Copy the code

For iOS10+, use UIGraphicsImageRenderer instead of the above to avoid memory explosion due to image size.

summary

For rounded corners, with only 1 layer, it can be clipped in one traverse without triggering an off-screen rendering.

If you go beyond 1 level, such as adding child views, you can’t crop it without using masksToBounds, which triggers an off-screen render.

If rounded corners require masksToBounds, then off-screen rendering will be triggered. Otherwise, it will not trigger even if it is set.

So how do you reduce off-screen rendering?

  1. When off-screen rendering is unavoidable, caching can be used to reduce the frequency.
  2. For images, the pre-image can be cropped to avoid off-screen rendering.