axiom-storage-management-ref

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

iOS Storage Management Reference

iOS 存储管理参考文档

Purpose: Comprehensive reference for storage pressure, purging policies, disk space, and URL resource values Availability: iOS 5.0+ (basic), iOS 11.0+ (modern capacity APIs) Context: Answer to "Does iOS provide any way to mark files as 'purge as last resort'?"
用途:存储压力、清理策略、磁盘空间及URL资源值的综合参考文档 适用版本:iOS 5.0+(基础功能),iOS 11.0+(现代容量API) 背景:解答「iOS是否提供将文件标记为‘最后才清理’的方法?」这一问题

When to Use This Skill

何时使用本技能

Use this skill when you need to:
  • Understand iOS file purging behavior
  • Check available disk space correctly
  • Set purge priorities for cached files
  • Exclude files from backup
  • Monitor storage pressure
  • Mark files as purgeable
  • Understand volume capacity APIs
  • Handle "low storage" scenarios
当你需要以下操作时,可使用本技能:
  • 了解iOS文件清理行为
  • 正确检查可用磁盘空间
  • 为缓存文件设置清理优先级
  • 将文件排除在备份外
  • 监控存储压力
  • 将文件标记为可清理
  • 了解容量相关API
  • 处理「存储空间不足」场景

The Core Question

核心问题

"Does iOS provide any way to mark files as 'purge as last resort'?"
Answer: Not directly, but iOS provides two approaches:
  1. Location-based purging (implicit priority):
    • tmp/
      → Purged aggressively (anytime)
    • Library/Caches/
      → Purged under storage pressure
    • Documents/
      ,
      Application Support/
      → Never purged
  2. Capacity checking (explicit strategy):
    • volumeAvailableCapacityForImportantUsage
      — For must-save data
    • volumeAvailableCapacityForOpportunisticUsage
      — For nice-to-have data
    • Check before saving, choose location based on available space

「iOS是否提供将文件标记为‘最后才清理’的方法?」
答案:没有直接的方法,但iOS提供两种实现思路:
  1. 基于存储位置的清理(隐式优先级):
    • tmp/
      → 被主动清理(随时可能)
    • Library/Caches/
      → 存储压力下会被清理
    • Documents/
      Application Support/
      → 永远不会被清理
  2. 容量检查(显式策略):
    • volumeAvailableCapacityForImportantUsage
      — 用于必须保存的数据
    • volumeAvailableCapacityForOpportunisticUsage
      — 用于非必需的数据
    • 保存前检查容量,根据可用空间选择存储位置

URL Resource Values for Storage

存储相关的URL资源值

Complete Reference Table

完整参考表

Resource KeyTypePurposeAvailability
volumeAvailableCapacityKey
Int64Total available spaceiOS 5.0+
volumeAvailableCapacityForImportantUsageKey
Int64Space for essential filesiOS 11.0+
volumeAvailableCapacityForOpportunisticUsageKey
Int64Space for optional filesiOS 11.0+
volumeTotalCapacityKey
Int64Total volume capacityiOS 5.0+
isExcludedFromBackupKey
BoolExclude from iCloud/iTunes backupiOS 5.1+
isPurgeableKey
BoolSystem can delete under pressureiOS 9.0+
fileAllocatedSizeKey
Int64Actual disk space usediOS 5.0+
totalFileAllocatedSizeKey
Int64Total allocated (including metadata)iOS 5.0+
资源键类型用途适用版本
volumeAvailableCapacityKey
Int64总可用空间iOS 5.0+
volumeAvailableCapacityForImportantUsageKey
Int64必需文件可用空间iOS 11.0+
volumeAvailableCapacityForOpportunisticUsageKey
Int64可选文件可用空间iOS 11.0+
volumeTotalCapacityKey
Int64总存储容量iOS 5.0+
isExcludedFromBackupKey
Bool排除在iCloud/iTunes备份外iOS 5.1+
isPurgeableKey
Bool系统可在存储压力下删除iOS 9.0+
fileAllocatedSizeKey
Int64实际占用磁盘空间iOS 5.0+
totalFileAllocatedSizeKey
Int64总占用空间(含元数据)iOS 5.0+

