mcpField
A
axiom-storage-management-ref

axiom-storage-management-ref

github.com/charleswiltgen开发1.2k168 次安装今天更新
代码仓库

关于 axiom-storage-management-ref

面向 iOS 开发者的存储管理参考技能,涵盖容量检查、文件清理优先级和备份排除。重点说明 iOS 通过目录位置隐式决定清理顺序,并提供容量 API 的使用指导。

主要功能

  1. 01查询可用容量与文件占用
  2. 02理解目录对应的清理优先级
  3. 03设置文件为可清理候选
  4. 04将可重新获取文件排除在备份外
  5. 05监测存储压力并清理缓存

适用场景

  1. 01保存前判断重要或可选数据所需空间
  2. 02为缓存和临时文件选择存储位置
  3. 03避免下载媒体占用设备备份空间

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-storage-management-ref

安装后在 Claude Code 中输入/axiom-storage-management-ref确认已加载。

详细介绍

iOS 存储管理参考

用途:全面介绍存储压力、清理策略、磁盘空间和 URL 资源值 可用性:iOS 5.0+(基础功能),iOS 11.0+(现代容量 API) 背景:回答“iOS 是否提供将文件标记为‘最后手段才清理’的方法?”

何时使用此技能

在以下情况下使用此技能:

  • 了解 iOS 文件清理行为
  • 正确检查可用磁盘空间
  • 设置缓存文件的清理优先级
  • 将文件排除在备份之外
  • 监控存储压力
  • 将文件标记为可清理
  • 了解卷容量 API
  • 处理“存储空间不足”的场景

核心问题

“iOS 是否提供将文件标记为‘最后手段才清理’的方法?”

回答:不能直接实现,但 iOS 提供了两种方法:

  1. 基于位置的清理(隐式优先级):

    • tmp/ → 随时可能被积极清理
    • Library/Caches/ → 存储压力下会被清理
    • Documents/、Application Support/ → 永不清理
  2. 容量检查(显式策略):

    • volumeAvailableCapacityForImportantUsage — 用于必须保存的数据
    • volumeAvailableCapacityForOpportunisticUsage — 用于有了更好的数据
    • 保存前先检查,再根据可用空间选择位置

存储相关的 URL 资源值

完整参考表
资源键类型用途可用性
volumeAvailableCapacityKeyInt64总可用空间iOS 5.0+
volumeAvailableCapacityForImportantUsageKeyInt64重要文件的可用空间iOS 11.0+
volumeAvailableCapacityForOpportunisticUsageKeyInt64可选文件的可用空间iOS 11.0+
volumeTotalCapacityKeyInt64卷的总容量iOS 5.0+
isExcludedFromBackupKeyBool排除在 iCloud/iTunes 备份之外iOS 5.1+
isPurgeableKeyBool系统可在存储压力下删除iOS 9.0+
fileAllocatedSizeKeyInt64实际占用的磁盘空间iOS 5.0+
totalFileAllocatedSizeKeyInt64分配的总空间(包括元数据)iOS 5.0+
检查可用空间(现代方法)
// ✅ CORRECT: Check appropriate capacity before saving
func checkSpaceBeforeSaving(fileSize: Int64, isEssential: Bool) -> Bool {
    let homeURL = FileManager.default.homeDirectoryForCurrentUser

    do {
        let values = try homeURL.resourceValues(forKeys: [
            .volumeAvailableCapacityForImportantUsageKey,
            .volumeAvailableCapacityForOpportunisticUsageKey
        ])

        if isEssential {
            // For must-save data (user-created content, critical app data)
            let importantCapacity = values.volumeAvailableCapacityForImportantUsage ?? 0
            return fileSize < importantCapacity
        } else {
            // For nice-to-have data (caches, thumbnails)
            let opportunisticCapacity = values.volumeAvailableCapacityForOpportunisticUsage ?? 0
            return fileSize < opportunisticCapacity
        }
    } catch {
        print("Error checking capacity: \(error)")
        return false
    }
}

// Usage
if checkSpaceBeforeSaving(fileSize: imageData.count, isEssential: true) {
    try imageData.write(to: documentsURL.appendingPathComponent("photo.jpg"))
} else {
    showLowStorageAlert()
}
重要容量与机会性容量

volumeAvailableCapacityForImportantUsage:

  • 为重要操作预留的空间
  • 用于:用户创建的内容、必须保存的数据
  • 系统会更积极地预留此空间
  • 阈值较高

volumeAvailableCapacityForOpportunisticUsage:

  • 可用于可选操作的空间
  • 用于:缓存、缩略图、预取内容
  • 阈值较低(系统可能已面临存储压力)
  • 表示“如果需要,可以继续,但系统存储空间正在变得紧张”
