---
id: native-memory-pressure-ios-android
title: Memory Pressure — iOS / Android
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [mobile, memory, vibe-coding]
tech_stack: { language: "Swift / Kotlin", applicable_to: ["iOS", "Android"] }
applied_in: []
aliases: [memory pressure, OOM, jetsam, low memory warning, onTrimMemory, image cache, leak]
---

# Mobile Memory Pressure

> Mobile = device 가 4-8 GB. **iOS = jetsam, Android = OOM killer**. Image cache + leak 가 주범. Profile + cache eviction.

## 📖 핵심 개념
- iOS jetsam: low memory = app kill (silent).
- Android OOM: heap limit 초과 = exception.
- Image cache 가 주된 cost.
- Background app 가 빨리 kill.

## 💻 코드 패턴

### iOS: Memory warning
```swift
class ViewController: UIViewController {
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Cache 정리
        ImageCache.shared.clearAll()
    }
}

// SwiftUI
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didReceiveMemoryWarningNotification)) { _ in
    cache.clear()
}
```

→ System 가 warn — clear 안 = jetsam (kill).

### Android: onTrimMemory
```kotlin
class MyApp : Application() {
    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        when (level) {
            TRIM_MEMORY_RUNNING_LOW -> imageCache.evictTrim(0.5f)
            TRIM_MEMORY_RUNNING_CRITICAL -> imageCache.clear()
            TRIM_MEMORY_BACKGROUND -> imageCache.clear()
            TRIM_MEMORY_COMPLETE -> imageCache.clear()
        }
    }
}
```

→ 매 level 가 다른 action.

### iOS: 현재 memory 측정
```swift
func currentMemory() -> UInt64 {
    var info = task_vm_info_data_t()
    var count = mach_msg_type_number_t(MemoryLayout<task_vm_info>.size) / 4
    
    let kerr = withUnsafeMutablePointer(to: &info) {
        $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
            task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
        }
    }
    
    if kerr == KERN_SUCCESS {
        return info.phys_footprint
    }
    return 0
}
```

### Android: 현재 memory
```kotlin
val rt = Runtime.getRuntime()
val used = (rt.totalMemory() - rt.freeMemory()) / 1024 / 1024
val max = rt.maxMemory() / 1024 / 1024
```

### Image cache (가장 흔한 cost)
```swift
// iOS — Kingfisher
import Kingfisher

ImageCache.default.memoryStorage.config.totalCostLimit = 100 * 1024 * 1024  // 100 MB
ImageCache.default.diskStorage.config.sizeLimit = 500 * 1024 * 1024          // 500 MB
```

```kotlin
// Android — Coil
val loader = ImageLoader.Builder(context)
    .memoryCache {
        MemoryCache.Builder(context).maxSizePercent(0.25).build()
    }
    .diskCache {
        DiskCache.Builder().directory(...).maxSizeBytes(500_000_000).build()
    }
    .build()
```

→ 25% RAM = image cache. Limit 가 OOM 방지.

### Image downsampling
```swift
// 큰 이미지 → 화면 size 만 decode
let request = ImageRequest(url: url, processors: [
    .resize(size: CGSize(width: 300, height: 300))
])
```

```kotlin
// Coil
ImageRequest.Builder(context)
    .data(url)
    .size(300, 300)   // downsample
    .build()
```

→ 4K 이미지 가 그대로 = 33 MB. Resize → 360 KB.

### Bitmap reuse (Android)
```kotlin
val options = BitmapFactory.Options().apply {
    inMutable = true
    inBitmap = recycledBitmap   // reuse memory
}

val bitmap = BitmapFactory.decodeFile(path, options)
```

→ Coil 가 자동.

### Leak (iOS)
```swift
// ❌ Strong reference cycle
class A {
    var b: B
}
class B {
    var a: A   // ← A retain, A 의 B retain → cycle.
}

// ✅ weak / unowned
class B {
    weak var a: A?
}
```

### Closure leak
```swift
// ❌
class VC {
    var name = ""
    func setup() {
        button.addAction { 
            print(self.name)   // ← self capture (strong)
        }
    }
}

// ✅
button.addAction { [weak self] in
    print(self?.name ?? "")
}
```

### Combine subscription
```swift
class VC {
    var cancellables = Set<AnyCancellable>()
    
    func setup() {
        publisher
            .sink { [weak self] value in self?.handle(value) }
            .store(in: &cancellables)
    }
    
    deinit {
        // cancellables 가 deinit 에 release.
    }
}
```

### Android: leak (Activity)
```kotlin
// ❌ static 가 Activity 참조
object MyManager {
    var activity: Activity? = null   // → Activity rotate 후 leak.
}

// ✅ ApplicationContext
context.applicationContext
```

### LeakCanary (Android)
```kotlin
// Auto detect
dependencies {
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.13'
}
```

