我一个稳定运行了多年的 macOS 应用,从 macOS Sequoia 15.1 上个 Beta 版开始,遇到一个奇怪的问题,就是 Datawrite(to:options:) 函数无法保存数据到本地,API 返回的图片数据无法保存到本地,且不会抛出任何异常。

简单如以下代码,项目运行后控制台输出了“save success”,但本地却没有相关文件,这就很匪夷所思了。打断点在第 3 行,然后在访达中可以看到该文件,但继续运行,文件就消失了。

1
2
3
4
5
6
do {
try data.write(to: localURL)
print("save success")
} catch {
print(error)
}

🤔,是哪个进程删了该文件?终端执行以下命令来监控该文件。

1
sudo fs_usage | grep "/Users/wy/Pictures/Tapet/2024/09/04.jpg

再次运行项目,终端输出:

1
2
3
4
5
21:51:49  stat64        /Users/wy/Pictures/Tapet/2024/09/04.jpg                       0.000005   usernoted
21:51:49 getattrlist /Users/wy/Pictures/Tapet/2024/09/04.jpg 0.000058 usernoted
21:51:49 open /Users/wy/Pictures/Tapet/2024/09/04.jpg 0.000075 usernoted
21:51:49 stat64 /Users/wy/Pictures/Tapet/2024/09/04.jpg 0.000011 usernoted
21:51:49 lstat64 /System/Volumes/Data/Users/wy/Pictures/Tapet/2024/09/04.jpg 0.000039 fseventsd

让 AI 来解读一下这些 log:

  • stat64 是一种系统调用,用于获取文件的状态信息,如文件大小、权限、修改时间等。这里可以看到 usernoted 进程对该文件执行了 stat64 操作,检查了文件的元数据。
  • getattrlist 是一个用于获取文件属性列表的系统调用,它比 stat64 更灵活,可以请求特定的属性。这表明 usernoted 正在进一步检查文件的属性。
  • open 是一个常见的文件操作,用于打开文件进行读取、写入或其他操作。这里显示 usernoted 进程打开了该文件,可能是为了读取文件内容或进一步处理。
  • lstat64 是类似于 stat64 的系统调用,但与 stat64 不同的是,lstat64 用于获取符号链接文件的状态信息,而不解析链接到的实际文件路径。这里 fseventsd 进程对文件执行了 lstat64 操作,fseventsd 是文件系统事件守护进程,用于监控和记录文件系统的变化。
  • usernoted 是 macOS 系统中的一个系统进程,主要负责处理用户通知。它与 macOS 的通知中心紧密集成,管理和显示用户的通知。这个进程确保当某些事件发生时,系统能够生成适当的通知并显示在通知中心或者通过弹出窗口提醒用户。

看起来并没有哪个进程删文件,那是系统问题?试了 FixTim,还是无法保存图片,后来我还试了在虚拟机里新安装 macOS Sequoia 15.1 Beta,居然可以保存。🤔🤔

同样的图片,放在项目中,再保存到本地,是可以的,从 API 获取的就不行;如果保存位置 localURL 中不加文件拓展名,可以保存,那是数据问题?不应该啊。🤔🤔🤔

那我换个方式保存图片,先把 API 返回的 data 转成 NSImageNSImagevar tiffRepresentation: Data? { get } 属性,再转成 NSBitmapImageRep 再保存到本地。下面是 StackOverflow 上的一段代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
extension NSImage {
func save(as fileName: String,
fileType: NSBitmapImageRep.FileType = .jpeg,
at directory: URL) -> Bool {
guard let tiffRepresentation = tiffRepresentation,
directory.isDirectory,
!fileName.isEmpty else {
return false
}
do {
try NSBitmapImageRep(data: tiffRepresentation)?
.representation(using: fileType, properties: [:])?
.write(to: directory.appendingPathComponent(fileName).appendingPathExtension(fileType.pathExtension))
return true
} catch {
print(error)
return false
}
}
}

extension URL {
var isDirectory: Bool {
return (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
}
}

extension NSBitmapImageRep.FileType {
var pathExtension: String {
switch self {
case .bmp:
return "bmp"
case .gif:
return "gif"
case .jpeg:
return "jpg"
case .jpeg2000:
return "jp2"
case .png:
return "png"
case .tiff:
return "tif"
@unknown default:
return ""
}
}
}

那么这个问题到底是什么原因造成的呢,可能跟我的系统环境有关,暂时先用 NSBitmapImageRep 绕过去,等正式版我再试试。