Suspended — що це, заморозка застосунку в фоні iOS

Автор: IT Sectr Опубліковано: 2026-03-03 Час читання: 11 хв

Suspended — призупинений стан життєвого циклу iOS-застосунку, в якому він заморожений у пам'яті, але не виконує код. Показуємо, як працює Suspended, які ризики несе заморозка застосунку в фоні, як iOS керує вивантаженням Suspended-застосунків та як реалізувати state restoration для безшовного відновлення після повернення з Suspended.

Головне

  • Suspended — застосунок заморожений у пам'яті, код не виконується, UI-стек збережено
  • iOS Suspended — унікальний стан, якого немає в Android; процес існує, але не активний
  • Вивантаження — при нестачі пам'яті Suspended-застосунки видаляються першими, дані в пам'яті втрачаються
  • State Restoration — механізм iOS для збереження та відновлення UI-стеку після вивантаження
  • didEnterBackground — останній метод, який гарантовано викликається перед Suspended

Suspended — що це за стан

Suspended — стан життєвого циклу iOS-застосунку, в якому він знаходиться в оперативній пам'яті пристрою, але не виконує жодного коду. Це фінальний стан перед повним завершенням: застосунок переходить у Suspended з Background після завершення всіх фонових завдань або після закінчення таймауту. У Suspended застосунок повністю заморожений — всі потоки призупинені, таймери не працюють, мережева активність відсутня.

Suspended — унікальна особливість iOS, відсутня в стандартному життєвому циклі Android. Причина в різній архітектурі управління процесами. iOS зберігає образ застосунку в пам'яті (аналог гібернації на десктопі), щоб при поверненні користувача миттєво відновити інтерфейс без холодного старту. Android не має Suspended — процес або існує і може виконувати код (Background), або завершений (Not Running), хоча Android може призупинити виконання потоків через LMK.

Для користувача Suspended виглядає як миттєве відновлення: він перемикається між застосунками через App Switcher, і кожен застосунок відкривається з того ж місця, де він його залишив. Це створює ілюзію, що всі застосунки працюють одночасно. Насправді більшість із них заморожені в Suspended. Гарячий старт із Suspended в рази швидше холодного старту з Not Running, оскільки код уже завантажений у пам'ять.

Як система керує Suspended

iOS відстежує стан всіх застосунків і приймає рішення про вивантаження Suspended-застосунків на основі доступної пам'яті. При нестачі пам'яті система починає вивантажувати Suspended-застосунки, починаючи з тих, які найдовше перебувають у цьому стані. Якщо пам'яті все ще недостатньо, система переводить застосунки з Background та Inactive в Suspended з подальшим вивантаженням. Цей процес повністю прозорий для користувача — він просто бачить іконку застосунку в App Switcher, яка при натисканні запускає холодний старт.

ХарактеристикаSuspended (iOS)Background (iOS)Background (Android)
Код виконуєтьсяНіТак (обмежено)Так (обмежено)
У пам'ятіТакТакТак
Споживання CPU0%НизькеНизьке
Гарячий стартТак — миттєве відновленняТак — через InactiveНі — процес міг бути вбитий
ТаймаутНі — може бути в пам'яті годинами~30 секунд (після beginBackgroundTask)Залежить від версії API
Вивантаження системоюПри нестачі пам'ятіПри критичній нестачі пам'ятіLMK (Low Memory Killer)
Повернення до роботиЗ App Switcher — миттєвоЗ App Switcher — через InactiveХолодний старт
State RestorationБажанийНе потрібенSavedStateHandle

Suspended в iOS: механізм заморозки

В iOS Suspended досягається автоматично після завершення всіх фонових завдань. Система викликає applicationDidEnterBackground, дає час на виконання beginBackgroundTask (близько 30 секунд), після чого примусово призупиняє всі потоки та переводить застосунок у Suspended. Об'єкти в пам'яті зберігаються, але жоден код не виконується — застосунок заморожений у поточному стані.

Критично важливий момент: applicationDidEnterBackground — останній метод, який гарантовано викликається перед Suspended. Після цього застосунок не отримує жодних сповіщень про вивантаження з пам'яті. Якщо користувач або система вбиває застосунок, що знаходиться в Suspended, не викликається ні applicationWillTerminate, ні applicationDidEnterBackground повторно. Тому все збереження даних має відбуватися в applicationDidEnterBackground, а не в applicationWillTerminate.

