Suspended — ανασταλμένη κατάσταση του κύκλου ζωής της εφαρμογής iOS, στην οποία είναι παγωμένη στη μνήμη αλλά δεν εκτελεί κώδικα. Δείχνουμε πώς λειτουργεί το Suspended, ποιους κινδύνους περιέχει το πάγωμα της εφαρμογής στο παρασκήνιο, πώς το iOS διαχειρίζεται την αποφόρτωση των εφαρμογών Suspended και πώς να υλοποιήσουμε state restoration για απρόσκοπτη αποκατάσταση μετά από επιστροφή από Suspended.
Κύρια σημεία
Suspended — κατάσταση του κύκλου ζωής της εφαρμογής iOS στην οποία βρίσκεται στην λειτουργική μνήμη της συσκευής αλλά δεν εκτελεί κανένα κώδικα. Αυτή είναι η τελική κατάσταση πριν από την πλήρη λήξη: η εφαρμογή μεταβαίνει σε Suspended από Background μετά τον τερματισμό όλων των εργασιών παρασκηνίου ή μετά από λήξη χρόνου αναμονής. Σε Suspended η εφαρμογή είναι πλήρως παγωμένη — όλα τα threads είναι ανασταλμένα, οι χρονομετρητές δεν λειτουργούν, δεν υπάρχει δραστηριότητα δικτύου.
Suspended — μοναδικό χαρακτηριστικό του iOS, που δεν υπάρχει στον τυπικό κύκλο ζωής του Android. Ο λόγος είναι η διαφορετική αρχιτεκτονική διαχείρισης διεργασιών. Το iOS διατηρεί την εικόνα της εφαρμογής στη μνήμη (ανάλογο με την αδρανοποίηση σε desktop) ώστε κατά την επιστροφή του χρήστη να αποκαταστήσει αμέσως τη διεπαφή χωρίς cold start. Το Android δεν έχει Suspended — η διεργασία είτε υπάρχει και μπορεί να εκτελέσει κώδικα (Background), είτε έχει τερματιστεί (Not Running), αν και το Android μπορεί να αναστείλει την εκτέλεση των threads μέσω 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 δευτερόλεπτα), μετά από το οποίο αναστέλλει βιαστικά όλα τα threads και μεταφέρει την εφαρμογή σε κατάσταση 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, αναστέλλοντας όλα τα threads. Κατά την επιστροφή της εφαρμογής στο προσκήνιο, στέλνεται 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. Για βαριούς πόρους (bitmap, δρομείς βάσης δεδομένων) το 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 — κατάλληλες αποθηκεύσεις. Η μνήμη (μεταβλητές, ιδιότητες) — αναξιόπιστη αποθήκευση για δεδομένα που πρέπει να επιζήσουν του Suspended.
Δεύτερος κανόνας — απελευθερώνετε πόρους πριν από Suspended. Κλείνετε περιγραφείς αρχείων, απελευθερώνετε μνήμη GPU (Metal, Core Graphics), κλείνετε συνδέσεις δικτύου. Παρόλο που η εφαρμογή δεν καταναλώνει CPU σε Suspended, οι κατειλημμένοι πόροι είναι μπλοκαρισμένοι για άλλες εφαρμογές. Στο iOS σε Suspended δεν μπορούν να διατηρούνται ανοιχτά sockets — κατά την επιστροφή από 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 μπορείτε να αποθηκεύσετε ένα flag στο scenePhase.background και να το ελέγξετε στην επόμενη εκκίνηση.
Κατά τη μετάβαση σε Suspended, το iOS λαμβάνει ένα snapshot — στιγμιότυπο οθόνης του τρέχοντος UI της εφαρμογής. Αυτό το στιγμιότυπο εμφανίζεται στο App Switcher και κατά την επιστροφή στην εφαρμογή (ως κινούμενη εικόνα απόψυξης). Αν η εφαρμογή περιέχει εμπιστευτικά δεδομένα, το snapshot μπορεί να τα αποκαλύψει. Για προστασία χρησιμοποιήστε UIApplication.shouldSnapshotSecureApp (iOS 16+) ή εφαρμόστε blur-overlay στο applicationDidEnterBackground.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης