Suspended — stan zawieszenia cyklu życia aplikacji iOS, w którym jest ona zamrożona w pamięci, ale nie wykonuje kodu. Pokazujemy, jak działa Suspended, jakie ryzyko niesie zamrożenie aplikacji w tle, jak iOS zarządza wyładowywaniem aplikacji w stanie Suspended i jak zaimplementować state restoration dla bezszwowego przywracania po powrocie z Suspended.
Najważniejsze
Suspended — stan cyklu życia aplikacji iOS, w którym znajduje się ona w pamięci operacyjnej urządzenia, ale nie wykonuje żadnego kodu. To końcowy stan przed całkowitym zakończeniem: aplikacja przechodzi w Suspended z Background po zakończeniu wszystkich zadań w tle lub po upływie limitu czasu. W Suspended aplikacja jest całkowicie zamrożona — wszystkie wątki są wstrzymane, timery nie działają, aktywność sieciowa nie występuje.
Suspended — unikalna cecha iOS, niewystępująca w standardowym cyklu życia Androida. Powodem jest różna architektura zarządzania procesami. iOS zachowuje obraz aplikacji w pamięci (analog hibernacji na desktopie), aby po powrocie użytkownika natychmiast przywrócić interfejs bez zimnego startu. Android nie ma Suspended — proces albo istnieje i może wykonywać kod (Background), albo jest zakończony (Not Running), choć Android może wstrzymać wykonanie wątków przez LMK.
Dla użytkownika Suspended wygląda jak natychmiastowe przywrócenie: przełącza się między aplikacjami przez App Switcher, a każda aplikacja otwiera się w tym samym miejscu, w którym ją zostawił. To stwarza złudzenie, że wszystkie aplikacje działają jednocześnie. W rzeczywistości większość z nich jest zamrożona w Suspended. Gorący start z Suspended jest znacznie szybszy niż zimny start z Not Running, ponieważ kod jest już załadowany do pamięci.
iOS śledzi stan wszystkich aplikacji i podejmuje decyzję o wyładowaniu aplikacji w stanie Suspended na podstawie dostępnej pamięci. Gdy brakuje pamięci, system zaczyna wyładowywać aplikacje w stanie Suspended, zaczynając od tych, które najdłużej znajdują się w tym stanie. Jeśli pamięci wciąż brakuje, system przenosi aplikacje z Background i Inactive do Suspended, a następnie je wyładowuje. Ten proces jest całkowicie przezroczysty dla użytkownika — widzi on jedynie ikonę aplikacji w App Switcher, która po kliknięciu uruchamia zimny start.
| Cecha | Suspended (iOS) | Background (iOS) | Background (Android) |
|---|---|---|---|
| Kod jest wykonywany | Nie | Tak (ograniczenie) | Tak (ograniczenie) |
| W pamięci | Tak | Tak | Tak |
| Zużycie CPU | 0% | Niskie | Niskie |
| Gorący start | Tak — natychmiastowe przywrócenie | Tak — przez Inactive | Nie — proces mógł zostać zabity |
| Limit czasu | Nie — może być w pamięci godzinami | ~30 sekund (po beginBackgroundTask) | Zależy od wersji API |
| Wyładowanie przez system | Przy braku pamięci | Przy krytycznym braku pamięci | LMK (Low Memory Killer) |
| Powrót do działania | Z App Switcher — natychmiast | Z App Switcher — przez Inactive | Zimny start |
| State Restoration | Zalecany | Nie wymagany | SavedStateHandle |
W iOS Suspended jest osiągany automatycznie po zakończeniu wszystkich zadań w tle. System wywołuje applicationDidEnterBackground, daje czas na wykonanie beginBackgroundTask (około 30 sekund), po czym przymusowo wstrzymuje wszystkie wątki i przenosi aplikację w stan Suspended. Obiekty w pamięci są zachowane, ale żaden kod nie jest wykonywany — aplikacja jest zamrożona w bieżącym stanie.
Krytycznie ważny moment: applicationDidEnterBackground — ostatnia metoda, która jest gwarantowanie wywoływana przed Suspended. Po tym aplikacja nie otrzymuje żadnych powiadomień o wyładowaniu z pamięci. Jeśli użytkownik lub system zabija aplikację znajdującą się w Suspended, nie jest wywoływane ani applicationWillTerminate, ani ponownie applicationDidEnterBackground. Dlatego całe zapisywanie danych musi odbywać się w applicationDidEnterBackground, a nie w applicationWillTerminate.
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
// Ostatnie gwarantowane wywołanie przed Suspended
func applicationDidEnterBackground(_ application: UIApplication) {
// Zapisujemy wszystko, co ma przetrwać wyładowanie z pamięci
savePersistentState()
saveNavigationStack()
// Prosimy o dodatkowy czas, jeśli potrzebny
let task = application.beginBackgroundTask {
application.endBackgroundTask(task)
}
}
// Powrót z Suspended — gorący start
func applicationWillEnterForeground(_ application: UIApplication) {
// Aplikacja była w Suspended, wracamy do pracy
print("Powrót z Suspended lub Background")
}
// Całkowite przywrócenie po wyładowaniu z pamięci
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Jeśli to zimny start po wyładowaniu z Suspended —
// przywracamy state restoration
return true
}
private func savePersistentState() {
UserDefaults.standard.set(Date(), forKey: "lastActiveDate")
}
private func saveNavigationStack() {
guard let rootVC = window?.rootViewController else { return }
// Zapisujemy bieżący stos nawigacji
if let navController = rootVC as? UINavigationController {
let vcClasses = navController.viewControllers.map { type(of: $0) }
UserDefaults.standard.set(vcClasses.map { NSStringFromClass($0) }, forKey: "navStack")
}
}
}Kod pokazuje krytycznie ważne obsługę Suspended na iOS. applicationDidEnterBackground — ostatnie gwarantowane wywołanie. Wszystkie zapisy danych powinny odbywać się tutaj: stan użytkownika, stos nawigacji, szkice, timery. applicationWillEnterForeground jest wywoływane przy powrocie z Suspended lub Background. didFinishLaunchingWithOptions — tylko przy zimnym starcie, gdy aplikacja została wyładowana z pamięci po Suspended.
W Androidzie nie ma bezpośredniego odpowiednika iOS Suspended. Android nie zamraża aplikacji w pamięci z zachowaniem kontekstu wykonania. Zamiast tego Android albo utrzymuje proces w tle (Background), albo go kończy (Not Running). Jednak na Android 11+ (API 30) pojawił się mechanizm App Freezer, który wstrzymuje wykonywanie procesów w tle za pomocą sygnału SIGSTOP. Jest to funkcjonalny odpowiednik Suspended, ale z istotnymi różnicami.
App Freezer — część systemu zarządzania pamięcią Androida. Gdy aplikacja długo znajduje się w tle i nie ma aktywnych powiadomień, system wysyła jej SIGSTOP, wstrzymując wszystkie wątki. Po przywróceniu aplikacji na pierwszy plan wysyłany jest SIGCONT i wykonywanie jest wznawiane. Kluczowa różnica w stosunku do iOS: App Freezer nie gwarantuje zachowania stanu — dane w pamięci mogą zostać utracone, jeśli proces zostanie zabity podczas zamrożenia.
Na Androidzie zaleca się używanie SavedStateHandle w ViewModel do automatycznego zapisywania stanu przy każdym zakończeniu procesu. SavedStateHandle zapisuje dane w Bundle przez onSaveInstanceState, który przetrwa zarówno App Freezer, jak i Process Death. W przeciwieństwie do iOS, gdzie wyładowanie z Suspended jest wyjątkową sytuacją, na Androidzie Process Death to normalne zachowanie, którego należy zawsze oczekiwać.
// SavedStateHandle — ratunek przed Process Death na Androidzie
class CheckoutViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// Stan, który przetrwa proces nawet po 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
}
}
// Zapis w onStop na wypadek App Freezer
class MainActivity : AppCompatActivity() {
override fun onStop() {
super.onStop()
// Zapisujemy dane, które mają przetrwać zamrożenie
saveDraftData()
// Zwalniamy zasoby, które nie są potrzebne w stanie zamrożonym
releaseHeavyResources()
// Ostrzegamy, że aplikacja zostanie zamrożona
// (Logowanie do debugowania)
Log.d("Lifecycle", "Activity zatrzymana — możliwe App Freeze")
}
}Kod pokazuje podejście do obsługi odpowiednika Suspended na Androidzie. SavedStateHandle w ViewModel automatycznie zapisuje i przywraca dane przy Process Death. onStop — ostatnie gwarantowane zdarzenie przed możliwym App Freezer lub zakończeniem procesu. Stan formularza zamówienia, lista produktów w koszyku — wszystkie te dane przetrwają zamrożenie dzięki SavedStateHandle. Dla ciężkich zasobów (bitmapy, kursory bazy danych) onStop jest miejscem do zwalniania pamięci.
State Restoration — wbudowany mechanizm iOS do zapisywania i przywracania stanu UI po wyładowaniu aplikacji z pamięci. Jeśli aplikacja była w Suspended i system ją wyładował, przy następnym zimnym starcie state restoration przywraca stos nawigacji, pozycję scrolla, stan formularzy i inne elementy UI. Użytkownik wraca do tego samego ekranu, na którym się zatrzymał.
State Restoration działa poprzez protokoły UIViewControllerRestoration i UIStateRestoring. Deweloper przypisuje restorationIdentifier każdemu ViewController i View, które chce przywrócić. Przy przejściu w Background iOS koduje stan tych obiektów. Po powrocie po wyładowaniu iOS tworzy nowe obiekty i dekoduje zapisany stan. Bez state restoration użytkownik zobaczy pusty ekran po zimnym starcie zamiast miejsca, w którym się zatrzymał.
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
// Przywracamy pozycję po załadowaniu danych
}
}
}
// AppDelegate — aktywacja State Restoration
func application(
_ application: UIApplication,
shouldSaveSecureApplicationState coder: NSCoder
) -> Bool {
return true
}
func application(
_ application: UIApplication,
shouldRestoreSecureApplicationState coder: NSCoder
) -> Bool {
return true
}Kod pokazuje implementację State Restoration na iOS. restorationIdentifier i restorationClass są niezbędne dla każdego przywracanego ViewController. encodeRestorableState/decodeRestorableState zapisują i ładują dane przez NSCoder. W AppDelegate shouldSaveSecureApplicationState i shouldRestoreSecureApplicationState włączają szyfrowane zapisywanie stanu. Od iOS 12+ zaleca się używanie secure encoding (NSSecureCoding) do ochrony danych.
Pierwsza zasada — nigdy nie zakładaj, że aplikacja wróci z Suspended. System może wyładować aplikację w dowolnym momencie. Wszystkie krytycznie ważne dane muszą być zapisane w trwałym magazynie przed przejściem w Suspended — to znaczy w applicationDidEnterBackground lub onStop. UserDefaults, Core Data, File Manager — odpowiednie magazyny. Pamięć (zmienne, właściwości) — zawodny magazyn dla danych, które mają przetrwać Suspended.
Druga zasada — zwalniaj zasoby przed Suspended. Zamykaj deskryptory plików, zwalniaj pamięć GPU (Metal, Core Graphics), zamykaj połączenia sieciowe. Mimo że aplikacja nie zużywa CPU w Suspended, zajęte zasoby są blokowane dla innych aplikacji. Na iOS w Suspended nie można utrzymywać otwartych socketów — po powrocie z Suspended mogą one być niedziałające, co spowoduje błędy.
Trzecia zasada — nie umieszczaj logiki zależnej od czasu w oczekiwaniu na powrót z Suspended. Timery, callbacki i aktywność sieciowa są zatrzymywane w Suspended. Jeśli aplikacja była w Suspended przez kilka godzin, po powrocie timer może zadziałać nieprawidłowo. Sprawdzaj aktualność danych przy powrocie — być może cache jest nieaktualny, a token autoryzacyjny wygasł.
Czwarta zasada — używaj State Restoration dla wszystkich ekranów, szczególnie dla formularzy, list z przewijaniem i ekranów szczegółowych. Bez state restoration użytkownik po powrocie z wyładowanego Suspended zobaczy początkowy ekran aplikacji zamiast miejsca, w którym się zatrzymał. Pogarsza to doświadczenie użytkownika i zmusza go do powtarzania czynności.
import UIKit
// Sprawdzenie: czy aplikacja została wyładowana z pamięci?
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Sprawdzamy, czy istnieje zapisany stan
if UserDefaults.standard.object(forKey: "navStack") != nil {
// Aplikacja została wyładowana z Suspended
// Należy przywrócić stan
restoreNavigationStack()
} else {
// Czysty zimny start z 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)
}
}
}Kod pokazuje praktykę określania, czy aplikacja została wyładowana z Suspended. Sprawdzenie UserDefaults na obecność zapisanego stosu nawigacji pozwala odróżnić zimny start po wyładowaniu od czystego zimnego startu. W pierwszym przypadku przywracany jest stos nawigacji, w drugim — wyświetlany jest onboarding lub ekran główny. Takie podejście uzupełnia wbudowany State Restoration w przypadkach, gdy NSCoder nie wystarcza.
Często zadawane pytania
Bez ograniczeń — od kilku sekund do kilku dni. iOS nie ma limitu czasu dla Suspended. Aplikacja będzie pozostawać w pamięci, dopóki system nie zdecyduje się jej wyładować z powodu braku zasobów. W praktyce aplikacje pozostają w Suspended od 15 minut do kilku godzin, w zależności od ilości pamięci RAM urządzenia i liczby aktywnych aplikacji.
Nie. applicationWillTerminate nie jest wywoływane przy wyładowaniu aplikacji z Suspended. System po prostu zwalnia pamięć bez powiadamiania aplikacji. To kolejny powód, dlaczego wszystkie zapisy danych powinny odbywać się w applicationDidEnterBackground. applicationWillTerminate jest wywoływane tylko wtedy, gdy użytkownik ręcznie zakończy aplikację przeciągnięciem z App Switcher.
Bezpośredniego odpowiednika nie ma. Na Android 11+ pojawił się App Freezer, który wstrzymuje procesy w tle przez SIGSTOP — jest to funkcjonalnie podobne do Suspended. Jednak aplikacje Androida powinny być projektowane z myślą o Process Death w dowolnym momencie. Używaj SavedStateHandle w ViewModel i onSaveInstanceState do zapisywania stanu, który przetrwa zarówno App Freezer, jak i Process Death.
W iOS nie ma bezpośredniego API do sprawdzenia. Metoda pośrednia: sprawdź UserDefaults na obecność zapisanego stanu w didFinishLaunchingWithOptions. Jeśli stan istnieje — aplikacja została wyładowana z Suspended i uruchamia się na zimno. Jeśli nie ma stanu — czysty zimny start. W SwiftUI można zapisać flagę w scenePhase.background i sprawdzić ją przy następnym uruchomieniu.
Przy przejściu w Suspended iOS robi snapshot — zrzut ekranu bieżącego UI aplikacji. Ten zrzut jest pokazywany w App Switcher oraz przy powrocie do aplikacji (jako animacja „odmrożenia”). Jeśli aplikacja zawiera poufne dane, snapshot może je ujawnić. Do ochrony używaj UIApplication.shouldSnapshotSecureApp (iOS 16+) lub nakładaj blur-overlay w applicationDidEnterBackground.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również