大尺寸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(_:)方法,只绘制当前传入的小块区域:
这样每次只渲染可见的小 tile,内存占用会直接降到几十MB级别。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() } }
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
相关产品推荐
相关产品推荐

