Swift绘图APP中CALayer合并引发发热与性能问题的优化求助
Swift绘图APP中CALayer合并导致性能下降与设备发热的优化求助
我开发了一款Swift绘图APP,采用CALayer实现不同纹理的各类画笔,但在合并CALayer时出现设备发热及性能下降问题。目前已尝试:
- 使用Instruments分析内存与性能问题
- 测试不同的合并技术与优化手段
问题仍未解决,希望获取CALayer合并优化的思路、建议及最佳实践,以提升性能并避免设备发热。同时想了解是否有开发者在Swift绘图APP场景中遇到类似问题并找到有效方案。
补充需求:寻求高效合并CALayer的优化方案或替代方案,避免影响性能,重点针对iPhone设备场景。
代码片段
// // Brush1.swift // sketch-book // // Created by Rezwan on 3/27/24. // import UIKit import AVKit class Brush1: UIBezierPath, SketchTool { var path: CGMutablePath var lineColor: UIColor var lineAlpha: CGFloat var drawingPenType: PenType // new Variavle start var shape: UIImage! var shape2: UIImage! var pts: [CGPoint] = [] var pts2: [CGPoint] = [] var shapeSize: CGSize = CGSize(width: 40, height: 40) var pixelsPerIteration: CGFloat = 1.0 var loopIndex = 0 var layerarray:[CALayer] = [] var previousPoint: CGPoint! public var drawLayer = CALayer() public var drawLayer2 = CALayer() public var original = CALayer() // new variable end var setting:BrushSetting! override init() { path = CGMutablePath.init() lineColor = .black lineAlpha = 0 drawingPenType = .normal super.init() lineCapStyle = CGLineCap.round } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setInitialPoint(_ firstPoint: CGPoint) { print("------Brush1") drawLayer2.frame = drawLayer.bounds original.frame = drawLayer.bounds drawLayer.addSublayer(drawLayer2) drawLayer.addSublayer(original) // reset points array to only the starting point //print("Start:", startPoint) loopIndex = 0 pts = [firstPoint] pts2 = [firstPoint] layerarray = [] previousPoint = nil draw() } func finishDrawing(){ let startDate = Date() print("----finishDrawing") self.screenshotOfLayer(layer: self.drawLayer2) { image in let endDate = Calendar.current.date(byAdding: .hour, value: 2, to: startDate)! let secondsDifference = endDate.timeIntervalSince(startDate) print("Difference in seconds: \(secondsDifference)") guard let image = image else{return} let imageLayer = self.createLayerFromImage(image: image) imageLayer.name = "Brush" self.layerarray = [ imageLayer] // imageLayer.backgroundColor = UIColor.red.cgColor // imageLayer.bounds = drawLayer.bounds // imageLayer.contents = image.cgImage // imageLayer.compositingFilter = setting.filter // imageLayer.minificationFilterBias = 10 self.drawLayer2.isHidden = true // self.drawLayer.addSublayer(imageLayer) self.drawLayer2.removeFromSuperlayer() self.drawLayer2 = CALayer() } } func moveFromPoint(_ startPoint: CGPoint, toPoint endPoint: CGPoint) { let x = Int(endPoint.x) let y = Int(endPoint.y) pts.append(CGPoint(x: x, y: y)) pts2 = interpolatePoints(from: pts) // setNeedsDisplay() } func createBezierRenderingBox(_ previousPoint2: CGPoint, widhPreviousPoint previousPoint1: CGPoint, withCurrentPoint cpoint: CGPoint) -> CGRect { let mid1 = middlePoint(previousPoint1, previousPoint2: previousPoint2) let mid2 = middlePoint(cpoint, previousPoint2: previousPoint1) let subpath = CGMutablePath.init() subpath.move(to: CGPoint(x: mid1.x, y: mid1.y)) subpath.addQuadCurve(to: CGPoint(x: mid2.x, y: mid2.y), control: CGPoint(x: previousPoint1.x, y: previousPoint1.y)) path.addPath(subpath) var boundingBox: CGRect = subpath.boundingBox boundingBox.origin.x -= lineWidth * 2.0 boundingBox.origin.y -= lineWidth * 2.0 boundingBox.size.width += lineWidth * 4.0 boundingBox.size.height += lineWidth * 4.0 return boundingBox } private func middlePoint(_ previousPoint1: CGPoint, previousPoint2: CGPoint) -> CGPoint { return CGPoint(x: (previousPoint1.x + previousPoint2.x) * 0.5, y: (previousPoint1.y + previousPoint2.y) * 0.5) } } extension Brush1{ func interpolatePoints(from array: [CGPoint]) -> [CGPoint] { var result: [CGPoint] = [] var prevPoint: CGPoint? for point in array { if let prev = prevPoint { let diffX = point.x - prev.x let diffY = point.y - prev.y if abs(diffX) != 0 || abs(diffY) != 0 { let steps = max(abs(diffX), abs(diffY)) for i in 0..<Int(steps ){ let newX = prev.x + (diffX * CGFloat(i) / CGFloat(steps)) let newY = prev.y + (diffY * CGFloat(i) / CGFloat(steps)) if (Int(prev.x) == Int(newX) && Int(prev.y) == Int(newY)){ }else{ let p = CGPoint(x: Double(Int(newX)), y: Double(Int(newY))) result.append(p) } } } } if let p = prevPoint{ if (Int(p.x) == Int(point.x) && Int(p.y) == Int(point.y)){ }else{ result.append(CGPoint(x: Int(point.x), y: Int(point.y))) } } prevPoint = point } if (result.count > 0){ result.remove(at: 0) } return result } func draw2() { for i in layerarray{ self.drawLayer.addSublayer(i) } } func draw() { guard pts2.count > 0 else { return } for idx in loopIndex..<(pts2.count) { let startx: CGFloat = pts2[idx].x let starty: CGFloat = pts2[idx].y let dstRect: CGRect = .init(x: 0.0, y: 0.0, width: shapeSize.width, height: shapeSize.height) let x: CGFloat = startx let y: CGFloat = starty if isAdd(p: pts2[idx], space: getSpace()){ previousPoint = pts2[idx] let imageLayer = CAReplicatorLayer() // imageLayer.backgroundColor = UIColor.red.cgColor if setting.angle{ imageLayer.rotate(angle: self.setting.angles.randomElement()!) }else{ imageLayer.rotate(angle: CGFloat(self.setting.degree)) } let maxScale:CGFloat = 1.0 let s = 1.0 - setting.dynamicScale let scale:CGFloat = CGFloat(s)+(CGFloat(idx)/400.0) <= maxScale ? CGFloat(s)+(CGFloat(idx)/400.0) : maxScale if setting.isAnimation{ // DispatchQueue.main.asyncAfter(deadline: .now()+0.02) { imageLayer.transform = CATransform3DScale(imageLayer.transform, scale, scale, 1) // } }else{ imageLayer.transform = CATransform3DScale(imageLayer.transform, scale, scale, 1) } let minopacity:CGFloat = CGFloat(1.0-setting.dynamicOpacity) let dunamicOpacity:CGFloat = minopacity+(CGFloat(idx)/300.0) <= CGFloat(setting.opacity) ? minopacity+(CGFloat(idx)/300.0) : CGFloat(setting.opacity) imageLayer.opacity = Float(dunamicOpacity) imageLayer.bounds = dstRect imageLayer.position = CGPoint(x:x ,y:y) imageLayer.contents = shape?.cgImage imageLayer.name = "Brush" imageLayer.compositingFilter = setting.filter imageLayer.minificationFilterBias = 10 // layerarray.append(imageLayer) // self.draw.addSublayer(imageLayer) // self.drawLayer.addSublayer(imageLayer) self.drawLayer2.addSublayer(imageLayer) } loopIndex = idx } } func isAdd(p:CGPoint, space:Int)->Bool{ if let previousPoint = previousPoint{ let a = max((abs(previousPoint.x - p.x)),(abs(previousPoint.y - p.y))) if a >= (CGFloat(space)){ return true }else{ //print(a) return false } }else{ return true } } func getSpace()->Int{ print(shapeSize) return Int(setting.distance) } func setShape(brush:BrushShape){ shape = brush.image updateSetting(setting:brush.setting) } func updateSetting(setting:BrushSetting){ let newSize = AVMakeRect( aspectRatio: shape.size, insideRect: CGRect( origin: .zero, size: CGSize( width: Int(setting.size), height: Int(setting.size)))).size self.shapeSize = newSize self.setting = setting } } extension Brush1{ func createLayerFromImage(image: UIImage) -> CALayer { original.contents = image.cgImage original.frame = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height) return original } func screenshotOfLayer(layer: CALayer, completion: @escaping (UIImage?) -> Void) { DispatchQueue.global(qos: .userInitiated).async { // Create a bitmap context with the size of the layer UIGraphicsBeginImageContextWithOptions(layer.bounds.size, false, UIScreen.main.scale) // Get the current context guard let context = UIGraphicsGetCurrentContext() else { UIGraphicsEndImageContext() DispatchQueue.main.async { completion(nil) } return } // Render the layer into the bitmap context layer.render(in: context) // Get the image from the context guard let screenshotImage = UIGraphicsGetImageFromCurrentImageContext() else { UIGraphicsEndImageContext() DispatchQueue.main.async { completion(nil) } return } // End the context UIGraphicsEndImageContext() // Call the completion handler with the screenshot image DispatchQueue.main.async { completion(screenshotImage) } } } } typealias Dispatch = DispatchQueue extension Dispatch { static func background(_ task: @escaping () -> ()) { Dispatch.global(qos: .userInteractive).async { task() } } static func main(_ task: @escaping () -> ()) { Dispatch.main.async { task() } } }
核心优化思路与实践
1. 减少CALayer层级与数量
- 避免创建大量
CAReplicatorLayer:当前draw()方法每绘制一个点就创建一个独立图层,会导致图层数量爆炸,大幅增加CPU/GPU渲染负载。建议直接在CGContext中绘制纹理图片,而非每个点对应一个图层。 - 批量合并图层:不要等到
finishDrawing才合并,可在用户绘制过程中定期(比如每绘制50个点)将临时图层合并为一张图片,减少活跃图层数量。
2. 优化图层合并逻辑
- 替换低效渲染方式:
UIGraphicsBeginImageContextWithOptions处理大尺寸图层时内存拷贝开销大,建议使用CGBitmapContextCreate直接创建位图上下文,提升渲染效率。 - 避免主线程阻塞:
layer.render(in:)会占用大量CPU资源,合并操作尽量在后台队列执行,同时禁用CATransaction隐式动画减少额外开销:CATransaction.begin() CATransaction.setDisableActions(true) // 执行图层合并操作 CATransaction.commit()
3. 替代方案:放弃CALayer,改用Core Graphics/Metal直接绘制
- Core Graphics方案:维护一个主画布
UIImage,每次绘制时在内存中更新该图片,仅用一个UIImageView显示,完全避免图层管理开销:var canvasImage: UIImage? let canvasSize = CGSize(width: 1024, height: 1024) func drawOnCanvas(points: [CGPoint]) { UIGraphicsBeginImageContextWithOptions(canvasSize, false, UIScreen.main.scale) canvasImage?.draw(at: .zero) // 批量绘制纹理点 for point in points { let drawRect = CGRect( x: point.x - shapeSize.width/2, y: point.y - shapeSize.height/2, width: shapeSize.width, height: shapeSize.height ) shape.draw(in: drawRect) } canvasImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() imageView.image = canvasImage } - Metal方案:对于高性能绘图场景,Metal可利用GPU并行计算,大幅降低CPU负载,避免设备发热。将纹理点数据传入GPU批量绘制,适合复杂纹理画笔场景。
4. 细节优化
- 移除不必要的图层属性:
compositingFilter和minificationFilterBias会增加GPU渲染开销,若非核心功能建议关闭。 - 优化点插值逻辑:
interpolatePoints方法循环次数过多,可降低插值精度(比如减少步数),在保证视觉效果的前提下减少绘制点数量。 - 复用图层对象:若必须使用图层,创建图层池复用已创建的图层,避免频繁创建销毁对象带来的内存开销。
内容的提问来源于stack exchange,提问作者Rezwan Khan chowdhury
相关产品推荐
相关产品推荐

