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 stopped — possible App Freeze")
}
}В коде показан подход к обработке аналога Suspended на Android. SavedStateHandle в ViewModel автоматически сохраняет и восстанавливает данные при Process Death. onStop — последнее гарантированное событие перед возможным App Freezer или завершением процесса. Состояние checkout-формы, список товаров в корзине — все эти данные переживают заморозку благодаря 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 на наличие сохранённого навигационного стека позволяет отличить холодный старт после выгрузки от чистого холодного старта. В первом случае восстанавливается навигационный стек, во втором — показывается онбординг или главный экран. Такой подход дополняет встроенный State Restoration для случаев, когда NSCoder недостаточно.
Часто задаваемые вопросы
Неограниченно — от нескольких секунд до нескольких дней. iOS не имеет таймаута для Suspended. Приложение будет висеть в памяти, пока система не решит его выгрузить из-за нехватки ресурсов. На практике приложения остаются в Suspended от 15 минут до нескольких часов, в зависимости от объёма оперативной памяти устройства и количества активных приложений.
Нет. 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-оверлей в applicationDidEnterBackground.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также