iPhone锁屏时Widget读取App Group文件调用Data(contentsOf:)抛出异常
问题描述
设备处于激活状态时,Widget可正常读取App Group中的JSON文件;但设备锁屏/休眠时,调用Data(contentsOf:)读取文件会抛出异常。
背景细节
- 共享文件存储的是336字节的
Event结构体数组,用于iPhone主应用与Widget同步天气事件数据 - 设备激活期间,无论主应用是否运行,Widget都能正常读写文件、更新Timeline,功能完全正常
- 锁屏后,Widget在执行
try Data(contentsOf: FileManager.appEventShare)时直接抛出异常,且异常发生在任何天气预报网络请求之前
复现代码
func getTimeline(in context: Context, completion: @escaping (Timeline<WeatherEntry>) -> ()) { Task { var widgetEvent: Event = Event() do { // 仅在设备锁屏时抛出异常 let data = try Data(contentsOf: FileManager.appEventShare) // <- 抛出异常 let widgetEvents: [Event] = try JSONDecoder().decode([Event].self, from: data) if !widgetEvents.isEmpty { widgetEvent = widgetEvents[0] } else { widgetEvent.userData.name = "No Event" } } catch { print("Widget could not open event", error.localizedDescription) widgetEvent.userData.name = "Failed Decode" } let currentDate = Date.now let timelineTime = timeForFiveMinuteInterval(from: currentDate) let entries = [ WeatherEntry(date: currentDate, event: widgetEvent), WeatherEntry(date: timelineTime, event: widgetEvent) ] let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } }
原因分析与解决方案
核心原因
- 后台权限限制:设备锁屏后,iOS会收紧沙箱权限,Widget进程的文件访问权限被限制。
Data(contentsOf:)是同步阻塞式读取,在低权限环境下容易触发权限拦截。 - 文件锁冲突:主应用可能在锁屏前持有文件写入锁,锁屏后主应用被挂起,文件锁未正常释放,导致Widget读取时触发资源占用异常。
- 系统资源优化:设备休眠时,iOS会降低文件系统访问优先级,同步读取操作可能被系统强制中断,抛出超时或权限类错误。
修复方案
1. 改用异步文件读取API
替换同步读取逻辑为异步方式,适配后台低优先级环境:
func getTimeline(in context: Context, completion: @escaping (Timeline<WeatherEntry>) -> ()) { var widgetEvent: Event = Event() guard let fileURL = FileManager.appEventShare else { widgetEvent.userData.name = "Invalid URL" finishTimeline(widgetEvent: widgetEvent, completion: completion) return } do { let fileHandle = try FileHandle(forReadingFrom: fileURL) fileHandle.readToEnd(completionHandler: { data in DispatchQueue.main.async { guard let data = data else { widgetEvent.userData.name = "Failed Read" self.finishTimeline(widgetEvent: widgetEvent, completion: completion) return } do { let widgetEvents: [Event] = try JSONDecoder().decode([Event].self, from: data) widgetEvent = widgetEvents.first ?? Event(userData: Event.UserData(name: "No Event")) } catch { print("Widget decode error:", error) widgetEvent.userData.name = "Failed Decode" } self.finishTimeline(widgetEvent: widgetEvent, completion: completion) } }) } catch { print("Widget file open error:", error) widgetEvent.userData.name = "Failed Open" finishTimeline(widgetEvent: widgetEvent, completion: completion) } } // 提取Timeline生成逻辑,简化代码 private func finishTimeline(widgetEvent: Event, completion: @escaping (Timeline<WeatherEntry>) -> ()) { let currentDate = Date.now let timelineTime = timeForFiveMinuteInterval(from: currentDate) let entries = [ WeatherEntry(date: currentDate, event: widgetEvent), WeatherEntry(date: timelineTime, event: widgetEvent) ] let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) }
2. 优化主应用文件写入逻辑
主应用写入文件时使用原子操作,避免文件锁残留:
// 主应用写入数据时添加.atomic选项 let data = try JSONEncoder().encode(events) try data.write(to: FileManager.appEventShare, options: .atomic)
3. 完善异常日志
捕获异常时打印完整错误信息,而非仅本地化描述,便于精准定位问题:
catch { print("Widget file operation failed:", error) // 打印完整Error对象 widgetEvent.userData.name = "Failed Decode" }
4. 可选:改用UserDefaults存储(适合小数据量)
由于你的数据仅336字节,远低于UserDefaults单条数据限制,可将数据序列化后存储到App Group的UserDefaults,规避文件系统的权限和锁问题:
// 主应用写入 let sharedDefaults = UserDefaults(suiteName: "your.app.group.id")! let data = try JSONEncoder().encode(events) sharedDefaults.set(data, forKey: "WeatherEvents") // Widget读取 if let data = sharedDefaults.data(forKey: "WeatherEvents") { let widgetEvents = try JSONDecoder().decode([Event].self, from: data) widgetEvent = widgetEvents.first ?? Event(userData: Event.UserData(name: "No Event")) }
内容的提问来源于stack exchange,提问作者SwiftAero
相关产品推荐
相关产品推荐

