You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift UIKit自定义PencilKit绘图性能优化方案咨询

优化UIKit自定义绘图卡顿的解决方案

针对你遇到的「笔触增多后全量重绘导致卡顿」的问题,以下是几种类似React Native Skia createPicture 缓存思路的优化方案,以及具体实现:

方案一:用UIImage缓存已完成笔触(最推荐)

核心思路是把已经绘制完成的所有笔触预先渲染成一张静态图片,每次重绘时只需要绘制这张图片,再叠加当前正在绘制的动态笔触,避免每次都遍历所有笔触重新生成路径和绘制。

代码修改示例:

class DrawingView: UIView {
    var strokeCollection: StrokeCollection?
    var strokeToDraw: Stroke?
    private var cachedStrokesImage: UIImage? // 缓存已完成笔触的图片

    // 完成当前笔触时调用,更新缓存
    func finishCurrentStroke() {
        guard let currentStroke = strokeToDraw else { return }
        strokeCollection?.strokes.append(currentStroke)
        strokeToDraw = nil
        updateCachedStrokesImage()
        setNeedsDisplay()
    }

    // 后台线程生成缓存图片,避免阻塞主线程
    private func updateCachedStrokesImage() {
        guard let strokes = strokeCollection?.strokes, !strokes.isEmpty else {
            cachedStrokesImage = nil
            return
        }

        DispatchQueue.global().async { [weak self] in
            guard let self = self else { return }
            UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, UIScreen.main.scale)
            defer { UIGraphicsEndImageContext() }

            for stroke in strokes {
                self.draw(stroke: stroke, in: self.bounds)
            }

            let cachedImage = UIGraphicsGetImageFromCurrentImageContext()
            DispatchQueue.main.async {
                self.cachedStrokesImage = cachedImage
                self.setNeedsDisplay()
            }
        }
    }

    override func draw(_ rect: CGRect) {
        // 先绘制缓存的已完成笔触图片
        cachedStrokesImage?.draw(in: bounds)

        // 再绘制当前正在绘制的笔触
        if let stroke = strokeToDraw {
            draw(stroke: stroke, in: rect)
        }
    }

    // 原有的draw(stroke:in:)方法保持不变
    func draw(stroke: Stroke, in rect: CGRect) {
        guard stroke.samples.isEmpty == false else { return }
        
        let path = UIBezierPath()
        var previousSample: StrokeSample?
        
        for sample in stroke.samples {
            if let prev = previousSample {
                let midPoint = midpoint(first: prev.location, second: sample.location)
                path.addQuadCurve(to: midPoint, controlPoint: prev.location)
            } else {
                path.move(to: sample.location)
            }
            previousSample = sample
        }
        
        if let lastSample = previousSample {
            path.addLine(to: lastSample.location)
        }
        
        if let context = UIGraphicsGetCurrentContext() {
            switch stroke.type {
            case .pen, .marker:
                context.setBlendMode(.normal)
                context.addPath(path.cgPath)
                context.setStrokeColor(stroke.color.cgColor)
                context.setLineWidth(stroke.width * 2)
                context.setAlpha(stroke.opacity)
                context.setLineCap(.round)
                context.setLineJoin(.round)
                context.strokePath()
            case .eraser:
                context.setBlendMode(.clear)
                context.addPath(path.cgPath)
                context.setStrokeColor(stroke.color.cgColor)
                context.setLineWidth(stroke.width * 2)
                context.setAlpha(stroke.opacity)
                context.setLineCap(.round)
                context.setLineJoin(.round)
                context.strokePath()
            }
        }
    }
}

方案二:用CALayer分层管理笔触

给每个已完成的笔触创建单独的CAShapeLayer,已完成的笔触layer会被系统缓存,不需要重复绘制;只需要实时更新当前正在绘制的笔触对应的layer即可。

代码修改示例:

class DrawingView: UIView {
    var strokeCollection: StrokeCollection?
    var strokeToDraw: Stroke?
    private var currentDrawingLayer: CAShapeLayer?

    // 开始新笔触时创建对应的layer
    func startNewStroke(with initialSample: StrokeSample) {
        let stroke = Stroke(samples: [initialSample], type: .pen, color: .black, width: 2, opacity: 1)
        strokeToDraw = stroke

        let layer = CAShapeLayer()
        layer.strokeColor = stroke.color.cgColor
        layer.lineWidth = stroke.width * 2
        layer.lineCap = .round
        layer.lineJoin = .round
        layer.opacity = Float(stroke.opacity)
        layer.blendMode = stroke.type == .eraser ? .clear : .normal
        
        currentDrawingLayer = layer
        layer.addSublayer(layer)
    }

    // 更新当前笔触的路径
    func updateCurrentStroke(with sample: StrokeSample) {
        guard var stroke = strokeToDraw, let layer = currentDrawingLayer else { return }
        stroke.samples.append(sample)

        let path = UIBezierPath()
        var previousSample: StrokeSample?
        for s in stroke.samples {
            if let prev = previousSample {
                let midPoint = midpoint(first: prev.location, second: s.location)
                path.addQuadCurve(to: midPoint, controlPoint: prev.location)
            } else {
                path.move(to: s.location)
            }
            previousSample = s
        }
        if let last = previousSample {
            path.addLine(to: last.location)
        }
        layer.path = path.cgPath

        strokeToDraw = stroke
    }

    // 完成当前笔触,保存layer并清空临时变量
    func finishCurrentStroke() {
        guard let stroke = strokeToDraw else { return }
        strokeCollection?.strokes.append(stroke)
        strokeToDraw = nil
        currentDrawingLayer = nil
    }

    // 不需要重写draw方法,所有绘制由layer处理
}

额外优化点

  1. 预缓存Stroke的路径:在Stroke模型中添加cachedPath: UIBezierPath?属性,当样本添加完成后生成并缓存路径,避免每次绘制时重复遍历样本生成路径。
  2. 批量设置上下文状态:如果多个连续笔触的类型、颜色、宽度相同,可以一次性设置好上下文状态,再批量绘制这些笔触的路径,减少状态切换开销。
  3. 限制重绘区域:调用setNeedsDisplay(_ rect: CGRect)时传入当前笔触的最小包围矩形,而不是整个视图,减少重绘范围。

内容的提问来源于stack exchange,提问作者bubobubobo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 00:09:54