Checking Available Space (Modern Approach)

检查可用空间(现代方法)

swift
// ✅ 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()
}
swift
// ✅ 正确做法:在保存前检查合适的容量
func checkSpaceBeforeSaving(fileSize: Int64, isEssential: Bool) -> Bool {
    let homeURL = FileManager.default.homeDirectoryForCurrentUser

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

        if isEssential {
            // 用于必须保存的数据(用户创建的内容、关键应用数据)
            let importantCapacity = values.volumeAvailableCapacityForImportantUsage ?? 0
            return fileSize < importantCapacity
        } else {
            // 用于非必需的数据(缓存、缩略图)
            let opportunisticCapacity = values.volumeAvailableCapacityForOpportunisticUsage ?? 0
            return fileSize < opportunisticCapacity
        }
    } catch {
        print("检查容量出错:\(error)")
        return false
    }
}

// 使用示例
if checkSpaceBeforeSaving(fileSize: imageData.count, isEssential: true) {
    try imageData.write(to: documentsURL.appendingPathComponent("photo.jpg"))
} else {
    showLowStorageAlert()
}

Important vs Opportunistic Capacity

必需容量 vs 可选容量

volumeAvailableCapacityForImportantUsage:
  • Space reserved for essential operations
  • Use for: User-created content, must-save data
  • System reserves this space more aggressively
  • Higher threshold
volumeAvailableCapacityForOpportunisticUsage:
  • Space available for optional operations
  • Use for: Caches, thumbnails, pre-fetching
  • Lower threshold (system may already be under pressure)
  • Indicates "go ahead if you want, but system is getting full"
swift
// ✅ 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
}

volumeAvailableCapacityForImportantUsage:
  • 关键操作预留的空间
  • 适用场景:用户创建的内容、必须保存的数据
  • 系统会更严格地保留此空间
  • 阈值更高
volumeAvailableCapacityForOpportunisticUsage:
  • 可选操作提供的可用空间
  • 适用场景:缓存、缩略图、预加载内容
  • 阈值更低(系统可能已处于存储压力下)
  • 表示「如果需要可以执行,但系统存储空间即将不足」
swift
// ✅ 正确做法:为不同类型数据设置不同阈值
func shouldDownloadThumbnail(size: Int64) -> Bool {
    let capacity = try? FileManager.default.homeDirectoryForCurrentUser
        .resourceValues(forKeys: [.volumeAvailableCapacityForOpportunisticUsageKey])
        .volumeAvailableCapacityForOpportunisticUsage ?? 0

    // 只有当空间充足时才下载可选内容
    return size < capacity
}

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

    // 用户文档属于必需内容
    return size < capacity
}

Backup Exclusion

备份排除设置

isExcludedFromBackup

isExcludedFromBackup

Files in
Caches/
are automatically excluded from backup, but you should explicitly mark re-downloadable files in other directories.
swift
// ✅ 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)
}
When to exclude from backup:
  • ✅ Downloaded content that can be re-fetched
  • ✅ Generated thumbnails
  • ✅ Cached API responses
  • ✅ Large media files from server
  • ❌ User-created content (always back up)
  • ❌ App data that can't be recreated
Caches/
目录下的文件会自动排除在备份外,但你应该显式标记其他目录中可重新下载的文件。
swift
// ✅ 正确做法:将可重新下载的大文件排除在备份外
func markExcludedFromBackup(url: URL) throws {
    var resourceValues = URLResourceValues()
    resourceValues.isExcludedFromBackup = true
    try url.setResourceValues(resourceValues)
}

// 示例:下载播客剧集
func downloadPodcast(url: URL) throws {
    let appSupportURL = FileManager.default.urls(
        for: .applicationSupportDirectory,
        in: .userDomainMask
    )[0]

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

    // 下载文件
    let data = try Data(contentsOf: url)
    try data.write(to: podcastURL)

    // 标记为排除备份(可重新下载)
    try markExcludedFromBackup(url: podcastURL)
}
何时排除备份:
  • ✅ 可重新获取的下载内容
  • ✅ 生成的缩略图
  • ✅ 缓存的API响应
  • ✅ 来自服务器的大媒体文件
  • ❌ 用户创建的内容(始终需要备份)
  • ❌ 无法重新生成的应用数据

