You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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)
    }
}

原因分析与解决方案

核心原因

  1. 后台权限限制:设备锁屏后,iOS会收紧沙箱权限,Widget进程的文件访问权限被限制。Data(contentsOf:)是同步阻塞式读取,在低权限环境下容易触发权限拦截。
  2. 文件锁冲突:主应用可能在锁屏前持有文件写入锁,锁屏后主应用被挂起,文件锁未正常释放,导致Widget读取时触发资源占用异常。
  3. 系统资源优化:设备休眠时,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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 05:05:17