swift
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    // Останній гарантований виклик перед Suspended
    func applicationDidEnterBackground(_ application: UIApplication) {
        // Зберігаємо все, що має пережити вивантаження з пам'яті
        savePersistentState()
        saveNavigationStack()

        // Запитуємо додатковий час, якщо потрібно
        let task = application.beginBackgroundTask {
            application.endBackgroundTask(task)
        }
    }

    // Повернення з Suspended — гарячий старт
    func applicationWillEnterForeground(_ application: UIApplication) {
        // Застосунок був у Suspended, повертаємося до роботи
        print("Повернення з Suspended або Background")
    }

    // Повне відновлення після вивантаження з пам'яті
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // Якщо це холодний старт після вивантаження з Suspended —
        // відновлюємо state restoration
        return true
    }

    private func savePersistentState() {
        UserDefaults.standard.set(Date(), forKey: "lastActiveDate")
    }

    private func saveNavigationStack() {
        guard let rootVC = window?.rootViewController else { return }
        // Зберігаємо поточний навігаційний стек
        if let navController = rootVC as? UINavigationController {
            let vcClasses = navController.viewControllers.map { type(of: $0) }
            UserDefaults.standard.set(vcClasses.map { NSStringFromClass($0) }, forKey: "navStack")
        }
    }
}

У коді показана критично важлива обробка Suspended на iOS. applicationDidEnterBackground — останній гарантований виклик. Все збереження даних має відбуватися тут: стан користувача, навігаційний стек, чернетки, таймери. applicationWillEnterForeground викликається при поверненні з Suspended або Background. didFinishLaunchingWithOptions — тільки при холодному старті, коли застосунок був вивантажений з пам'яті після Suspended.

Suspended в Android — чи існує аналог

В Android немає прямого аналога iOS Suspended. Android не заморожує застосунки в пам'яті зі збереженням контексту виконання. Замість цього Android або тримає процес у фоні (Background), або завершує його (Not Running). Однак на Android 11+ (API 30) з'явився механізм App Freezer, який призупиняє виконання фонових процесів за допомогою сигналу SIGSTOP. Це функціональний аналог Suspended, але з важливими відмінностями.

App Freezer — частина системи управління пам'яттю Android. Коли застосунок довго перебуває в фоні та не має активних сповіщень, система надсилає йому SIGSTOP, призупиняючи всі потоки. При поверненні застосунку на передній план надсилається SIGCONT, і виконання відновлюється. Ключова відмінність від iOS: App Freezer не гарантує збереження стану — дані в пам'яті можуть бути втрачені, якщо процес буде вбито під час заморозки.

На Android рекомендується використовувати SavedStateHandle в ViewModel для автоматичного збереження стану при будь-якому завершенні процесу. SavedStateHandle зберігає дані в Bundle через onSaveInstanceState, який переживає як App Freezer, так і Process Death. На відміну від iOS, де вивантаження з Suspended — виняткова ситуація, на Android Process Death — нормальна поведінка, яка має очікуватися завжди.

kotlin
// SavedStateHandle — спасіння від Process Death на Android
class CheckoutViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    // Стан, який переживе процес навіть після App Freezer
    var currentStep: MutableLiveData<Int> =
        savedStateHandle.getLiveData("checkout_step", 1)

    var cartItems: MutableLiveData<List<CartItem>> =
        savedStateHandle.getLiveData("cart_items", emptyList())

    fun proceedToNextStep() {
        currentStep.value = (currentStep.value ?: 0) + 1
    }

    fun addToCart(item: CartItem) {
        val updatedList = (cartItems.value ?: emptyList()) + item
        cartItems.value = updatedList
        savedStateHandle["cart_items"] = updatedList
    }
}

// Збереження в onStop на випадок App Freezer
class MainActivity : AppCompatActivity() {

    override fun onStop() {
        super.onStop()
        // Зберігаємо дані, які мають пережити заморозку
        saveDraftData()
        // Звільняємо ресурси, які не потрібні в замороженому стані
        releaseHeavyResources()
        // Попереджаємо, що застосунок буде заморожено
        // (Логування для налагодження)
        Log.d("Lifecycle", "Activity stopped — possible App Freeze")
    }
}