Checking Backup Status

检查备份状态

swift
// ✅ Check if file is excluded from backup
func isExcludedFromBackup(url: URL) -> Bool {
    let values = try? url.resourceValues(forKeys: [.isExcludedFromBackupKey])
    return values?.isExcludedFromBackup ?? false
}

swift
// ✅ 检查文件是否已排除在备份外
func isExcludedFromBackup(url: URL) -> Bool {
    let values = try? url.resourceValues(forKeys: [.isExcludedFromBackupKey])
    return values?.isExcludedFromBackup ?? false
}

Purgeable Files

可清理文件

isPurgeable

isPurgeable

Mark files as candidates for automatic purging by the system.
swift
// ✅ 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)
}
Note: Files in
Caches/
are already purgeable by location. Setting
isPurgeable
is advisory for files in other locations.

将文件标记为系统可自动清理的候选文件。
swift
// ✅ 正确做法:将缓存文件标记为可清理
func markAsPurgeable(url: URL) throws {
    var resourceValues = URLResourceValues()
    resourceValues.isPurgeable = true
    try url.setResourceValues(resourceValues)
}

// 示例:缩略图缓存
func cacheThumbnail(image: UIImage, for url: URL) throws {
    let cacheURL = FileManager.default.urls(
        for: .cachesDirectory,
        in: .userDomainMask
    )[0]

    let thumbnailURL = cacheURL.appendingPathComponent(url.lastPathComponent)

    // 保存缩略图
    try image.pngData()?.write(to: thumbnailURL)

    // 标记为可清理
    try markAsPurgeable(url: thumbnailURL)

    // 同时排除在备份外
    var resourceValues = URLResourceValues()
    resourceValues.isExcludedFromBackup = true
    try thumbnailURL.setResourceValues(resourceValues)
}
注意
Caches/
目录下的文件已通过存储位置默认标记为可清理。为其他目录下的文件设置
isPurgeable
仅作为系统的参考建议。

Implicit Purge Priority (Location-Based)

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

iOS purges files based on location, not explicit priority flags.
iOS根据文件的存储位置而非显式优先级标记来清理文件。

Purge Priority Hierarchy

清理优先级层级

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
最先被清理(主动):
└── tmp/
    - 清理时机:随时(即使应用在运行中)
    - 生命周期:数小时至数天
    - 适用场景:真正的临时中间文件

其次被清理(存储压力下):
└── Library/Caches/
    - 清理时机:系统需要空间时
    - 生命周期:数周至数月(若空间充足)
    - 适用场景:可重新下载、可重新生成的内容

永不被清理(永久存储):
├── Documents/
│   - 是否备份:✅ 是
│   - 是否被清理:❌ 永不(除非应用被删除)
│   - 适用场景:用户创建的内容
└── Library/Application Support/
    - 是否备份:✅ 是
    - 是否被清理:❌ 永不(除非应用被删除)
    - 适用场景:关键应用数据

Implementation Strategy

实现策略

swift
// ✅ 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
}

swift
// ✅ 正确做法:根据清理优先级需求选择存储位置
func saveFile(data: Data, priority: FilePriority) throws {
    let url: URL

    switch priority {
    case .essential:
        // 永不被清理 - 用于用户创建或关键应用数据
        url = FileManager.default.urls(
            for: .documentDirectory,
            in: .userDomainMask
        )[0].appendingPathComponent("important.dat")

    case .cacheable:
        // 存储压力下被清理 - 用于可重新下载的内容
        url = FileManager.default.urls(
            for: .cachesDirectory,
            in: .userDomainMask
        )[0].appendingPathComponent("cache.dat")

    case .temporary:
        // 被主动清理 - 用于临时文件
        url = FileManager.default.temporaryDirectory
            .appendingPathComponent("temp.dat")
    }

    try data.write(to: url)

    // 对于可缓存文件,标记为排除备份
    if priority == .cacheable {
        var resourceValues = URLResourceValues()
        resourceValues.isExcludedFromBackup = true
        try url.setResourceValues(resourceValues)
    }
}

