图片处理是iOS应用中常见的性能瓶颈。本文介绍图片解码原理以及各种优化方案。
图片解码原理 为什么需要解码 图片文件(PNG、JPEG等)是压缩格式,无法直接显示。GPU需要的是位图(Bitmap)格式:
flowchart LR A[压缩图片PNG/JPEG] --> B[解码DecodeCPU密集操作] B --> C[位图BitmapGPU可直接使用] 位图大小 = 宽度 × 高度 × 每像素字节数 例如:1000×1000 RGBA图片 = 1000 × 1000 × 4 = 4MB
默认解码时机 // 加载图片(此时未解码) let image = UIImage(named: "large_image") // 设置到ImageView(仍未解码) imageView.image = image // 在 CATransaction commit 的 prepare 阶段,Core Animation 会在主线程对未解码的图片执行解码 // 未提前解码的图片一定会在此阶段被解码,从而阻塞主线程引发卡顿 异步解码 基本原理 将解码工作移到后台线程:
sequenceDiagram participant M as 主线程 participant B as 后台线程 M->>B: 请求加载图片 Note over M: 继续处理其他事件 B->>B: 加载压缩数据 B->>B: 解码为位图 B->>B: 创建CGImage B->>M: 返回解码后的图片 M->>M: 更新UI 实现方案1:强制解码 extension UIImage { /// 强制解码图片 func decodedImage() -> UIImage? { guard let cgImage = self.cgImage else { return nil } let width = cgImage.width let height = cgImage.height // 创建位图上下文 let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue) guard let context = CGContext( data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue ) else { return nil } // 绘制到上下文(触发解码) context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) // 从上下文创建新图片 guard let decodedCGImage = context.makeImage() else { return nil } return UIImage(cgImage: decodedCGImage, scale: scale, orientation: imageOrientation) } } // 异步使用 func loadImageAsync(named name: String, completion: @escaping (UIImage?) -> Void) { DispatchQueue.global(qos: .userInitiated).async { let image = UIImage(named: name)?.decodedImage() DispatchQueue.main.async { completion(image) } } } 实现方案2:使用ImageIO import ImageIO class ImageDecoder { static func decodeImage(from url: URL) -> UIImage? { guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else { return nil } let options: [CFString: Any] = [ kCGImageSourceShouldCache: true, kCGImageSourceShouldCacheImmediately: true // 立即解码 ] guard let cgImage = CGImageSourceCreateImageAtIndex(source, 0, options as CFDictionary) else { return nil } return UIImage(cgImage: cgImage) } static func decodeImageAsync(from url: URL, completion: @escaping (UIImage?) -> Void) { DispatchQueue.global(qos: .userInitiated).async { let image = ImageDecoder.decodeImage(from: url) DispatchQueue.main.async { completion(image) } } } } 实现方案3:使用UIGraphicsImageRenderer extension UIImage { func decodedImageUsingRenderer() -> UIImage { let format = UIGraphicsImageRendererFormat() format.scale = scale format.opaque = true format.preferredRange = .standard let renderer = UIGraphicsImageRenderer(size: size, format: format) return renderer.image { context in draw(at: .zero) } } } 图片降采样 为什么需要降采样 当图片尺寸远大于显示尺寸时,加载原图是浪费:
...