UIImageをプログラムで再生成する際のパフォーマンス比較

## はじめに

通常だと`UIImageView`に画像を設定して`UIImageView`自体に角丸をつけたりすることが多いと思うのですが、`NSTextAttachment`で`UITextView`上などに画像を簡単に置きたい場合には`UIImage`で置くしかありません。そのため、`UIImage`の画像自体に角丸をつける必要があったので、ついでに描画のパフォーマンスを調べようと思います。

## 描画方法

#### その１

-  `UIImage`から`CGImage`を取り出してリサイズ
-  リサイズされた画像を`UIImageView`設定
-  マスク用の`UIBezierPath`も設定
-  `UIImageView`の`CALayer`を`CGContext`上に描画して、そこから新しい`UIImage`を生成

```swift
private struct SizeReletaion {
    private enum Element {
        case Height
        case Width
    }
    let longerSide: (value: CGFloat, element: Element)
    let shoterSide: (value: CGFloat, element: Element)
    init(size: CGSize, scale: CGFloat) {
        longerSide = size.width > size.height ? (size.width * scale, .Width) : (size.height * scale, .Height)
        shoterSide = size.width > size.height ? (size.height * scale, .Height) : (size.width * scale, .Width)
    }
    func centerTrimRect() -> CGRect {
        let delta = (longerSide.value - shoterSide.value) / 2
        let x = longerSide.element == .Height ? 0 : delta
        let y = longerSide.element == .Width ? 0 : delta
        return CGRect(x: x, y: y, width: shoterSide.value, height: shoterSide.value)
    }
    
    func relativeCenterTrimRect(targetSize: CGSize) -> CGRect {
        let delta = -(longerSide.value - shoterSide.value) / 2
        let x = longerSide.element == .Height ? 0 : delta
        let y = longerSide.element == .Width ? 0 : delta
        let width = longerSide.element == .Width ? longerSide.value : shoterSide.value
        let height = longerSide.element == .Height ? longerSide.value : shoterSide.value
        let scale = shoterSide.element == .Height ? targetSize.height / height : targetSize.width / width
        return CGRect(
            x: x * scale,
            y: y * scale,
            width: width * scale,
            height: height * scale
        )
    }
}

extension UIImage {
    func roundedCenterTrimImage1(cornerRadius: CGFloat, targetSize: CGSize) -> UIImage? {
        let sizeRelation = SizeReletaion(size: size, scale: scale)
        guard let newImage = trimmedImage(sizeRelation.centerTrimRect()) else {
            return nil
        }
        let scaledSize = CGSize(width: targetSize.width * scale, height: targetSize.height * scale)
        UIGraphicsBeginImageContext(scaledSize)
        let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: scaledSize.width, height: scaledSize.height), cornerRadius: cornerRadius)
        let imageView = UIImageView(image: newImage)
        imageView.frame = CGRect(x: 0, y: 0, width: scaledSize.width, height: scaledSize.height)
        imageView.layer.masksToBounds = true
        let maskLayer = CAShapeLayer()
        maskLayer.path = path.CGPath
        imageView.layer.mask = maskLayer
        guard let context = UIGraphicsGetCurrentContext() else {
            return nil
        }
        imageView.layer.renderInContext(context)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image
    }
    
    func trimmedImage(rect: CGRect) -> UIImage? {
        guard let cgImage = CGImageCreateWithImageInRect(self.CGImage, rect) else {
            return nil
        }
        return UIImage(CGImage: cgImage)
    }
}
```

<img width="375" alt="ss1.png" src="https://qiita-image-store.s3.amazonaws.com/0/60325/985d3428-6cfd-b0f0-0dd7-064a914cb6a5.png">

#### その2

- フルサイズの画像を`UIImageView`に設定
- マスク用の`UIBezierPath`も設定
- `UIImageView`の`CALayer`を`CGContext`上に描画して、そこから新しい`UIImage`を生成

```swift:UIImageViewとCGContext
extension UIImage {
    func roundedCenterTrimImage2(cornerRadius: CGFloat, targetSize: CGSize) -> UIImage? {
        let scaledSize = CGSize(width: targetSize.width * scale, height: targetSize.height * scale)
        UIGraphicsBeginImageContext(scaledSize)
        let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: scaledSize.width, height: scaledSize.height), cornerRadius: cornerRadius)
        let imageView = UIImageView(image: self)
        imageView.frame = CGRect(x: 0, y: 0, width: scaledSize.width, height: scaledSize.height)
        imageView.contentMode = .ScaleAspectFill
        imageView.layer.masksToBounds = true
        let maskLayer = CAShapeLayer()
        maskLayer.path = path.CGPath
        imageView.layer.mask = maskLayer
        guard let context = UIGraphicsGetCurrentContext() else {
            return nil
        }
        imageView.layer.renderInContext(context)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image
    }
}
```

<img width="375" alt="ss2.png" src="https://qiita-image-store.s3.amazonaws.com/0/60325/db57b8eb-5464-7a6a-4a08-0839b4228b5c.png">

#### その3