enum FilePriority {
    case essential    // 永不清理
    case cacheable    // 存储压力下清理
    case temporary    // 主动清理
}

Storage Pressure Detection

存储压力检测

Responding to Low Storage

响应存储空间不足

swift
// ✅ 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)
        }
    }
}
swift
// ✅ 正确做法:监控存储空间并主动清理
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

        // 计算可用空间百分比
        let percentAvailable = Double(availableSpace) / Double(totalSpace)

        if percentAvailable < 0.10 {  // 可用空间不足10%
            print("⚠️ 检测到存储空间不足,开始清理...")
            cleanupCaches()
        }
    }

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

        // 删除旧缓存文件
        let fileManager = FileManager.default
        guard let files = try? fileManager.contentsOfDirectory(
            at: cacheURL,
            includingPropertiesForKeys: [.contentModificationDateKey]
        ) else { return }

        // 按修改日期排序
        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)
        }

        // 先删除最旧的文件
        for fileURL in sortedFiles.prefix(100) {
            try? fileManager.removeItem(at: fileURL)
        }
    }
}

Background Cleanup Task

后台清理任务

swift
// ✅ 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)
}

swift
// ✅ 正确做法:注册后台任务以清理存储空间
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)
    }

    // 清理旧缓存
    cleanupOldFiles()

    task.setTaskCompleted(success: true)
}

File Size Calculation

文件大小计算

Getting Accurate File Sizes

获取准确文件大小

swift
// ✅ 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")

swift
// ✅ 正确做法:获取实际磁盘占用(含文件系统开销)
func getFileSize(url: URL) -> Int64? {
    let values = try? url.resourceValues(forKeys: [
        .fileAllocatedSizeKey,
        .totalFileAllocatedSizeKey
    ])

    // 使用totalFileAllocatedSize获取准确磁盘占用
    return values?.totalFileAllocatedSize.map { Int64($0) }
}

// ✅ 计算目录大小
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
}

// 使用示例
let cacheSize = getDirectorySize(url: cachesDirectory)
print("缓存占用 \(cacheSize / 1_000_000) MB")

Common Patterns

常见模式

Pattern 1: Smart Download Based on Available Space

模式1:基于可用空间的智能下载

swift
// ✅ 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))
}
swift
// ✅ 正确做法:仅当空间充足时下载可选内容
func downloadOptionalContent(url: URL, size: Int64) async throws {
    // 检查可选容量
    let homeURL = FileManager.default.homeDirectoryForCurrentUser
    let values = try homeURL.resourceValues(forKeys: [
        .volumeAvailableCapacityForOpportunisticUsageKey
    ])

    guard let available = values.volumeAvailableCapacityForOpportunisticUsage,
          size < available else {
        print("跳过下载 - 存储空间不足")
        return
    }

    // 执行下载
    let data = try await URLSession.shared.data(from: url).0
    try data.write(to: cachesDirectory.appendingPathComponent(url.lastPathComponent))
}

Pattern 2: Progressive Cache Cleanup

模式2:渐进式缓存清理

swift
// ✅ 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)
    }
}
swift
// ✅ 正确做法:接近存储限制时清理缓存
class CacheManager {
    func addToCache(data: Data, key: String) throws {
        let cacheURL = getCacheURL(for: key)

        // 检查是否需要先清理
        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

        // 可用空间不足200 MB时清理
        return available < 200 * 1_000_000
    }

    func cleanupOldestFiles(targetSize: Int64) {
        // 删除最旧的缓存文件直到容量达标
        // (实现方式参考之前的示例)
    }
}

Pattern 3: Exclude Downloaded Media from Backup

模式3:将下载的媒体文件排除在备份外

swift
// ✅ 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)
    }
}

swift
// ✅ 正确做法:下载的播客/视频管理
class MediaDownloader {
    func downloadMedia(url: URL) async throws {
        let data = try await URLSession.shared.data(from: url).0

        // 存储在Application Support目录(而非Caches,以便持久保存)
        let mediaURL = applicationSupportDirectory
            .appendingPathComponent("Downloads")
            .appendingPathComponent(url.lastPathComponent)

        try data.write(to: mediaURL)

        // 但排除在备份外(可重新下载)
        var resourceValues = URLResourceValues()
        resourceValues.isExcludedFromBackup = true
        try mediaURL.setResourceValues(resourceValues)
    }
}