// ✅ CORRECT: Different thresholds for different data types
func shouldDownloadThumbnail(size: Int64) -> Bool {
    let capacity = try? FileManager.default.homeDirectoryForCurrentUser
        .resourceValues(forKeys: [.volumeAvailableCapacityForOpportunisticUsageKey])
        .volumeAvailableCapacityForOpportunisticUsage ?? 0

    // Only download optional content if there's plenty of space
    return size < capacity
}

func canSaveUserDocument(size: Int64) -> Bool {
    let capacity = try? FileManager.default.homeDirectoryForCurrentUser
        .resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey])
        .volumeAvailableCapacityForImportantUsage ?? 0

    // User documents are essential
    return size < capacity
}

排除备份

isExcludedFromBackup

Caches/ 中的文件会自动排除在备份之外,但对于其他目录中的可重新下载文件,你应该明确标记。

// ✅ CORRECT: Exclude large re-downloadable files from backup
func markExcludedFromBackup(url: URL) throws {
    var resourceValues = URLResourceValues()
    resourceValues.isExcludedFromBackup = true
    try url.setResourceValues(resourceValues)
}

// Example: Downloaded podcast episodes
func downloadPodcast(url: URL) throws {
    let appSupportURL = FileManager.default.urls(
        for: .applicationSupportDirectory,
        in: .userDomainMask
    )[0]

    let podcastURL = appSupportURL
        .appendingPathComponent("Podcasts")
        .appendingPathComponent(url.lastPathComponent)

    // Download file
    let data = try Data(contentsOf: url)
    try data.write(to: podcastURL)

    // Mark as excluded from backup (can re-download)
    try markExcludedFromBackup(url: podcastURL)
}

适合排除在备份之外的内容:

  • ✅ 可以重新获取的下载内容
  • ✅ 生成的缩略图
  • ✅ 缓存的 API 响应
  • ✅ 来自服务器的大型媒体文件
  • ❌ 用户创建的内容(始终备份)
  • ❌ 无法重新创建的应用数据
检查备份状态
// ✅ Check if file is excluded from backup
func isExcludedFromBackup(url: URL) -> Bool {
    let values = try? url.resourceValues(forKeys: [.isExcludedFromBackupKey])
    return values?.isExcludedFromBackup ?? false
}

可清理文件

isPurgeable

将文件标记为系统自动清理的候选项。

// ✅ CORRECT: Mark cache files as purgeable
func markAsPurgeable(url: URL) throws {
    var resourceValues = URLResourceValues()
    resourceValues.isPurgeable = true
    try url.setResourceValues(resourceValues)
}

// Example: Thumbnail cache
func cacheThumbnail(image: UIImage, for url: URL) throws {
    let cacheURL = FileManager.default.urls(
        for: .cachesDirectory,
        in: .userDomainMask
    )[0]

    let thumbnailURL = cacheURL.appendingPathComponent(url.lastPathComponent)

    // Save thumbnail
    try image.pngData()?.write(to: thumbnailURL)

    // Mark as purgeable
    try markAsPurgeable(url: thumbnailURL)

    // Also exclude from backup
    var resourceValues = URLResourceValues()
    resourceValues.isExcludedFromBackup = true
    try thumbnailURL.setResourceValues(resourceValues)
}

注意:Caches/ 中的文件根据其位置已可被清理。对于其他位置的文件,设置 isPurgeable 仅起提示作用。


隐式清理优先级(基于位置)

iOS 根据位置清理文件,而不是根据显式优先级标志。

清理优先级层级
PURGED FIRST (Aggressive):
└── tmp/
    - Purged: Anytime (even while app running)
    - Lifetime: Hours to days
    - Use for: Truly temporary intermediates

PURGED SECOND (Storage Pressure):
└── Library/Caches/
    - Purged: When system needs space
    - Lifetime: Weeks to months (if space available)
    - Use for: Re-downloadable, regenerable content

NEVER PURGED (Permanent):
├── Documents/
│   - Backed up: ✅ Yes
│   - Purged: ❌ Never (unless app deleted)
│   - Use for: User-created content
│
└── Library/Application Support/
    - Backed up: ✅ Yes
    - Purged: ❌ Never (unless app deleted)
    - Use for: Essential app data
实施策略
// ✅ CORRECT: Choose location based on purge priority needs
func saveFile(data: Data, priority: FilePriority) throws {
    let url: URL

    switch priority {
    case .essential:
        // Never purged - for user-created or critical app data
        url = FileManager.default.urls(
            for: .documentDirectory,
            in: .userDomainMask
        )[0].appendingPathComponent("important.dat")

    case .cacheable:
        // Purged under storage pressure - for re-downloadable content
        url = FileManager.default.urls(
            for: .cachesDirectory,
            in: .userDomainMask
        )[0].appendingPathComponent("cache.dat")

    case .temporary:
        // Purged aggressively - for temp files
        url = FileManager.default.temporaryDirectory
            .appendingPathComponent("temp.dat")
    }

    try data.write(to: url)

    // For cacheable files, mark excluded from backup
    if priority == .cacheable {
        var resourceValues = URLResourceValues()
        resourceValues.isExcludedFromBackup = true
        try url.setResourceValues(resourceValues)
    }
}