У коді показано підхід до обробки аналога Suspended на Android. SavedStateHandle в ViewModel автоматично зберігає та відновлює дані при Process Death. onStop — остання гарантована подія перед можливим App Freezer або завершенням процесу. Стан форми оформлення замовлення, список товарів у кошику — всі ці дані переживають заморозку завдяки SavedStateHandle. Для важких ресурсів (бітмапи, курсори БД) onStop — місце для звільнення пам'яті.

State Restoration: відновлення після Suspended

State Restoration — вбудований механізм iOS для збереження та відновлення UI-стану після вивантаження застосунку з пам'яті. Якщо застосунок був у Suspended і система вивантажила його, при наступному холодному старті state restoration відновлює навігаційний стек, позицію прокрутки, стан форм та інші UI-елементи. Користувач повертається на той самий екран, де зупинився.

State Restoration працює через протоколи UIViewControllerRestoration та UIStateRestoring. Розробник присвоює restorationIdentifier кожному ViewController та View, які хоче відновити. При переході в фон iOS кодує стан цих об'єктів. При поверненні після вивантаження iOS створює нові об'єкти та декодує збережений стан. Без state restoration користувач побачить порожній екран після холодного старту замість того місця, де зупинився.

swift
import UIKit

class DetailViewController: UIViewController {

    var itemID: String = ""
    var scrollPosition: CGPoint = .zero

    override func viewDidLoad() {
        super.viewDidLoad()
        restorationIdentifier = "DetailViewController"
        restorationClass = type(of: self)
    }

    override func encodeRestorableState(with coder: NSCoder) {
        super.encodeRestorableState(with: coder)
        coder.encode(itemID, forKey: "itemID")
        coder.encode(scrollPosition, forKey: "scrollPosition")
    }

    override func decodeRestorableState(with coder: NSCoder) {
        super.decodeRestorableState(with: coder)
        if let savedID = coder.decodeObject(forKey: "itemID") as? String {
            itemID = savedID
            loadItem()
        }
        if let savedPosition = coder.decodeCGPoint(forKey: "scrollPosition") {
            scrollPosition = savedPosition
            // Відновлюємо позицію після завантаження даних
        }
    }
}

// AppDelegate — активація State Restoration
func application(
    _ application: UIApplication,
    shouldSaveSecureApplicationState coder: NSCoder
) -> Bool {
    return true
}

func application(
    _ application: UIApplication,
    shouldRestoreSecureApplicationState coder: NSCoder
) -> Bool {
    return true
}

У коді показана реалізація State Restoration на iOS. restorationIdentifier та restorationClass необхідні кожному відновлюваному ViewController. encodeRestorableState/decodeRestorableState зберігають та завантажують дані через NSCoder. В AppDelegate shouldSaveSecureApplicationState та shouldRestoreSecureApplicationState вмикають зашифроване збереження стану. З iOS 12+ рекомендується використовувати безпечне кодування (NSSecureCoding) для захисту даних.

Найкращі практики роботи з Suspended

Перше правило — ніколи не розраховуйте, що застосунок повернеться з Suspended. Система може вивантажити застосунок у будь-який момент. Всі критично важливі дані мають бути збережені в постійне сховище до переходу в Suspended — тобто в applicationDidEnterBackground або onStop. UserDefaults, Core Data, File Manager — підходящі сховища. Пам'ять (змінні, властивості) — ненадійне сховище для даних, які мають пережити Suspended.

Друге правило — звільняйте ресурси перед Suspended. Закривайте файлові дескриптори, звільняйте GPU-пам'ять (Metal, Core Graphics), закривайте мережеві з'єднання. Хоча застосунок не споживає CPU в Suspended, зайняті ресурси блокуються для інших застосунків. На iOS в Suspended не можна тримати відкриті сокети — при поверненні з Suspended вони можуть бути непрацездатними, що викличе помилки.

Третє правило — не розміщуйте логіку, залежну від часу, в очікуванні повернення з Suspended. Таймери, callback-и та мережева активність припиняються в Suspended. Якщо застосунок був у Suspended кілька годин, при поверненні таймер може спрацювати некоректно. Перевіряйте актуальність даних при поверненні — можливо, кеш застарів, а токен авторизації минув.

Четверте правило — використовуйте State Restoration для всіх екранів, особливо для форм введення, списків з прокруткою та детальних екранів. Без state restoration користувач після повернення з вивантаженого Suspended побачить початковий екран застосунку замість того місця, де зупинився. Це погіршує користувацький досвід і змушує користувача повторювати дії.

