Suspended — суспендовано стање животног циклуса iOS апликације, у којем је она замрзнута у меморији, али не извршава код. Показујемо како функционише Suspended, које ризике носи замрзавање апликације у позадини, како iOS управља истоваром Suspended апликација и како имплементирати state restoration за беспрекоран опоравак након повратка из Suspended-а.
Главно
Suspended — стање животног циклуса iOS апликације у којем се она налази у оперативној меморији уређаја, али не извршава никакав код. Ово је коначно стање пре потпуног завршетка: апликација прелази у Suspended из Background-а након завршетка свих позадинских задатака или након истека временског ограничења. У Suspended-у апликација је потпуно замрзнута — све нити су обустављене, тајмери не раде, мрежна активност не постоји.
Suspended — јединствена карактеристика iOS-а, која не постоји у стандардном животном циклусу Android-а. Разлог је различита архитектура управљања процесима. iOS чува слику апликације у меморији (аналогно хибернацији на десктопу) како би при повратку корисника тренутно обновио интерфејс без хладног старта. Android нема Suspended — процес или постоји и може да извршава код (Background), или је завршен (Not Running), мада Android може да обустави извршавање нити путем LMK-а.
За корисника Suspended изгледа као тренутни опоравак: он се пребацује између апликација преко App Switcher-а и свака апликација се отвара са истог места где ју је оставио. То ствара илузију да све апликације раде истовремено. У стварности већина њих је замрзнута у Suspended-у. Врући старт из Suspended-а је вишеструко бржи од хладног старта из Not Running-а, јер је код већ учитан у меморију.
iOS прати стање свих апликација и доноси одлуку о истовару Suspended апликација на основу доступне меморије. Када недостаје меморије, систем почиње да истоварује Suspended апликације, почевши од оних које су најдуже у овом стању. Ако меморије и даље недостаје, систем пребацује апликације из Background-а и Inactive-а у Suspended, а затим их истоварује. Овај процес је потпуно транспарентан за корисника — он само види икону апликације у App Switcher-у која при клику покреће хладни старт.
| Карактеристика | Suspended (iOS) | Background (iOS) | Background (Android) |
|---|---|---|---|
| Код се извршава | Не | Да (ограничено) | Да (ограничено) |
| У меморији | Да | Да | Да |
| Потрошња CPU-а | 0% | Ниска | Ниска |
| Врући старт | Да — тренутни опоравак | Да — преко Inactive-а | Не — процес је могао бити убијен |
| Временско ограничење | Не — може бити у меморији сатима | ~30 секунди (након beginBackgroundTask) | Зависи од верзије API-ја |
| Истовар од стране система | При недостатку меморије | При критичном недостатку меморије | LMK (Low Memory Killer) |
| Повратак на рад | Из App Switcher-а — тренутно | Из App Switcher-а — преко Inactive-а | Хладни старт |
| State Restoration | Пожељан | Није потребан | SavedStateHandle |
У iOS-у Suspended се достиже аутоматски након завршетка свих позадинских задатака. Систем позива applicationDidEnterBackground, даје време за извршавање beginBackgroundTask (око 30 секунди), након чега принудно обуставља све нити и пребацује апликацију у Suspended стање. Објекти у меморији су сачувани, али се ниједан код не извршава — апликација је замрзнута у тренутном стању.
Критично важан тренутак: applicationDidEnterBackground — последња метода која се гарантовано позива пре Suspended-а. Након тога апликација не прима никаква обавештења о истовару из меморије. Ако корисник или систем убије апликацију која се налази у Suspended-у, не позива се ни applicationWillTerminate, ни поново applicationDidEnterBackground. Зато се све чување података мора обавити у applicationDidEnterBackground, а не у applicationWillTerminate.
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-а.
У 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 је нормално понашање које треба увек очекивати.
// 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 заустављена — могуће App Freeze")
}
}Код приказује приступ обради аналога Suspended-а на Android-у. SavedStateHandle у ViewModel-у аутоматски чува и обнавља податке при Process Death-у. onStop — последњи гарантовани догађај пре могућег App Freezer-а или завршетка процеса. Стање форме за поруџбину, листа производа у корпи — сви ови подаци преживљавају замрзавање захваљујући SavedStateHandle-у. За тешке ресурсе (битмапе, курсоре базе) onStop је место за ослобађање меморије.
State Restoration — уграђени iOS механизам за чување и обнављање UI стања након истовара апликације из меморије. Ако је апликација била у Suspended-у и систем ју је истоварио, при следећем хладном старту state restoration обнавља навигациони стек, позицију скроловања, стање форми и друге UI елементе. Корисник се враћа на исти екран где се зауставио.
State Restoration ради кроз протоколе UIViewControllerRestoration и UIStateRestoring. Програмер додељује restorationIdentifier сваком ViewController-у и View-у које жели да обнови. При одласку у Background, iOS кодира стање ових објеката. Након повратка из истовара, iOS креира нове објекте и декодира сачувано стање. Без state restoration-а корисник ће видети празан екран након хладног старта уместо места где се зауставио.
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+ препоручује се коришћење secure encoding-а (NSSecureCoding) за заштиту података.
Прво правило — никада не рачунајте да ће се апликација вратити из 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-а видеће почетни екран апликације уместо места где се зауставио. То погоршава корисничко искуство и приморава корисника да понавља радње.
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-а на постојање сачуваног навигационог стека омогућава разликовање хладног старта након истовара од чистог хладног старта. У првом случају се обнавља навигациони стек, у другом — приказује се onboarding или главни екран. Овај приступ допуњује уграђени State Restoration за случајеве када NSCoder није довољан.
Често постављана питања
Неограничено — од неколико секунди до неколико дана. iOS нема временско ограничење за Suspended. Апликација ће висити у меморији док систем не одлучи да је истовари због недостатка ресурса. У пракси апликације остају у Suspended-у од 15 минута до неколико сати, у зависности од количине RAM меморије уређаја и броја активних апликација.
Не. applicationWillTerminate се не позива при истовару апликације из Suspended-а. Систем једноставно ослобађа меморију без обавештавања апликације. Ово је још један разлог зашто сва чувања података треба да се обаве у applicationDidEnterBackground. applicationWillTerminate се позива само када корисник ручно заврши апликацију превлачењем из App Switcher-а.
Директног аналога нема. На Android 11+ се појавио App Freezer, који обуставља позадинске процесе путем SIGSTOP-а — функционално је слично Suspended-у. Међутим, Android апликације треба пројектовати са очекивањем Process Death у било ком тренутку. Користите SavedStateHandle у ViewModel-у и onSaveInstanceState за чување стања које ће преживети и App Freezer и Process Death.
У iOS-у не постоји директан API за проверу. Индиректан метод: проверите UserDefaults на постојање сачуваног стања у didFinishLaunchingWithOptions. Ако стање постоји — апликација је истоварена из Suspended-а и стартује хладно. Ако стања нема — чист хладни старт. У SwiftUI-ју можете сачувати флаг у scenePhase.background и проверити га при следећем покретању.
При преласку у Suspended, iOS прави snapshot — снимак екрана тренутног UI апликације. Овај снимак се приказује у App Switcher-у и при повратку у апликацију (као анимација «одмрзавања»). Ако апликација садржи поверљиве податке, snapshot их може открити. За заштиту користите UIApplication.shouldSnapshotSecureApp (iOS 16+) или нанесите blur-overlay у applicationDidEnterBackground.
Резиме
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође