```
feat(AI): 集成MNN推理引擎替换MLX作为主AI运行时 - 引入MNN(alibaba) + Arm SME2 + CPU作为主AI运行时,支持A19/iPhone17的 SME2和A17的NEON加速 - 添加MLX Swift作为兜底GPU推理方案,实现双后端切换机制 - 使用单一Qwen3.5-2B多模态模型(1.2GB),替代原有的LLM+VL分离架构 - 实现InferenceEngine.current引擎选择逻辑,真机默认MNN,模拟器回退MLX - 更新AIAgent架构,通过MNNLLMBridge(ObjC++) → MNNBackend进行推理 - 修改队列机制防止并发推理导致OOM,使用信号量闸门控制显存占用 - 更新文档中的技术栈说明、模块边界和周次交付计划 ```
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import ImageIO
|
||||
|
||||
enum FileVaultError: Error {
|
||||
case readFailed
|
||||
@@ -10,7 +11,10 @@ enum FileVaultError: Error {
|
||||
|
||||
/// `@unchecked Sendable`:rootURL 是 let,方法只 I/O 到沙盒目录(线程安全),
|
||||
/// 可被任意 actor / Task 跨边界访问。实例方法显式 `nonisolated`,见 ModelStore 同款注释。
|
||||
final class FileVault: @unchecked Sendable {
|
||||
/// 类级 `nonisolated`:工程开了 `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`,默认会把本类
|
||||
/// 连同 `thumbnailCache`(非 Sendable 的 NSCache)推成 MainActor,令 nonisolated I/O 方法 / 单例
|
||||
/// 初始化访问不了;本类是纯文件 I/O + 缓存工具,必须脱离 MainActor 供任意 actor 调用,故整类标 nonisolated。
|
||||
nonisolated final class FileVault: @unchecked Sendable {
|
||||
nonisolated static let shared: FileVault = {
|
||||
do {
|
||||
let appSupport = try FileManager.default.url(
|
||||
@@ -28,6 +32,17 @@ final class FileVault: @unchecked Sendable {
|
||||
|
||||
let rootURL: URL
|
||||
|
||||
/// 已降采样图片的内存缓存。NSCache 线程安全、内存吃紧时系统自动回收;
|
||||
/// key = "相对路径@目标像素",避免 TabView 翻页 / 列表滚动反复读盘解码同一张图。
|
||||
/// 只缓存降采样后的小图(几百 KB),不缓存全分辨率原图。
|
||||
/// `nonisolated(unsafe)`:本工程默认 MainActor 隔离,非 Sendable 的 NSCache 存储属性即便整类标
|
||||
/// nonisolated 仍被推成 MainActor,令各 nonisolated I/O 方法访问不到;NSCache 本身线程安全,故 unsafe 豁免。
|
||||
private nonisolated(unsafe) let thumbnailCache: NSCache<NSString, UIImage> = {
|
||||
let cache = NSCache<NSString, UIImage>()
|
||||
cache.countLimit = 40
|
||||
return cache
|
||||
}()
|
||||
|
||||
init(rootURL: URL) throws {
|
||||
self.rootURL = rootURL
|
||||
try FileManager.default.createDirectory(
|
||||
@@ -81,6 +96,33 @@ final class FileVault: @unchecked Sendable {
|
||||
return image
|
||||
}
|
||||
|
||||
/// 按目标最大边降采样加载。用 ImageIO 直接解出缩略图,**绝不**把全分辨率位图载进内存:
|
||||
/// 一张 4000×3000 体检照全量解码是 ~48MB RGBA,翻几页就 jetsam;降到 ≤2000px 后仅几 MB。
|
||||
/// 自动尊重 EXIF 方向。结果按尺寸缓存,翻页/滚动回看命中缓存不再读盘。
|
||||
/// 失败(锁屏读不到 / 损坏)抛 readFailed,与 loadImage 一致,UI 显示占位即可。
|
||||
nonisolated func loadDownsampledImage(relativePath: String, maxPixelSize: CGFloat) throws -> UIImage {
|
||||
let cacheKey = "\(relativePath)@\(Int(maxPixelSize))" as NSString
|
||||
if let cached = thumbnailCache.object(forKey: cacheKey) { return cached }
|
||||
|
||||
let url = try resolveSafePath(relativePath)
|
||||
let srcOptions: [CFString: Any] = [kCGImageSourceShouldCache: false]
|
||||
guard let src = CGImageSourceCreateWithURL(url as CFURL, srcOptions as CFDictionary) else {
|
||||
throw FileVaultError.readFailed
|
||||
}
|
||||
let thumbOptions: [CFString: Any] = [
|
||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||
kCGImageSourceCreateThumbnailWithTransform: true, // 应用 EXIF 旋转,免得横竖颠倒
|
||||
kCGImageSourceShouldCacheImmediately: true, // 在后台线程就解码,不拖到主线程绘制时
|
||||
kCGImageSourceThumbnailMaxPixelSize: maxPixelSize
|
||||
]
|
||||
guard let cg = CGImageSourceCreateThumbnailAtIndex(src, 0, thumbOptions as CFDictionary) else {
|
||||
throw FileVaultError.decodeFailed
|
||||
}
|
||||
let image = UIImage(cgImage: cg)
|
||||
thumbnailCache.setObject(image, forKey: cacheKey)
|
||||
return image
|
||||
}
|
||||
|
||||
nonisolated func remove(relativePath: String) throws {
|
||||
let url = try resolveSafePath(relativePath)
|
||||
do {
|
||||
@@ -88,6 +130,8 @@ final class FileVault: @unchecked Sendable {
|
||||
} catch {
|
||||
throw FileVaultError.removeFailed
|
||||
}
|
||||
// 删文件后清掉降采样缓存,避免详情页仍显示已删原图(删除次数极少,整清无虞)。
|
||||
thumbnailCache.removeAllObjects()
|
||||
}
|
||||
|
||||
/// 清空 Vault 全部文件。单个文件删除失败(被占用/权限)不中断,继续删其余;
|
||||
@@ -99,6 +143,7 @@ final class FileVault: @unchecked Sendable {
|
||||
try? fm.removeItem(at: url)
|
||||
}
|
||||
let remaining = (try? fm.contentsOfDirectory(at: rootURL, includingPropertiesForKeys: nil)) ?? []
|
||||
thumbnailCache.removeAllObjects()
|
||||
if !remaining.isEmpty {
|
||||
throw FileVaultError.removeFailed
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user