swift
import UIKit

// Перевірка: чи був застосунок вивантажений з пам'яті?
func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    // Перевіряємо, чи є збережений стан
    if UserDefaults.standard.object(forKey: "navStack") != nil {
        // Застосунок був вивантажений з Suspended
        // Потрібно відновити стан
        restoreNavigationStack()
    } else {
        // Чистий холодний старт з Not Running
        showOnboardingIfNeeded()
    }
    return true
}

private func restoreNavigationStack() {
    guard let savedStack = UserDefaults.standard.array(forKey: "navStack") as? [String],
          let navController = window?.rootViewController as? UINavigationController
    else { return }

    for vcClassName in savedStack {
        if let vcClass = NSClassFromString(vcClassName) as? UIViewController.Type {
            let vc = vcClass.init()
            navController.pushViewController(vc, animated: false)
        }
    }
}

У коді показана практика визначення, чи був застосунок вивантажений із Suspended. Перевірка UserDefaults на наявність збереженого навігаційного стеку дозволяє відрізнити холодний старт після вивантаження від чистого холодного старту. У першому випадку відновлюється навігаційний стек, у другому — показується онбординг або головний екран. Такий підхід доповнює вбудований State Restoration для випадків, коли NSCoder недостатньо.

Часто задавані питання

Скільки часу застосунок може перебувати в Suspended?

Необмежено — від кількох секунд до кількох днів. iOS не має таймауту для Suspended. Застосунок висітиме в пам'яті, поки система не вирішить його вивантажити через нестачу ресурсів. На практиці застосунки залишаються в Suspended від 15 хвилин до кількох годин, залежно від об'єму оперативної пам'яті пристрою та кількості активних застосунків.

Чи викликається applicationWillTerminate при вивантаженні з Suspended?

Ні. applicationWillTerminate не викликається при вивантаженні застосунку з Suspended. Система просто звільняє пам'ять без сповіщення застосунку. Це ще одна причина, чому всі збереження даних мають відбуватися в applicationDidEnterBackground. applicationWillTerminate викликається тільки коли користувач вручну завершує застосунок свайпом з App Switcher.

Чи є у Android аналог Suspended?

Прямого аналога немає. На Android 11+ з'явився App Freezer, який призупиняє фонові процеси через SIGSTOP — це функціонально схоже на Suspended. Однак Android-застосунки мають проектуватися в розрахунку на Process Death у будь-який момент. Використовуйте SavedStateHandle в ViewModel та onSaveInstanceState для збереження стану, який переживе і App Freezer, і Process Death.

Як перевірити, чи був застосунок у Suspended?

В iOS немає прямого API для перевірки. Непрямий метод: перевірити UserDefaults на наявність збереженого стану в didFinishLaunchingWithOptions. Якщо стан є — застосунок був вивантажений із Suspended і стартує холодно. Якщо стану немає — чистий холодний старт. В SwiftUI можна зберігати прапорець у scenePhase.background і перевіряти його при наступному запуску.

Що таке snapshot в контексті Suspended?

При переході в Suspended iOS робить snapshot — скріншот поточного UI застосунку. Цей скріншот показується в App Switcher та при поверненні в застосунок (як анімація "розморозки"). Якщо застосунок містить конфіденційні дані, snapshot може їх розкрити. Для захисту використовуйте UIApplication.shouldSnapshotSecureApp (iOS 16+) або накладайте blur-оверлей в applicationDidEnterBackground.

Підсумки

  • Suspended — застосунок заморожений у пам'яті iOS, код не виконується, але UI-стек збережено для миттєвого відновлення
  • Унікальність iOS — Suspended відсутній в Android; Android використовує App Freezer (SIGSTOP) як частковий аналог
  • Вивантаження — система вивантажує Suspended-застосунки першою чергою при нестачі пам'яті без сповіщення
  • Збереження — applicationDidEnterBackground — останній гарантований метод, всі дані мають зберігатися тут
  • State Restoration — NSCoder-механізм для автоматичного відновлення UI після вивантаження з Suspended
  • Android альтернатива — SavedStateHandle + onSaveInstanceState для переживання Process Death
  • Snapshot — iOS робить скріншот при Suspended; конфіденційні дані потрібно приховувати через blur-оверлей або secure snapshot

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також