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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 00:17:02