如何解决Mac Catalyst主线程用UIGraphicsImageRenderer的Hang Risk警告?
问题现象
主线程调用UIGraphicsImageRenderer从已有图片生成新图时,iOS运行正常,但Mac Catalyst环境下(Xcode 16.4,macOS 15.4.1)会弹出紫色运行时警告:
Hang Risk: [Internal] Thread running at User-interactive quality-of-service class waiting on a lower QoS thread running at Default quality-of-service class. Investigate ways to avoid priority inversions
仅当UIImage从HEIC文件加载时触发该警告,加载JPEG图片无此问题,当前应用可正常运行,但存在潜在主线程阻塞风险。
相关代码
Objective-C版本
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:finalSize]; return [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) { // 警告触发行 [image drawInRect:CGRectMake(0, 0, finalSize.width, finalSize.height)]; }];
Swift版本
let renderer = UIGraphicsImageRenderer(size: finalSize) return renderer.image { context in // 警告触发行 image.draw(in: CGRect(x: 0, y: 0, width: finalSize.width, height: finalSize.height)) }
全程为主线程同步调用,未使用额外线程或GCD逻辑。
复现步骤
- 在Xcode中创建基于Storyboard的iOS新项目(Swift/Objective-C均可)
- 将一张HEIC图片添加到项目资源包(可通过AirDrop从iPhone获取)
- 替换
ViewController.swift代码为:
class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // 替换为你添加的HEIC文件名 let url = Bundle.main.url(forResource: "IMG_5149", withExtension: "HEIC")! let image = UIImage(contentsOfFile: url.path())! let thumb = scale(image: image) } func scale(image: UIImage) -> UIImage { let finalSize = CGSize(width: 100, height: 100) let renderer = UIGraphicsImageRenderer(size: finalSize) return renderer.image { context in // 运行时警告触发点 image.draw(in: CGRect(x: 0, y: 0, width: finalSize.width, height: finalSize.height)) } } }
- 为项目目标添加Mac Catalyst支持
- 在Mac上构建并运行应用,查看运行时警告(可忽略未使用变量的编译器警告)
原因分析与修复方案
问题原因
这是Mac Catalyst的底层实现问题,而非代码逻辑错误。HEIC图片的解码逻辑在Mac Catalyst下会在低QoS优先级的线程执行,而UIGraphicsImageRenderer的image方法在主线程(User-interactive QoS)同步等待解码完成,从而触发优先级反转警告。JPEG图片的解码路径在Mac Catalyst下是同步在主线程完成的,因此不会触发警告。
修复方案
异步渲染并切回主线程更新UI
将渲染逻辑移到低优先级队列执行,避免主线程等待低QoS线程:func scale(image: UIImage, completion: @escaping (UIImage) -> Void) { let finalSize = CGSize(width: 100, height: 100) DispatchQueue.global(qos: .userInitiated).async { let renderer = UIGraphicsImageRenderer(size: finalSize) let scaledImage = renderer.image { context in image.draw(in: CGRect(x: 0, y: 0, width: finalSize.width, height: finalSize.height)) } DispatchQueue.main.async { completion(scaledImage) } } } // 调用示例 override func viewDidLoad() { super.viewDidLoad() let url = Bundle.main.url(forResource: "IMG_5149", withExtension: "HEIC")! let image = UIImage(contentsOfFile: url.path())! scale(image: image) { thumb in // 在这里更新UI } }预解码HEIC图片
提前将HEIC图片绘制到临时上下文完成解码,后续渲染时使用已解码的图片:func preDecodedImage(_ image: UIImage) -> UIImage { UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale) image.draw(at: .zero) let decodedImage = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() return decodedImage } // 使用方式 let image = UIImage(contentsOfFile: url.path())! let decodedImage = preDecodedImage(image) let thumb = scale(image: decodedImage)临时忽略警告(不推荐)
如果应用当前运行稳定,可暂时通过Xcode的警告忽略功能屏蔽该提示,但长期来看建议采用前两种方案避免潜在的主线程阻塞风险。
内容的提问来源于stack exchange,提问作者HangarRash

