|
1 | | -# iOS and Android Behavior Parity |
| 1 | +# iOS and Android Streaming Behavior |
2 | 2 |
|
3 | 3 | ## Overview |
4 | 4 |
|
5 | | -The iOS implementation has been updated to match Android's response handling behavior, providing consistent cross-platform functionality. |
| 5 | +Both iOS and Android now implement true streaming downloads where response bodies are NOT loaded into memory until explicitly accessed. This provides memory-efficient handling of large files. |
6 | 6 |
|
7 | | -## Response Handling Behavior |
| 7 | +## How It Works |
| 8 | + |
| 9 | +### Android (OkHttp) |
| 10 | + |
| 11 | +Android uses OkHttp's `ResponseBody` which provides a stream: |
| 12 | + |
| 13 | +1. **Request completes** - Response returned with `ResponseBody` (unopened stream) |
| 14 | +2. **Inspect response** - User can check status code and headers |
| 15 | +3. **Process data** - When `.toFile()`, `.toArrayBuffer()`, etc. is called: |
| 16 | + - Stream is opened and consumed |
| 17 | + - For `toFile()`: Data streams directly to disk |
| 18 | + - For `toArrayBuffer()`: Data streams into memory |
| 19 | + - For `toJSON()`: Data streams, parsed, returned |
| 20 | + |
| 21 | +**Memory Usage**: Only buffered data in memory during streaming (typically ~8KB at a time) |
| 22 | + |
| 23 | +### iOS (Alamofire) |
8 | 24 |
|
9 | | -### How It Works |
| 25 | +iOS now uses Alamofire's `DownloadRequest` which downloads to a temp file: |
| 26 | + |
| 27 | +1. **Request completes** - Response body downloaded to temp file |
| 28 | +2. **Inspect response** - User can check status code and headers |
| 29 | +3. **Process data** - When `.toFile()`, `.toArrayBuffer()`, etc. is called: |
| 30 | + - For `toFile()`: Temp file is moved to destination (no copy, no memory) |
| 31 | + - For `toArrayBuffer()`: Temp file loaded into memory |
| 32 | + - For `toJSON()`: Temp file loaded and parsed |
| 33 | + - For `toString()`: Temp file loaded as string |
| 34 | + |
| 35 | +**Memory Usage**: Temp file on disk during download, loaded into memory only when explicitly accessed |
| 36 | + |
| 37 | +## Response Handling Behavior |
10 | 38 |
|
11 | 39 | Both iOS and Android now follow the same pattern: |
12 | 40 |
|
@@ -55,58 +83,144 @@ if (response.statusCode === 200) { |
55 | 83 |
|
56 | 84 | On Android, the response includes a `ResponseBody` that provides an input stream: |
57 | 85 |
|
58 | | -- Request completes and returns response |
59 | | -- ResponseBody is available with the data |
60 | | -- When `toFile()` is called, it reads from the ResponseBody stream and writes to disk |
61 | | -- When `toArrayBuffer()` is called, it reads from the ResponseBody stream into memory |
| 86 | +- Request completes and returns response with ResponseBody (stream not yet consumed) |
| 87 | +- ResponseBody stream is available but not opened |
| 88 | +- When `toFile()` is called, it opens the stream and writes to disk chunk by chunk |
| 89 | +- When `toArrayBuffer()` is called, it opens the stream and reads into memory |
| 90 | +- Stream is consumed only once - subsequent calls use cached data |
62 | 91 |
|
63 | 92 | **Native Code Flow:** |
64 | 93 | ```java |
65 | | -// Response is returned with ResponseBody |
| 94 | +// Response is returned with ResponseBody (stream) |
66 | 95 | ResponseBody responseBody = response.body(); |
67 | 96 |
|
68 | 97 | // Later, when toFile() is called: |
69 | 98 | InputStream inputStream = responseBody.byteStream(); |
70 | 99 | FileOutputStream output = new FileOutputStream(file); |
71 | | -// Stream data from input to output |
| 100 | +byte[] buffer = new byte[1024]; |
| 101 | +while ((count = inputStream.read(buffer)) != -1) { |
| 102 | + output.write(buffer, 0, count); // Streaming write |
| 103 | +} |
72 | 104 | ``` |
73 | 105 |
|
| 106 | +**Memory Characteristics:** |
| 107 | +- Only buffer size (~1KB) in memory during streaming |
| 108 | +- Large files: ~1-2MB RAM overhead maximum |
| 109 | +- File writes happen progressively as data arrives |
| 110 | + |
74 | 111 | ### iOS (Alamofire) |
75 | 112 |
|
76 | | -On iOS, the response includes the data as NSData: |
| 113 | +On iOS, the response downloads to a temporary file automatically: |
77 | 114 |
|
78 | | -- Request completes and returns response |
79 | | -- Data is loaded into memory as NSData |
80 | | -- When `toFile()` is called, it writes the NSData to disk |
81 | | -- When `toArrayBuffer()` is called, it converts NSData to ArrayBuffer |
| 115 | +- Request completes and downloads body to temp file |
| 116 | +- Temp file path stored in response object |
| 117 | +- When `toFile()` is called, it moves the temp file to destination (fast file system operation) |
| 118 | +- When `toArrayBuffer()` is called, it loads the temp file into memory |
| 119 | +- When `toJSON()` is called, it loads and parses the temp file |
82 | 120 |
|
83 | 121 | **Native Code Flow:** |
84 | 122 | ```swift |
85 | | -// Response is returned with NSData |
86 | | -let data: NSData = responseData |
| 123 | +// Response downloads to temp file during request |
| 124 | +let tempFileURL = FileManager.default.temporaryDirectory |
| 125 | + .appendingPathComponent(UUID().uuidString) |
| 126 | +// Download happens here, saved to tempFileURL |
87 | 127 |
|
88 | 128 | // Later, when toFile() is called: |
89 | | -data.writeToFile(filePath, atomically: true) |
| 129 | +try FileManager.default.moveItem(at: tempFileURL, to: destinationURL) |
| 130 | +// Fast move operation, no data copying |
| 131 | + |
| 132 | +// Or when toArrayBuffer() is called: |
| 133 | +let data = try Data(contentsOf: tempFileURL) |
| 134 | +// File loaded into memory at this point |
90 | 135 | ``` |
91 | 136 |
|
| 137 | +**Memory Characteristics:** |
| 138 | +- Temp file written to disk during download |
| 139 | +- No memory overhead during download (except small buffer) |
| 140 | +- Memory used only when explicitly loading via toArrayBuffer()/toJSON() |
| 141 | +- toFile() uses file move (no memory overhead) |
| 142 | + |
92 | 143 | ## Memory Considerations |
93 | 144 |
|
94 | | -### Android |
95 | | -- ResponseBody provides a stream, so data isn't necessarily all in memory |
96 | | -- OkHttp may buffer data internally |
97 | | -- Large files will consume memory proportional to the buffering strategy |
| 145 | +### Comparison |
98 | 146 |
|
99 | | -### iOS |
100 | | -- Response data is loaded into memory as NSData |
101 | | -- Large files will consume memory equal to the file size |
102 | | -- This matches Android's effective behavior for most use cases |
| 147 | +| Operation | Android Memory | iOS Memory | |
| 148 | +|-----------|----------------|------------| |
| 149 | +| **During download** | ~1-2MB buffer | ~1-2MB buffer + temp file on disk | |
| 150 | +| **After download** | ResponseBody (minimal) | Temp file on disk (0 RAM) | |
| 151 | +| **toFile()** | Stream to disk (~1MB buffer) | File move (0 RAM) | |
| 152 | +| **toArrayBuffer()** | Load into memory | Load from temp file into memory | |
| 153 | +| **toJSON()** | Stream and parse | Load from temp file and parse | |
103 | 154 |
|
104 | | -### Recommendation |
| 155 | +### Benefits |
| 156 | + |
| 157 | +Both platforms now provide true memory-efficient streaming: |
| 158 | + |
| 159 | +1. **Large File Downloads**: Won't cause OOM errors |
| 160 | +2. **Flexible Processing**: Inspect headers before committing to download |
| 161 | +3. **Efficient File Saving**: Direct streaming (Android) or file move (iOS) |
| 162 | +4. **On-Demand Loading**: Data loaded into memory only when explicitly requested |
| 163 | + |
| 164 | +### Recommendations |
105 | 165 |
|
106 | 166 | For both platforms: |
107 | | -- Small files (<10MB): No concern, data handling is efficient |
108 | | -- Medium files (10-50MB): Monitor memory usage, should work on most devices |
109 | | -- Large files (>50MB): Test on low-memory devices, consider chunked downloads if needed |
| 167 | +- **Small files (<10MB)**: Any method works efficiently |
| 168 | +- **Medium files (10-100MB)**: Use `toFile()` for best memory efficiency |
| 169 | +- **Large files (>100MB)**: Always use `toFile()` to avoid memory issues |
| 170 | +- **JSON APIs**: `toJSON()` works well for responses up to ~50MB |
| 171 | + |
| 172 | +## Example Usage |
| 173 | + |
| 174 | +```typescript |
| 175 | +import { request } from '@nativescript-community/https'; |
| 176 | + |
| 177 | +async function downloadLargeFile() { |
| 178 | + console.log('Starting download...'); |
| 179 | + |
| 180 | + // Step 1: Make the request |
| 181 | + // iOS: Downloads to temp file on disk (not in RAM) |
| 182 | + // Android: Opens connection, keeps ResponseBody stream (not in RAM) |
| 183 | + const response = await request({ |
| 184 | + method: 'GET', |
| 185 | + url: 'https://example.com/large-file.zip', |
| 186 | + onProgress: (current, total) => { |
| 187 | + const percent = (current / total * 100).toFixed(1); |
| 188 | + console.log(`Downloading: ${percent}%`); |
| 189 | + } |
| 190 | + }); |
| 191 | + |
| 192 | + // Step 2: Request completes, inspect the response |
| 193 | + // At this point, large file is NOT in memory on either platform! |
| 194 | + console.log('Download complete!'); |
| 195 | + console.log('Status code:', response.statusCode); |
| 196 | + console.log('Content-Type:', response.headers['Content-Type']); |
| 197 | + console.log('Content-Length:', response.contentLength); |
| 198 | + |
| 199 | + // Step 3: Now decide what to do with the data |
| 200 | + if (response.statusCode === 200) { |
| 201 | + // Option A: Save to file (MOST MEMORY EFFICIENT) |
| 202 | + // iOS: Moves temp file (0 RAM overhead) |
| 203 | + // Android: Streams ResponseBody to file (~1MB RAM overhead) |
| 204 | + const file = await response.content.toFile('~/Downloads/file.zip'); |
| 205 | + console.log('Saved to:', file.path); |
| 206 | + |
| 207 | + // Option B: Load into memory (for processing) |
| 208 | + // iOS: Loads temp file into RAM |
| 209 | + // Android: Streams ResponseBody into RAM |
| 210 | + // WARNING: Use only for files that fit in memory! |
| 211 | + // const buffer = await response.content.toArrayBuffer(); |
| 212 | + // console.log('Buffer size:', buffer.byteLength); |
| 213 | + |
| 214 | + // Option C: Parse as JSON (for APIs) |
| 215 | + // iOS: Loads temp file and parses |
| 216 | + // Android: Streams ResponseBody and parses |
| 217 | + // const json = response.content.toJSON(); |
| 218 | + // console.log('Data:', json); |
| 219 | + } else { |
| 220 | + console.error('Download failed with status:', response.statusCode); |
| 221 | + } |
| 222 | +} |
| 223 | +``` |
110 | 224 |
|
111 | 225 | ## Benefits of Consistent Behavior |
112 | 226 |
|
|
0 commit comments