enum FilePriority {
    case essential    // Never purge
    case cacheable    // Purge under pressure
    case temporary    // Purge aggressively
}

检测存储压力

应对存储空间不足
// ✅ CORRECT: Monitor for low storage and clean up proactively
class StorageMonitor {
    func checkStorageAndCleanup() {
        let homeURL = FileManager.default.homeDirectoryForCurrentUser

        guard let values = try? homeURL.resourceValues(forKeys: [
            .volumeAvailableCapacityForOpportunisticUsageKey,
            .volumeTotalCapacityKey
        ]) else { return }

        let availableSpace = values.volumeAvailableCapacityForOpportunisticUsage ?? 0
        let totalSpace = values.volumeTotalCapacity ?? 1

        // Calculate percentage
        let percentAvailable = Double(availableSpace) / Double(totalSpace)

        if percentAvailable < 0.10 {  // Less than 10% free
            print("⚠️ Low storage detected, cleaning up...")
            cleanupCaches()
        }
    }

    func cleanupCaches() {
        let cacheURL = FileManager.default.urls(
            for: .cachesDirectory,
            in: .userDomainMask
        )[0]

        // Delete old cache files
        let fileManager = FileManager.default
        guard let files = try? fileManager.contentsOfDirectory(
            at: cacheURL,
            includingPropertiesForKeys: [.contentModificationDateKey]
        ) else { return }

        // Sort by modification date
        let sortedFiles = files.sorted { url1, url2 in
            let date1 = (try? url1.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate
            let date2 = (try? url2.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate
            return (date1 ?? .distantPast) < (date2 ?? .distantPast)
        }

        // Delete oldest files first
        for fileURL in sortedFiles.prefix(100) {
            try? fileManager.removeItem(at: fileURL)
        }
    }
}
后台清理任务
// ✅ CORRECT: Register background task to clean up storage
import BackgroundTasks

func registerBackgroundCleanup() {
    BGTaskScheduler.shared.register(
        forTaskWithIdentifier: "com.example.app.cleanup",
        using: nil
    ) { task in
        self.handleStorageCleanup(task: task as! BGProcessingTask)
    }
}

func handleStorageCleanup(task: BGProcessingTask) {
    task.expirationHandler = {
        task.setTaskCompleted(success: false)
    }

    // Clean up old caches
    cleanupOldFiles()

    task.setTaskCompleted(success: true)
}

文件大小计算

获取准确的文件大小
// ✅ CORRECT: Get actual disk usage (includes filesystem overhead)
func getFileSize(url: URL) -> Int64? {
    let values = try? url.resourceValues(forKeys: [
        .fileAllocatedSizeKey,
        .totalFileAllocatedSizeKey
    ])

    // Use totalFileAllocatedSize for accurate disk usage
    return values?.totalFileAllocatedSize.map { Int64($0) }
}

// ✅ Calculate directory size
func getDirectorySize(url: URL) -> Int64 {
    guard let enumerator = FileManager.default.enumerator(
        at: url,
        includingPropertiesForKeys: [.totalFileAllocatedSizeKey]
    ) else { return 0 }

    var totalSize: Int64 = 0

    for case let fileURL as URL in enumerator {
        if let size = getFileSize(url: fileURL) {
            totalSize += size
        }
    }

    return totalSize
}

// Usage
let cacheSize = getDirectorySize(url: cachesDirectory)
print("Cache using \(cacheSize / 1_000_000) MB")

常见模式

模式 1:根据可用空间智能下载
// ✅ CORRECT: Only download optional content if space available
func downloadOptionalContent(url: URL, size: Int64) async throws {
    // Check opportunistic capacity
    let homeURL = FileManager.default.homeDirectoryForCurrentUser
    let values = try homeURL.resourceValues(forKeys: [
        .volumeAvailableCapacityForOpportunisticUsageKey
    ])

    guard let available = values.volumeAvailableCapacityForOpportunisticUsage,
          size < available else {
        print("Skipping download - low storage")
        return
    }

    // Proceed with download
    let data = try await URLSession.shared.data(from: url).0
    try data.write(to: cachesDirectory.appendingPathComponent(url.lastPathComponent))
}
模式 2:渐进式清理缓存
// ✅ CORRECT: Clean up caches when approaching storage limits
class CacheManager {
    func addToCache(data: Data, key: String) throws {
        let cacheURL = getCacheURL(for: key)

        // Check if we should clean up first
        if shouldCleanupCache(addingSize: Int64(data.count)) {
            cleanupOldestFiles(targetSize: 100 * 1_000_000) // 100 MB
        }

        try data.write(to: cacheURL)
    }

    func shouldCleanupCache(addingSize: Int64) -> Bool {
        let homeURL = FileManager.default.homeDirectoryForCurrentUser
        guard let values = try? homeURL.resourceValues(forKeys: [
            .volumeAvailableCapacityForOpportunisticUsageKey
        ]) else { return false }

        let available = values.volumeAvailableCapacityForOpportunisticUsage ?? 0

        // Clean up if less than 200 MB free
        return available < 200 * 1_000_000
    }

    func cleanupOldestFiles(targetSize: Int64) {
        // Delete oldest cache files until under target
        // (implementation similar to earlier example)
    }
}
模式 3:将下载的媒体文件排除在备份之外
// ✅ CORRECT: Downloaded podcast/video management
class MediaDownloader {
    func downloadMedia(url: URL) async throws {
        let data = try await URLSession.shared.data(from: url).0

        // Store in Application Support (not Caches, so it persists)
        let mediaURL = applicationSupportDirectory
            .appendingPathComponent("Downloads")
            .appendingPathComponent(url.lastPathComponent)

        try data.write(to: mediaURL)

        // But exclude from backup (can re-download)
        var resourceValues = URLResourceValues()
        resourceValues.isExcludedFromBackup = true
        try mediaURL.setResourceValues(resourceValues)
    }
}

调试存储问题

审查备份大小
// ✅ Check what's being backed up
func auditBackupSize() {
    let documentsURL = FileManager.default.urls(
        for: .documentDirectory,
        in: .userDomainMask
    )[0]

    let size = getDirectorySize(url: documentsURL)
    print("Documents (backed up): \(size / 1_000_000) MB")

    // Check for large files that should be excluded
    if size > 100 * 1_000_000 {  // > 100 MB
        print("⚠️ Large backup size - check for re-downloadable files")
        findLargeFiles(in: documentsURL)
    }
}

func findLargeFiles(in directory: URL) {
    guard let enumerator = FileManager.default.enumerator(
        at: directory,
        includingPropertiesForKeys: [.totalFileAllocatedSizeKey]
    ) else { return }

    for case let fileURL as URL in enumerator {
        if let size = getFileSize(url: fileURL),
           size > 10 * 1_000_000 {  // > 10 MB
            print("Large file: \(fileURL.lastPathComponent) (\(size / 1_000_000) MB)")

            // Check if excluded from backup
            if !isExcludedFromBackup(url: fileURL) {
                print("⚠️ Should this be excluded from backup?")
            }
        }
    }
}

快速参考

任务API代码
检查重要文件的可用空间volumeAvailableCapacityForImportantUsageKeyvalues.volumeAvailableCapacityForImportantUsage
检查缓存的可用空间volumeAvailableCapacityForOpportunisticUsageKeyvalues.volumeAvailableCapacityForOpportunisticUsage
排除在备份之外isExcludedFromBackupKeyresourceValues.isExcludedFromBackup = true
标记为可清除isPurgeableKeyresourceValues.isPurgeable = true
获取文件大小totalFileAllocatedSizeKeyvalues.totalFileAllocatedSize
清理优先级基于位置使用 tmp/ 或 Caches/ 目录

文件保护快速参考

为每个文件设置加密级别。完整指南请参阅 axiom-file-protection-ref。

级别可访问时机用途
.complete仅在设备解锁时密码、令牌、健康数据
.completeUnlessOpen首次解锁后,若文件已打开则可访问正在进行的下载、媒体录制
.completeUntilFirstUserAuthentication首次解锁后(默认)大多数应用数据
.none始终可访问,即使设备尚未解锁后台抓取数据、推送负载
// Set protection on file
try data.write(to: url, options: .completeFileProtection)

// Set protection on directory
try FileManager.default.createDirectory(
    at: url,
    withIntermediateDirectories: true,
    attributes: [.protectionKey: FileProtectionType.complete]
)

// Check current protection
let values = try url.resourceValues(forKeys: [.fileProtectionKey])
print("Protection: \(values.fileProtection ?? .none)")

相关技能

  • axiom-storage — 决定文件的存储位置
  • axiom-file-protection-ref — 文件加密与安全
  • axiom-storage-diag — 调试存储相关问题

最后更新:2025-12-12 技能类型:参考 最低 iOS 版本:5.0(基础),11.0(现代容量 API)

评论

支持 Markdown