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

大尺寸UIView绘图内存过高致崩溃的原因与修复方案

嘿,这个问题我之前做长图手绘功能时也踩过一模一样的坑!咱们先把问题根源理清楚,再一步步解决:

问题原因分析

你的App内存直接飙到1GB濒临崩溃,核心原因有两个:

  • 超大尺寸UIView的绘制上下文爆炸:UIView的draw(_:)方法会创建一个和视图bounds完全一致的位图上下文。你的视图高40000px、宽3700px,算一下内存:每个RGBA像素占4字节,单上下文就需要 3700×40000×4 = 592MB,再加上绘图过程中的临时对象、路径缓存,轻松突破1GB。
  • 无差别全量重绘:每次触摸移动都调用setNeedsDisplay(),触发整个视图的重绘——意味着每次都要重新创建这个巨无霸上下文,还得遍历所有已绘制路径重新画一遍,内存根本来不及释放就被新的占用填满。

另外,drawLine()里的绘制是在CPU端完成的,没有利用GPU加速,大尺寸下CPU负载和内存占用都会雪上加霜。

修复方案

针对这个长图手绘的场景,最有效的方式是放弃全尺寸UIView的CPU绘制,改用分块渲染+GPU加速的方案,下面是几种可行的实现思路:

1. 用CATiledLayer实现分块绘制(适合超长大视图)

CATiledLayer会自动把大视图分割成小块(默认256×256),只渲染当前可见区域的块,完全避免创建全尺寸的位图上下文。

  • 首先修改DrawableView的layer类型:
    class DrawableView: UIView {
        override class var layerClass: AnyClass {
            return CATiledLayer.self
        }
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            // 配置tile大小,根据需求调整,建议512×512以内
            if let tiledLayer = layer as? CATiledLayer {
                tiledLayer.tileSize = CGSize(width: 512, height: 512)
            }
        }
        // ... 其他代码 ...
    }
    
  • 然后优化draw(_:)方法,只绘制当前传入的小块区域:
    override func draw(_ rect: CGRect) {
        debugPrint("drawing tile rect: \(rect)")
        UIColor.blue.setStroke()
        // 过滤掉不在当前tile范围内的路径,减少绘制量
        for line in drawingList {
            guard line.bounds.intersects(rect) else { continue }
            line.lineWidth = 4
            line.lineCapStyle = .round
            line.stroke()
        }
    }
    
    这样每次只渲染可见的小 tile,内存占用会直接降到几十MB级别。

2. 用CAShapeLayer替代CPU绘制(推荐,GPU加速更高效)

把每个手绘路径转换成CAShapeLayer,添加到视图的layer上,利用GPU加速渲染,完全绕过draw(_:)的巨量内存开销:

  • 修改触摸处理逻辑,实时更新shapeLayer的路径:
    class DrawableView: UIView {
        // 存储所有路径对应的shapeLayer
        private var drawingLayers: [CAShapeLayer] = []
        
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard touches.count == 1, let location = touches.first?.location(in: self) else { return }
            let path = UIBezierPath()
            path.move(to: location)
            
            // 创建对应的shapeLayer
            let shapeLayer = CAShapeLayer()
            shapeLayer.strokeColor = UIColor.blue.cgColor
            shapeLayer.lineWidth = 4
            shapeLayer.lineCap = .round
            shapeLayer.fillColor = UIColor.clear.cgColor
            shapeLayer.path = path.cgPath
            self.layer.addSublayer(shapeLayer)
            
            drawingLayers.append(shapeLayer)
            mLastPath = path
            prevPoint = location
            isFirst = true
        }
        
        override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard let touch = touches.first, let coalescedtouches = event?.coalescedTouches(for: touch) else { return }
            guard let lastPath = mLastPath, let lastLayer = drawingLayers.last else { return }
            
            for coalescedTouch in coalescedtouches {
                let locationInView = coalescedTouch.location(in: self)
                if let prevPoint = prevPoint {
                    let midPoint = CGPoint(x: (locationInView.x + prevPoint.x)/2, y: (locationInView.y + prevPoint.y)/2)
                    if isFirst {
                        lastPath.addLine(to: midPoint)
                    } else {
                        lastPath.addQuadCurve(to: midPoint, controlPoint: prevPoint)
                    }
                    isFirst = false
                } else {
                    lastPath.move(to: locationInView)
                }
                prevPoint = locationInView
            }
            
            // 直接更新shapeLayer的路径,Core Animation会自动GPU渲染
            lastLayer.path = lastPath.cgPath
            // 这里不需要调用setNeedsDisplay()!
        }
        // ... 其他代码 ...
    }
    
    这种方式下,内存占用只和路径数量、路径复杂度相关,和视图尺寸完全无关,手绘体验也会更流畅。

3. 紧急优化:缩小重绘范围(如果暂时不想改架构)

如果暂时没时间重构,至少要避免全量重绘,只重绘路径更新的区域:

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    // ... 你的路径拼接逻辑 ...
    // 计算本次路径更新的区域(加上线宽的缓冲)
    let lineWidth: CGFloat = 4
    let updateRect = midPoint.insetBy(dx: -lineWidth, dy: -lineWidth)
    // 只重绘这个小区域
    setNeedsDisplay(updateRect)
}

这样每次只重绘很小的一块区域,内存占用会大幅降低,但还是不如前两种方案彻底。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 03:51:48