Debugging Storage Issues

调试存储问题

Audit Backup Size

审计备份大小

swift
// ✅ 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?")
            }
        }
    }
}

swift
// ✅ 检查备份内容
func auditBackupSize() {
    let documentsURL = FileManager.default.urls(
        for: .documentDirectory,
        in: .userDomainMask
    )[0]

    let size = getDirectorySize(url: documentsURL)
    print("Documents目录(已备份):\(size / 1_000_000) MB")

    // 检查应排除备份的大文件
    if size > 100 * 1_000_000 {  // 超过100 MB
        print("⚠️ 备份体积过大 - 检查可重新下载的文件")
        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("大文件:\(fileURL.lastPathComponent) (\(size / 1_000_000) MB)")

            // 检查是否已排除备份
            if !isExcludedFromBackup(url: fileURL) {
                print("⚠️ 是否应将此文件排除在备份外?")
            }
        }
    }
}

Quick Reference

快速参考

TaskAPICode
Check space for essential file
volumeAvailableCapacityForImportantUsageKey
values.volumeAvailableCapacityForImportantUsage
Check space for cache
volumeAvailableCapacityForOpportunisticUsageKey
values.volumeAvailableCapacityForOpportunisticUsage
Exclude from backup
isExcludedFromBackupKey
resourceValues.isExcludedFromBackup = true
Mark purgeable
isPurgeableKey
resourceValues.isPurgeable = true
Get file size
totalFileAllocatedSizeKey
values.totalFileAllocatedSize
Purge priorityLocation-basedUse
tmp/
or
Caches/
directory

任务API代码示例
检查必需文件可用空间
volumeAvailableCapacityForImportantUsageKey
values.volumeAvailableCapacityForImportantUsage
检查缓存可用空间
volumeAvailableCapacityForOpportunisticUsageKey
values.volumeAvailableCapacityForOpportunisticUsage
排除备份
isExcludedFromBackupKey
resourceValues.isExcludedFromBackup = true
标记为可清理
isPurgeableKey
resourceValues.isPurgeable = true
获取文件大小
totalFileAllocatedSizeKey
values.totalFileAllocatedSize
清理优先级基于存储位置使用
tmp/
Caches/
目录

File Protection Quick Reference

文件保护快速参考

Set encryption level per file. See
axiom-file-protection-ref
for full guide.
LevelWhen AccessibleUse For
.complete
Only while unlockedPasswords, tokens, health data
.completeUnlessOpen
After first unlock if already openActive downloads, media recording
.completeUntilFirstUserAuthentication
After first unlock (default)Most app data
.none
Always, even before unlockBackground fetch data, push payloads
swift
// 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-file-protection-ref
级别可访问时机适用场景
.complete
仅设备解锁时密码、令牌、健康数据
.completeUnlessOpen
首次解锁后若已打开则可访问正在进行的下载、媒体录制
.completeUntilFirstUserAuthentication
首次解锁后(默认)大多数应用数据
.none
始终可访问,甚至解锁前后台获取数据、推送负载
swift
// 为文件设置保护级别
try data.write(to: url, options: .completeFileProtection)

// 为目录设置保护级别
try FileManager.default.createDirectory(
    at: url,
    withIntermediateDirectories: true,
    attributes: [.protectionKey: FileProtectionType.complete]
)

// 检查当前保护级别
let values = try url.resourceValues(forKeys: [.fileProtectionKey])
print("保护级别:\(values.fileProtection ?? .none)")

Related Skills

相关技能

  • axiom-storage
    — Decide where to store files
  • axiom-file-protection-ref
    — File encryption and security
  • axiom-storage-diag
    — Debug storage-related issues

Last Updated: 2025-12-12 Skill Type: Reference Minimum iOS: 5.0 (basic), 11.0 (modern capacity APIs)
  • axiom-storage
    — 选择文件存储位置
  • axiom-file-protection-ref
    — 文件加密与安全
  • axiom-storage-diag
    — 调试存储相关问题

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