Suspended — спряно състояние на жизнения цикъл на iOS приложението, в което то е замразено в паметта, но не изпълнява код. Показваме как работи Suspended, какви рискове носи замразяването на приложението на фон, как iOS управлява разтоварването на Suspended приложения и как да имплементираме state restoration за безпроблемно възстановяване след връщане от Suspended.
Основни моменти
Suspended — състояние на жизнения цикъл на iOS приложението, в което то се намира в оперативната памет на устройството, но не изпълнява никакъв код. Това е крайното състояние преди пълно приключване: приложението преминава в Suspended от Background след завършване на всички фонови задачи или след изтичане на времето. В Suspended приложението е напълно замразено — всички нишки са спряни, таймерите не работят, мрежова активност няма.
Suspended — уникална характеристика на iOS, която не съществува в стандартния жизнен цикъл на Android. Причината е различната архитектура на управление на процесите. iOS запазва образа на приложението в паметта (аналог на хибернация на десктоп), за да при връщане на потребителя незабавно възстанови интерфейса без cold start. Android няма Suspended — процесът или съществува и може да изпълнява код (Background), или е приключен (Not Running), въпреки че Android може да спре изпълнението на нишките чрез LMK.
За потребителя Suspended изглежда като незабавно възстановяване: той превключва между приложения чрез App Switcher и всяко приложение се отваря от същото място, където го е оставил. Това създава илюзията, че всички приложения работят едновременно. В действителност повечето от тях са замразени в Suspended. Горещ старт от Suspended е многократно по-бърз от cold start от Not Running, тъй като кодът вече е зареден в паметта.
iOS проследява състоянието на всички приложения и взема решение за разтоварване на Suspended приложения на базата на наличната памет. Когато паметта не достига, системата започва да разтоварва Suspended приложения, започвайки от тези, които са най-дълго в това състояние. Ако паметта все още е недостатъчна, системата премества приложенията от Background и Inactive в Suspended и след това ги разтоварва. Този процес е напълно прозрачен за потребителя — той просто вижда иконата на приложението в App Switcher, която при щракване стартира cold start.
| Характеристика | Suspended (iOS) | Background (iOS) | Background (Android) |
|---|---|---|---|
| Код се изпълнява | Не | Да (ограничено) | Да (ограничено) |
| В паметта | Да | Да | Да |
| Консумация на CPU | 0% | Ниска | Ниска |
| Горещ старт | Да — незабавно възстановяване | Да — чрез Inactive | Не — процесът може да е бил убит |
| Времеви лимит | Не — може да е в паметта часове | ~30 секунди (след beginBackgroundTask) | Зависи от версията на API |
| Разтоварване от системата | При липса на памет | При критична липса на памет | LMK (Low Memory Killer) |
| Връщане към работа | От App Switcher — незабавно | От App Switcher — чрез Inactive | Cold start |
| 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 {
// Ако това е cold start след разтоварване от 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 — само при cold start, когато приложението е било разтоварено от паметта след 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 и системата го е разтоварила, при следващия cold start state restoration възстановява навигационния стек, позицията на превъртане, състоянието на формите и други UI елементи. Потребителят се връща на същия екран, където е спрял.
State Restoration работи чрез протоколите UIViewControllerRestoration и UIStateRestoring. Разработчикът присвоява restorationIdentifier на всеки ViewController и View, които иска да възстанови. При преминаване в Background iOS кодира състоянието на тези обекти. След връщане от разтоварване iOS създава нови обекти и декодира запазеното състояние. Без state restoration потребителят ще види празен екран след cold start вместо мястото, където е спрял.
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 — подходящи хранилища. Паметта (променливи, свойства) — ненадeждно хранилище за данни, които трябва да оцелеят 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 {
// Чист cold start от 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 за наличие на запазен навигационен стек позволява да се разграничи cold start след разтоварване от чист cold start. В първия случай навигационният стек се възстановява, във втория — се показва 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 и стартира студено. Ако няма състояние — чист cold start. В SwiftUI можете да запазите флаг в scenePhase.background и да го проверите при следващото стартиране.
При преминаване в Suspended iOS прави snapshot — снимка на текущия UI на приложението. Тази снимка се показва в App Switcher и при връщане в приложението (като анимация на размразяване). Ако приложението съдържа поверителни данни, snapshot може да ги разкрие. За защита използвайте UIApplication.shouldSnapshotSecureApp (iOS 16+) или приложете blur-overlay в applicationDidEnterBackground.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също