- `UIImage`とマスク用の`UIBezierPath`を`CGContext`に描画して、そこから新しい`UIImage`として生成

```swift:CGContext
extension UIImage {
    func roundedCenterTrimImage3(cornerRadius: CGFloat, targetSize: CGSize) -> UIImage? {
        let sizeRelation = SizeReletaion(size: size, scale: scale)
        let scaledSize = CGSize(width: targetSize.width * scale, height: targetSize.height * scale)
        UIGraphicsBeginImageContext(scaledSize)
        drawInRect(sizeRelation.relativeCenterTrimRect(scaledSize))
        let context = UIGraphicsGetCurrentContext()
        let path = roundedMaskPath(cornerRadius, targetSize: scaledSize)
        CGContextAddPath(context, path.CGPath)
        CGContextClip(context)
        CGContextClearRect(context, CGRect(x: 0, y: 0, width: scaledSize.width, height: scaledSize.height))
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image
    }
    
    private func roundedMaskPath(cornerRadius: CGFloat, targetSize: CGSize) -> UIBezierPath {
        struct CornerContainer {
            let corner: UIRectCorner, point: CGPoint
            func linePoint(cornerRadius: CGFloat) -> CGPoint {
                let operatorValue: CGFloat
                switch corner {
                case UIRectCorner.TopLeft, UIRectCorner.BottomLeft: operatorValue = 1
                case UIRectCorner.TopRight, UIRectCorner.BottomRight: operatorValue = -1
                default: return .zero
                }
                return CGPoint(x: point.x + (operatorValue * (cornerRadius)), y: point.y)
            }
            func curvePoint(cornerRadius: CGFloat) -> CGPoint {
                let operatorValue: CGFloat
                switch corner {
                case UIRectCorner.TopLeft, UIRectCorner.TopRight: operatorValue = 1
                case UIRectCorner.BottomRight, UIRectCorner.BottomLeft: operatorValue = -1
                default: return .zero
                }
                return CGPoint(x: point.x, y: point.y + (operatorValue * (cornerRadius )))
            }
        }
        
        let corners: [CornerContainer] = [
            CornerContainer(corner: .TopLeft, point: .zero),
            CornerContainer(corner: .TopRight, point: CGPoint(x: targetSize.width, y: 0)),
            CornerContainer(corner: .BottomRight, point: CGPoint(x: targetSize.width, y: targetSize.height)),
            CornerContainer(corner: .BottomLeft, point: CGPoint(x: 0, y: targetSize.height))
        ]
        
        let path = UIBezierPath()
        corners.forEach {
            let cornerRoundPath = UIBezierPath()
            cornerRoundPath.moveToPoint($0.point)
            cornerRoundPath.addLineToPoint($0.linePoint(cornerRadius))
            cornerRoundPath.addQuadCurveToPoint($0.curvePoint(cornerRadius), controlPoint: $0.point)
            cornerRoundPath.addLineToPoint($0.point)
            cornerRoundPath.closePath()
            path.appendPath(cornerRoundPath)
        }
        return path
    }
}
```

<img width="375" alt="ss3.png" src="https://qiita-image-store.s3.amazonaws.com/0/60325/97c9dfe4-13d3-8f25-bd8b-433a2b870485.png">

ちなみに計測の方法は以下のように
`UIImage`生成前の`NSDate`と生成後の`NSDate`の差になってます。

```swift:ViewController.swift
class ViewController: UIViewController {
    @IBOutlet var imageViews: [UIImageView]!
    @IBOutlet weak var baseImageView: UIImageView!
    @IBOutlet weak var textView: UITextView!

    @IBAction func didTapRender1(sender: UIButton) {
        let image = printRenderingTime(0) {
            return $0?.roundedCenterTrimImage1(8, targetSize: CGSize(width: 80, height: 80))
        }
        imageViews[0].image = image
    }
    
    @IBAction func didTapRender2(sender: UIButton) {
        let image = printRenderingTime(1) {
            return $0?.roundedCenterTrimImage2(8, targetSize: CGSize(width: 80, height: 80))
        }
        imageViews[1].image = image
    }
    
    @IBAction func didTapRender3(sender: UIButton) {
        let image = printRenderingTime(2) {
            return $0?.roundedCenterTrimImage3(8, targetSize: CGSize(width: 80, height: 80))
        }
        imageViews[2].image = image
    }
    
    private func printRenderingTime(index: Int, rendring: (UIImage?) -> UIImage?) -> UIImage? {
        let image = baseImageView.image
        let startDate = NSDate()
        let roundedImage = rendring(image)
        let finishDate = NSDate()
        let rendringTime = finishDate.timeIntervalSince1970 - startDate.timeIntervalSince1970
        let text = "Rendering\((index + 1)) Time: \(rendringTime)"
        textView.insertText(text)
        textView.insertText("\n")
        print(text)
        return roundedImage
    }
}
```

## 最後に

`CGImage`でリサイズしたものを`UIImageView`でマスクをかけたものを描画をして、`UIImage`にしたものが一番速かったです。
`CGContext`で生成したものが一番速いのかと思ってたのですが、意外な結果でした。