|
6 | 6 |
|
7 | 7 | ## Overview
|
8 | 8 |
|
9 |
| -Footprint is a Swift library that helps manage and monitor memory usage in your app. It provides a flexible approach to handling memory levels, allowing you to adapt your app's behavior based on the available memory and potential termination risks. |
| 9 | +Footprint is a Swift library that provides proactive memory management for your Apple platform apps. Instead of waiting for memory warnings that come too late, Footprint gives you real-time insights into your app's memory usage and proximity to termination, allowing you to adapt your app's behavior dynamically. |
10 | 10 |
|
11 |
| -### Key Features |
| 11 | +### The Problem |
12 | 12 |
|
13 |
| -- **Memory State Management:** Footprint categorizes memory states into normal, warning, critical, and terminal, providing insights into your app's proximity to termination due to memory constraints. |
| 13 | +Traditional memory management on Apple platforms relies on memory warnings that often arrive too late, especially for larger apps. While `os_proc_available_memory` tells you how much memory remains, you still lack the complete picture of your memory boundaries and usage patterns. |
14 | 14 |
|
15 |
| -- **Dynamic Memory Handling:** Change your app's behavior dynamically based on the current memory state. For instance, adjust cache sizes or optimize resource usage to enhance performance. |
| 15 | +### The Solution |
16 | 16 |
|
17 |
| -- **SwiftUI Integration:** Easily observe and respond to changes in the app's memory state within SwiftUI views using the `onFootprintMemoryStateDidChange` modifier. |
| 17 | +Footprint bridges this gap by providing: |
| 18 | +- **Complete memory visibility**: Track used, remaining, and total memory limits |
| 19 | +- **Proactive state management**: Five distinct memory states from normal to terminal |
| 20 | +- **Behavioral adaptation**: Change your app's behavior before hitting critical memory limits |
| 21 | +- **Multiple observation patterns**: NotificationCenter, async streams, and SwiftUI modifiers |
| 22 | + |
| 23 | +## Key Features |
| 24 | + |
| 25 | +- **Five Memory States**: Navigate through normal, warning, urgent, critical, and terminal states based on memory usage ratios |
| 26 | +- **Dual Tracking**: Monitor both memory footprint and system memory pressure |
| 27 | +- **Real-time Monitoring**: 500ms heartbeat with smart change detection |
| 28 | +- **SwiftUI Integration**: Convenient view modifiers for reactive UI updates |
| 29 | +- **Async Support**: Modern async/await patterns with AsyncStream |
| 30 | +- **Cross-platform**: Works on iOS, macOS, tvOS, watchOS, and visionOS |
18 | 31 |
|
19 | 32 | ## Installation
|
20 | 33 |
|
21 |
| -Add the Footprint library to your project: |
| 34 | +Add Footprint to your project using Swift Package Manager: |
22 | 35 |
|
23 |
| -1. In Xcode, with your app project open, navigate to File > Add Packages. |
24 |
| -2. When prompted, add the Firebase Apple platforms SDK repository: |
| 36 | +1. In Xcode, navigate to File > Add Package Dependencies |
| 37 | +2. Enter the repository URL: |
25 | 38 | ```
|
26 | 39 | https://github.com/naftaly/Footprint
|
27 | 40 | ```
|
28 | 41 |
|
29 | 42 | ## Usage
|
30 | 43 |
|
31 |
| -### Initialization |
| 44 | +### Basic Setup |
32 | 45 |
|
33 |
| -Initialize Footprint as early as possible in your app's lifecycle: |
| 46 | +Initialize Footprint early in your app's lifecycle. The shared instance automatically begins monitoring: |
34 | 47 |
|
35 | 48 | ```swift
|
| 49 | +// Start monitoring (typically in your App or AppDelegate) |
36 | 50 | let _ = Footprint.shared
|
37 | 51 | ```
|
38 | 52 |
|
39 | 53 | ### Memory State Observation
|
40 | 54 |
|
41 |
| -Respond to changes in memory state using the provided notification: |
| 55 | +#### Using NotificationCenter |
| 56 | + |
| 57 | +```swift |
| 58 | +NotificationCenter.default.addObserver( |
| 59 | + forName: Footprint.memoryDidChangeNotification, |
| 60 | + object: nil, |
| 61 | + queue: nil |
| 62 | +) { notification in |
| 63 | + guard let newMemory = notification.userInfo?[Footprint.newMemoryKey] as? Footprint.Memory, |
| 64 | + let oldMemory = notification.userInfo?[Footprint.oldMemoryKey] as? Footprint.Memory, |
| 65 | + let changes = notification.userInfo?[Footprint.changesKey] as? Set<Footprint.ChangeType> |
| 66 | + else { return } |
| 67 | + |
| 68 | + if changes.contains(.state) { |
| 69 | + print("Memory state changed from \(oldMemory.state) to \(newMemory.state)") |
| 70 | + adaptBehavior(for: newMemory.state) |
| 71 | + } |
| 72 | +} |
| 73 | +``` |
| 74 | + |
| 75 | +#### Using Closures |
42 | 76 |
|
43 | 77 | ```swift
|
44 |
| -NotificationCenter.default.addObserver(forName: Footprint.stateDidChangeNotification, object: nil, queue: nil) { notification in |
45 |
| - if let newState = notification.userInfo?[Footprint.newMemoryStateKey] as? Footprint.Memory.State, |
46 |
| - let oldState = notification.userInfo?[Footprint.oldMemoryStateKey] as? Footprint.Memory.State { |
47 |
| - print("Memory state changed from \(oldState) to \(newState)") |
48 |
| - // Perform actions based on the memory state change |
| 78 | +Footprint.shared.observe { memory in |
| 79 | + print("Current memory state: \(memory.state)") |
| 80 | + print("Used: \(ByteCountFormatter.string(fromByteCount: memory.used, countStyle: .memory))") |
| 81 | + print("Remaining: \(ByteCountFormatter.string(fromByteCount: memory.remaining, countStyle: .memory))") |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +#### Using Async Streams |
| 86 | + |
| 87 | +```swift |
| 88 | +Task { |
| 89 | + for await memory in Footprint.shared.memoryStream { |
| 90 | + await handleMemoryChange(memory) |
49 | 91 | }
|
50 | 92 | }
|
51 | 93 | ```
|
52 | 94 |
|
53 | 95 | ### SwiftUI Integration
|
54 | 96 |
|
55 |
| -Use the SwiftUI extension to observe changes in memory state within your views: |
| 97 | +#### Comprehensive Memory Changes |
| 98 | + |
| 99 | +```swift |
| 100 | +Text("Memory Status: \(memoryState)") |
| 101 | + .onFootprintMemoryDidChange { newMemory, oldMemory, changes in |
| 102 | + if changes.contains(.state) { |
| 103 | + updateCachePolicy(for: newMemory.state) |
| 104 | + } |
| 105 | + if changes.contains(.pressure) { |
| 106 | + handleMemoryPressure(newMemory.pressure) |
| 107 | + } |
| 108 | + } |
| 109 | +``` |
| 110 | + |
| 111 | +#### State-Specific Changes |
56 | 112 |
|
57 | 113 | ```swift
|
58 |
| -Text("Hello, World!") |
| 114 | +MyView() |
59 | 115 | .onFootprintMemoryStateDidChange { newState, oldState in
|
60 |
| - print("Memory state changed from \(oldState) to \(newState)") |
61 |
| - // Perform actions based on the memory state change |
| 116 | + switch newState { |
| 117 | + case .normal: |
| 118 | + enableFullFeatures() |
| 119 | + case .warning: |
| 120 | + reduceCacheSize(by: 0.2) |
| 121 | + case .urgent: |
| 122 | + reduceCacheSize(by: 0.5) |
| 123 | + case .critical: |
| 124 | + clearNonEssentialCaches() |
| 125 | + case .terminal: |
| 126 | + emergencyMemoryCleanup() |
| 127 | + } |
| 128 | + } |
| 129 | +``` |
| 130 | + |
| 131 | +#### Pressure-Specific Changes |
| 132 | + |
| 133 | +```swift |
| 134 | +ContentView() |
| 135 | + .onFootprintMemoryPressureDidChange { newPressure, oldPressure in |
| 136 | + handleSystemMemoryPressure(newPressure) |
62 | 137 | }
|
63 | 138 | ```
|
64 | 139 |
|
65 |
| -### Memory Information Retrieval |
| 140 | +### Memory Information |
66 | 141 |
|
67 |
| -Retrieve current memory information: |
| 142 | +Access current memory state and information: |
68 | 143 |
|
69 | 144 | ```swift
|
70 |
| -let currentMemory = footprint.memory |
71 |
| -print("Used Memory: \(currentMemory.used) bytes") |
72 |
| -print("Remaining Memory: \(currentMemory.remaining) bytes") |
73 |
| -print("Memory Limit: \(currentMemory.limit) bytes") |
74 |
| -print("Memory State: \(currentMemory.state)") |
| 145 | +let memory = Footprint.shared.memory |
| 146 | + |
| 147 | +print("Used: \(memory.used) bytes") |
| 148 | +print("Remaining: \(memory.remaining) bytes") |
| 149 | +print("Limit: \(memory.limit) bytes") |
| 150 | +print("State: \(memory.state)") |
| 151 | +print("Pressure: \(memory.pressure)") |
| 152 | +print("Timestamp: \(memory.timestamp)") |
75 | 153 | ```
|
76 | 154 |
|
77 |
| -### Memory Allocation Check |
| 155 | +### Memory Allocation Planning |
78 | 156 |
|
79 |
| -Check if a certain amount of memory can be allocated: |
| 157 | +Check if memory allocation is likely to succeed: |
80 | 158 |
|
81 | 159 | ```swift
|
82 |
| -let canAllocate = footprint.canAllocate(bytes: 1024) |
83 |
| -print("Can allocate 1KB: \(canAllocate)") |
| 160 | +let sizeNeeded: UInt64 = 50_000_000 // 50MB |
| 161 | +if Footprint.shared.canAllocate(bytes: sizeNeeded) { |
| 162 | + // Proceed with allocation |
| 163 | + performMemoryIntensiveOperation() |
| 164 | +} else { |
| 165 | + // Consider alternatives or cleanup |
| 166 | + cleanupBeforeAllocation() |
| 167 | +} |
84 | 168 | ```
|
85 | 169 |
|
| 170 | +## Memory States Explained |
| 171 | + |
| 172 | +Footprint categorizes memory usage into five states based on the ratio of used memory to total limit: |
| 173 | + |
| 174 | +- **Normal** (< 25%): Full functionality, optimal performance |
| 175 | +- **Warning** (25-50%): Begin reducing memory usage, optimize caches |
| 176 | +- **Urgent** (50-75%): Significant memory reduction needed |
| 177 | +- **Critical** (75-90%): Aggressive cleanup required |
| 178 | +- **Terminal** (> 90%): Imminent termination risk, emergency measures |
| 179 | + |
| 180 | +## Practical Examples |
| 181 | + |
| 182 | +### Adaptive Cache Management |
| 183 | + |
| 184 | +```swift |
| 185 | +class ImageCache { |
| 186 | + private var maxCost: Int = 100_000_000 // 100MB default |
| 187 | + |
| 188 | + init() { |
| 189 | + Footprint.shared.observe { [weak self] memory in |
| 190 | + self?.adjustCacheSize(for: memory.state) |
| 191 | + } |
| 192 | + } |
| 193 | + |
| 194 | + private func adjustCacheSize(for state: Footprint.Memory.State) { |
| 195 | + let multiplier: Double = switch state { |
| 196 | + case .normal: 1.0 |
| 197 | + case .warning: 0.8 |
| 198 | + case .urgent: 0.5 |
| 199 | + case .critical: 0.2 |
| 200 | + case .terminal: 0.0 |
| 201 | + } |
| 202 | + |
| 203 | + cache.totalCostLimit = Int(Double(maxCost) * multiplier) |
| 204 | + } |
| 205 | +} |
| 206 | +``` |
| 207 | + |
| 208 | +### Conditional Feature Loading |
| 209 | + |
| 210 | +```swift |
| 211 | +func loadOptionalFeatures() { |
| 212 | + let currentState = Footprint.shared.state |
| 213 | + |
| 214 | + guard currentState < .urgent else { |
| 215 | + // Skip non-essential features in high memory usage |
| 216 | + return |
| 217 | + } |
| 218 | + |
| 219 | + enableAdvancedAnimations() |
| 220 | + preloadAdditionalContent() |
| 221 | +} |
| 222 | +``` |
| 223 | + |
| 224 | +## Development and Testing |
| 225 | + |
| 226 | +### Simulator Support |
| 227 | + |
| 228 | +Footprint includes simulator-specific handling since memory limits work differently. You can enable simulated termination for testing: |
| 229 | + |
| 230 | +```bash |
| 231 | +# Enable simulated out-of-memory termination in simulator |
| 232 | +export SIM_FOOTPRINT_OOM_TERM_ENABLED=1 |
| 233 | +``` |
| 234 | + |
| 235 | +### Custom Memory Providers |
| 236 | + |
| 237 | +For testing or custom scenarios, implement the `MemoryProvider` protocol: |
| 238 | + |
| 239 | +```swift |
| 240 | +class MockMemoryProvider: MemoryProvider { |
| 241 | + func provide(_ pressure: Footprint.Memory.State) -> Footprint.Memory { |
| 242 | + // Return custom memory values for testing |
| 243 | + } |
| 244 | +} |
| 245 | +``` |
| 246 | + |
| 247 | +## Requirements |
| 248 | + |
| 249 | +- iOS 13.0+, macOS 10.15+, tvOS 13.0+, watchOS 6.0+, visionOS 1.0+ |
| 250 | +- Swift 5.0+ |
| 251 | +- Xcode 11.0+ |
| 252 | + |
86 | 253 | ## License
|
87 | 254 |
|
88 | 255 | Footprint is available under the MIT license. See the [LICENSE](LICENSE) file for more info.
|
0 commit comments