NSMallocException崩溃:大图片模糊处理代码优化咨询
修复大图片模糊时的NSMallocException崩溃
崩溃原因分析
我之前也碰到过一模一样的问题!你遇到的Failed to grow buffer崩溃,本质是inProvider?.data背后调用的CGDataProviderCopyData方法,会尝试把整张图片的像素数据一次性复制到内存的连续缓冲区。当图片尺寸过大(比如几K分辨率的ARGB8888格式图片),系统根本拿不出这么大的连续内存块,直接就触发了内存分配异常。
解决方案:分块处理图片
我们完全不需要一次性加载整张图片到内存,而是把图片分割成多个小「块(tile)」,逐个处理每个块的模糊效果,最后把所有模糊后的块拼接成完整图片。这样每个块占用的内存极小,再也不会触发内存分配失败。
需要注意两个关键点:
- 分块时要保留一定的重叠区域,避免块与块之间出现生硬的拼接痕迹(毕竟box模糊需要用到周围的像素)
- 处理完每个块后及时释放临时内存,防止内存累积
修改后的代码
import CoreGraphics import Accelerate extension CGImage { func blurEffect(_ boxSize: CGFloat) -> CGImage? { let boxSize = boxSize - (boxSize.truncatingRemainder(dividingBy: 2)) + 1 let kernelRadius = Int((boxSize - 1) / 2) let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapInfo = self.bitmapInfo // 输出图片的基础参数 let outputWidth = self.width let outputHeight = self.height let bitsPerComponent = 8 let bytesPerPixel = 4 // ARGB8888格式 let bytesPerRow = outputWidth * bytesPerPixel // 创建最终输出的上下文 guard let outputContext = CGContext( data: nil, width: outputWidth, height: outputHeight, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo.rawValue ) else { return nil } // 分块大小(可根据设备内存调整,比如在低内存设备用512x512) let tileSize = 1024 // 每个块需要额外的边缘区域,用于模糊时的边界计算 let tilePadding = kernelRadius // 遍历所有分块 for y in stride(from: 0, to: outputHeight, by: tileSize) { for x in stride(from: 0, to: outputWidth, by: tileSize) { // 计算当前块的实际范围(包含padding) let tileX = max(0, x - tilePadding) let tileY = max(0, y - tilePadding) let tileWidth = min(outputWidth - tileX, tileSize + tilePadding * 2) let tileHeight = min(outputHeight - tileY, tileSize + tilePadding * 2) // 创建临时上下文,绘制当前块的原始像素 guard let tileContext = CGContext( data: nil, width: tileWidth, height: tileHeight, bitsPerComponent: bitsPerComponent, bytesPerRow: tileWidth * bytesPerPixel, space: colorSpace, bitmapInfo: bitmapInfo.rawValue ) else { continue } // 把原始图片的对应区域绘制到临时上下文 let drawRect = CGRect(x: -tileX, y: -tileY, width: outputWidth, height: outputHeight) tileContext.draw(self, in: drawRect) // 获取临时上下文的像素数据,准备模糊处理 guard let tileData = tileContext.data else { continue } var inBuffer = vImage_Buffer( data: tileData, height: vImagePixelCount(tileHeight), width: vImagePixelCount(tileWidth), rowBytes: tileContext.bytesPerRow ) // 分配模糊后的临时缓冲区 guard let outData = malloc(tileHeight * tileContext.bytesPerRow) else { continue } defer { free(outData) } // 处理完立即释放内存 var outBuffer = vImage_Buffer( data: outData, height: vImagePixelCount(tileHeight), width: vImagePixelCount(tileWidth), rowBytes: tileContext.bytesPerRow ) // 执行box模糊 vImageBoxConvolve_ARGB8888( &inBuffer, &outBuffer, nil, 0, 0, UInt32(boxSize), UInt32(boxSize), nil, vImage_Flags(kvImageEdgeExtend) ) // 创建模糊后的块图片 guard let blurredTileCGImage = CGContext( data: outBuffer.data, width: tileWidth, height: tileHeight, bitsPerComponent: bitsPerComponent, bytesPerRow: tileContext.bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo.rawValue )?.makeImage() else { continue } // 计算需要绘制到最终输出的区域(去掉padding部分) let destX = x let destY = y let destWidth = min(tileSize, outputWidth - x) let destHeight = min(tileSize, outputHeight - y) let sourceRect = CGRect( x: tilePadding, y: tilePadding, width: destWidth, height: destHeight ) // 将模糊后的块绘制到最终输出上下文 outputContext.draw(blurredTileCGImage, in: CGRect(x: destX, y: destY, width: destWidth, height: destHeight), source: sourceRect) } } // 返回最终的模糊图片 return outputContext.makeImage() } }
代码说明
- 分块策略:默认用
1024x1024的块大小,你可以根据设备内存情况调整(比如低内存设备改成512x512),每个块额外保留kernelRadius大小的边缘,确保模糊处理时边缘像素计算正确。 - 内存管理:用
defer { free(outData) }保证每个块处理完后立即释放临时缓冲区,不会造成内存泄漏。 - 边缘过渡:通过
kvImageEdgeExtend标记处理边缘像素,同时分块的重叠区域保证了块与块之间的过渡自然,不会出现拼接痕迹。
额外优化建议
- 如果不需要原尺寸的模糊效果,可以先把图片缩小到合适尺寸再模糊,能大幅减少内存占用和处理时间。
- 可以根据设备的内存状态动态调整分块大小,比如通过
ProcessInfo.processInfo.physicalMemory判断后设置不同的块尺寸。
内容的提问来源于stack exchange,提问作者Manuel
相关产品推荐
相关产品推荐