→ Leak 발견 = notification + report.

### Instruments (iOS)
```
Xcode > Open Developer Tool > Instruments.
- Allocations: 매 type 의 instance 수.
- Leaks: leak detected.
- VM Tracker: virtual memory.
```

→ Profile 가 진짜 답.

### Memory graph (Xcode)
```
Pause app → Debug Memory Graph.
- 모든 instance + reference 시각화.
- Cycle 발견.
- "이 ViewController 가 안 release?" 검증.
```

### Background limit
```
iOS: ~30 MB (Background App).
Android: ~150 MB (varies).

→ Background = 적은 work + small footprint.
```

### Image format
```
PNG: lossless, 큰.
JPEG: lossy, 작은.
WebP: 작은 + 좋은 quality.
HEIF (iOS): 30% 작음 보다 JPEG.
AVIF: 50% 작음 (수년 future).
```

### Lazy load
```swift
// SwiftUI LazyVStack / LazyHStack
LazyVStack {
    ForEach(items) { Card($0) }
}
```

```kotlin
// Compose LazyColumn
LazyColumn {
    items(items) { Card(it) }
}
```

→ Visible 만 render.

### Pagination
```swift
// 매 page 100 item.
// Scroll bottom = 다음 page.
```

→ 1M item 한번에 = OOM.

### Background image processing
```swift
DispatchQueue.global(qos: .userInitiated).async {
    let processed = process(image)
    DispatchQueue.main.async {
        imageView.image = processed
    }
}
```

→ Main thread 가 큰 image = jank.

### File I/O 가 memory
```swift
// ❌ 큰 file 전체 load
let data = try Data(contentsOf: url)

// ✅ stream
let stream = InputStream(url: url)!
stream.open()
var buffer = [UInt8](repeating: 0, count: 4096)
while stream.hasBytesAvailable {
    let read = stream.read(&buffer, maxLength: 4096)
    process(buffer, read)
}
stream.close()
```

### URLSession download
```swift
// Stream download (memory 작음)
URLSession.shared.downloadTask(with: url) { tempURL, _, _ in
    // tempURL 가 disk file.
}.resume()

// vs dataTask (memory 큰)
URLSession.shared.dataTask(with: url) { data, _, _ in
    // data 가 memory.
}.resume()
```

### Database (Room / Core Data)
```
큰 query result = OOM.

→ Pagination + cursor.
Room: PagingSource.
Core Data: NSFetchedResultsController + batchSize.
```

### 함정
```
- Image load + 그대로 전체 size: 큰 cost.
- Static singleton 가 Activity / VC 참조: leak.
- Closure 가 self 강 참조: leak.
- Background 가 cache 안 evict: jetsam.
- Lazy 안 + 큰 list: OOM.
- 큰 file 가 전체 load: OOM.
- Bitmap reuse 안 함: alloc 폭발.
```

### Eval
```
Production:
- Crash report 의 OOM count.
- 90% percentile memory usage.
- Background eviction frequency.

→ Crashlytics / Sentry.
```

### Crash from low memory
```
iOS: jetsam (silent kill, no crash log).
- "App 가 갑자기 종료" 사용자 보고.
- Diagnostic = Xcode Organizer > Crashes > Jetsam.

Android: OutOfMemoryError exception.
- Crash log 가 visible.
- Heap dump 가능.
```

### Memory budget per device
```
iPhone 8: 1.5 GB total, 800 MB / app warning.
iPhone 15: 6 GB total, 3 GB / app.
Android low-end: 2 GB, 192 MB / app.
Android flagship: 12 GB, 1+ GB.

→ Old / low-end device 가 strict.
```

→ Test on low-end.

## 🤔 의사결정 기준
| 작업 | 추천 |
|---|---|
| Image cache | LRU + size limit |
| 큰 image | Downsample to view size |
| 큰 file | Stream / chunk |
| 큰 list | Lazy + pagination |
| Background | Cache evict |
| Memory warning | Clear non-critical |
| Leak | Profiler (LeakCanary, Instruments) |
| Database | Pagination |

## ❌ 안티패턴
- **Image 가 그대로 size**: 4K 이미지 = MB.
- **Static 가 Activity 참조**: leak.
- **Cache 무한**: OOM.
- **Memory warning 무시**: jetsam.
- **Background 가 full memory**: 빨리 kill.
- **Closure self 강 참조**: cycle.
- **Profile 안 함**: blind.

## 🤖 LLM 활용 힌트
- Image cache 가 가장 큰 cost.
- LeakCanary / Instruments 가 leak 검출.
- iOS jetsam = silent kill, Android OOM = exception.
- Lazy load + pagination 필수.

## 🔗 관련 문서
- [[Native_Memory_Profiling]]
- [[Mobile_App_Size_Optimization]]
- [[Mobile_Crash_Free_SLO]]
