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处理 }
额外优化点
- 预缓存Stroke的路径:在
Stroke模型中添加cachedPath: UIBezierPath?属性,当样本添加完成后生成并缓存路径,避免每次绘制时重复遍历样本生成路径。 - 批量设置上下文状态:如果多个连续笔触的类型、颜色、宽度相同,可以一次性设置好上下文状态,再批量绘制这些笔触的路径,减少状态切换开销。
- 限制重绘区域:调用
setNeedsDisplay(_ rect: CGRect)时传入当前笔触的最小包围矩形,而不是整个视图,减少重绘范围。
内容的提问来源于stack exchange,提问作者bubobubobo
相关产品推荐
相关产品推荐

