Not Running — lo stato iniziale del ciclo di vita di un’applicazione mobile quando non è ancora stata avviata o ha già terminato il suo lavoro. Scopri come iOS e Android gestiscono questo stato, quali eventi portano alla transizione da Not Running e come gestire correttamente l’avvio e la terminazione dell’applicazione in Swift e Kotlin.
Punti chiave
Not Running è lo stato di base del ciclo di vita di un’applicazione mobile in cui non è caricata nella RAM del dispositivo e non consuma risorse di sistema. In iOS e Android, questo stato significa la completa assenza di processi e thread associati all’applicazione. L’utente vede l’icona dell’app sulla schermata home, ma l’applicazione stessa non è attiva e non si trova nell’elenco delle app recenti.
Quando l’utente tocca l’icona dell’app, il sistema crea un nuovo processo, carica il codice eseguibile in memoria e inizializza tutte le strutture dati necessarie. Questo processo si chiama avvio a freddo (cold start) ed è il più dispendioso in termini di risorse per quanto riguarda il tempo di caricamento.
Il sistema può spostare l’applicazione in Not Running da qualsiasi altro stato. Se l’applicazione è in background o sospesa, il sistema operativo ha il diritto di scaricarla quando non c’è abbastanza RAM per attività con priorità maggiore — ad esempio, per un’applicazione attiva in primo piano.
Lo sviluppatore deve considerare che l’applicazione può essere terminata dal sistema in qualsiasi momento quando è in background. Ciò significa che tutti i dati non salvati possono essere persi. Pertanto, è di fondamentale importanza salvare lo stato in archivi chiave-valore (UserDefaults, SharedPreferences) o in un database locale durante le transizioni da Active a Background.
iOS utilizza priorità basate sullo stato corrente dell’applicazione: Active ha la priorità più alta, seguita da Inactive, Background, Suspended e infine Not Running — la priorità più bassa. Android utilizza una gerarchia di processi simile: il processo in primo piano ha priorità OOM_ADJ = 0, processo Visible = 100, processo Service = 200, processo Background = 300, processo Empty = 400. Più alto è il valore, maggiore è la probabilità che il processo venga terminato quando la memoria è scarsa.
| Piattaforma | Stato | Priorità di scarico | Descrizione |
|---|---|---|---|
| iOS | Not Running | Più alta | App non caricata — non consuma risorse di sistema |
| iOS | Suspended | Alta | App in memoria ma codice non eseguito — primo bersaglio per lo scarico |
| iOS | Background | Media | App in esecuzione di attività in background — scaricata dopo timeout |
| iOS | Active | Bassa | App attiva — scaricata solo sotto pressione critica di memoria |
| Android | Empty Process | Più alta | Processo senza componenti attivi — rimosso per primo |
| Android | Background Process | Alta | Processo in background senza Activity visibile |
| Android | Foreground Service | Bassa | Servizio con notifica — raramente terminato |
| Android | Foreground Process | Minima | Activity attiva — terminato per ultimo |
Avvio a freddo (cold start) si verifica quando l’applicazione passa direttamente da Not Running ad Active. Il sistema crea un nuovo processo, carica le classi, inizializza i campi statici, crea il thread principale e avvia il framework UI. Su iOS, ciò significa chiamare application(_:didFinishLaunchingWithOptions:), su Android — chiamare Application.onCreate() e Activity.onCreate(). Il tempo di avvio a freddo può variare da 200 ms a diversi secondi a seconda della complessità dell’applicazione.
Avvio a caldo (warm start o hot start) — l’applicazione era nello stato Suspended e riprende senza un ricaricamento completo. Il sistema ripristina l’ultimo stack UI dalla memoria e l’utente continua a lavorare dallo stesso punto. L’avvio a caldo è significativamente più veloce dell’avvio a freddo perché la maggior parte del codice è già caricata in memoria. Su iOS, un avvio a caldo non chiama application(_:didFinishLaunchingWithOptions:), solo applicationWillEnterForeground e applicationDidBecomeActive.
La differenza tra avvio a freddo e a caldo è critica per l’esperienza utente. Durante un avvio a freddo, lo sviluppatore deve garantire che l’avvio avvenga il più rapidamente possibile — inizializzazione pigra dei moduli, caricamento differito delle risorse pesanti, minimizzazione del lavoro nel thread principale all’avvio. Google raccomanda un avvio a freddo non superiore a 500 ms, Apple — non superiore a 400 ms per iOS.
// Misurazione del tempo di avvio a freddo in Android
class App : Application() {
private var startTime: Long = 0L
override fun onCreate() {
super.onCreate()
startTime = System.currentTimeMillis()
}
fun getStartupTime(): Long {
return System.currentTimeMillis() - startTime
}
}
// Avviare Activity con inizializzazione pigra
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by lazy {
ViewModelProvider(this).get(MainViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Solo il minimo necessario per il primo frame
setupNavigation()
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
// Inizializzazione pesante dopo il rendering
initializeHeavyModules()
}
}L’esempio mostra la misurazione del tempo di avvio a freddo in Android. Application.onCreate() viene chiamato quando si passa da Not Running ad Active. Il timestamp viene registrato all’avvio del processo. L’Activity utilizza l’inizializzazione pigra tramite un delegato lazy per evitare di bloccare il primo frame. onPostCreate è il luogo ottimale per inizializzare i moduli pesanti poiché l’UI è già stata renderizzata.
In iOS, Not Running viene gestito tramite il protocollo UIApplicationDelegate. Metodi chiave: application(_:didFinishLaunchingWithOptions:) viene chiamato dopo un avvio a freddo, applicationWillTerminate(_:) viene chiamato prima che l’utente termini l’applicazione. Tuttavia, il sistema può terminare l’applicazione senza chiamare applicationWillTerminate — ad esempio, durante una terminazione di emergenza o pressione di memoria. iOS non garantisce che questo metodo venga chiamato, quindi i dati devono essere salvati in applicationDidEnterBackground.
L’utente può terminare manualmente l’applicazione con uno swipe nell’App Switcher. Il sistema può scaricare l’applicazione dalla memoria mentre è in background. L’applicazione può crashare. In tutti i casi, tutti gli oggetti creati durante l’avvio vengono distrutti. Lo stato che non è stato salvato viene perso per sempre. In iOS 13+, si consiglia di utilizzare NSUserActivity o il meccanismo di ripristino dello stato tramite UIApplication.stateRestorationIdentifier per preservare lo stato.
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
// Avvio a freddo: applicazione passata da Not Running
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Inizializzazione del set minimo di servizi
setupAnalytics()
configureAppearance()
return true
}
// L’applicazione termina — solo chiusura manuale
func applicationWillTerminate(
_ application: UIApplication
) {
saveCriticalData()
}
// Salvare i dati prima di andare in background
func applicationDidEnterBackground(
_ application: UIApplication
) {
saveApplicationState()
}
private func saveCriticalData() {
UserDefaults.standard.synchronize()
}
private func saveApplicationState() {
let state = ["lastScreen": "main", "timestamp": Date()]
try? NSKeyedArchiver.archivedData(
withRootObject: state,
requiringSecureCoding: true
)
}
}Il codice mostra la corretta gestione di Not Running su iOS. applicationWillTerminate viene chiamato solo quando l’utente termina manualmente l’app. Il salvataggio dei dati critici viene duplicato in applicationDidEnterBackground poiché questo metodo è garantito per essere chiamato prima di andare in background. Il ripristino dello stato consente di salvare lo stack UI per un successivo recupero durante un avvio a freddo.
In Android, Not Running significa che il processo dell’applicazione non esiste. Il sistema Linux alla base di Android gestisce i processi attraverso il meccanismo Zygote. Quando un’applicazione viene avviata, Zygote biforca un nuovo processo, carica Dalvik/ART e chiama Application.onCreate(). Android non ha un equivalente diretto di applicationWillTerminate — il sistema può terminare il processo in qualsiasi momento senza preavviso.
Quando un’Activity viene chiamata per la prima volta, il sistema crea il processo, l’Application e l’Activity attraverso la catena onCreate → onStart → onResume. Se l’utente preme Indietro, l’Activity viene distrutta (onDestroy) e il processo può essere terminato dal sistema. Una differenza chiave rispetto a iOS: in Android, il processo può continuare a esistere anche senza Activities attive — ad esempio, se è in esecuzione un Foreground Service o c’è un BroadcastReceiver attivo.
// Gestione di Not Running tramite SavedStateHandle in ViewModel
class MainViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
companion object {
private const val KEY_LAST_SCREEN = "last_screen"
private const val KEY_USER_DATA = "user_data"
}
fun saveCurrentState(screen: String, data: String) {
savedStateHandle[KEY_LAST_SCREEN] = screen
savedStateHandle[KEY_USER_DATA] = data
}
fun restoreState(): AppState? {
val screen = savedStateHandle.get<String>(KEY_LAST_SCREEN)
val data = savedStateHandle.get<String>(KEY_USER_DATA)
return if (screen != null && data != null) {
AppState(screen, data)
} else null
}
}
// Application — il primo callback dopo Not Running
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
initCrashReporter()
initDependencyInjection()
}
}SavedStateHandle è un componente di Android Architecture Components che salva automaticamente lo stato durante una transizione a Not Running e lo ripristina in un avvio a freddo. Un ViewModel creato tramite ViewModelProvider sopravvive alla rotazione dello schermo e alla distruzione dell’Activity. Quando il processo termina, i dati da SavedStateHandle vengono serializzati in un Bundle e salvati nello stato dell’istanza salvata.
Not Running si verifica per diversi motivi. L’utente chiude manualmente l’applicazione. Il sistema scarica l’applicazione per mancanza di memoria. L’applicazione crasha con un’eccezione. Su Android, il sistema può terminare il processo durante un aggiornamento massivo delle app o un riavvio del dispositivo. iOS può terminare l’applicazione quando un’attività in background va in timeout (di solito 30 secondi).
| Motivo | iOS | Android | Può essere prevenuto |
|---|---|---|---|
| Chiusura manuale dall’utente | Swipe in App Switcher | Swipe da Recents | No — azione dell’utente |
| Mancanza di memoria | Attivazione avviso di memoria | onTrimMemory / LMK | Parzialmente — ottimizzazione memoria |
| Crash dell’applicazione | NSException / segnale | UncaughtException / ANR | Sì — gestione errori e segnalazione crash |
| Timeout attività in background | 30 sec per attività in background | 10 min per JobScheduler | Sì — pianificazione corretta delle attività |
| Riavvio del sistema operativo | applicationWillTerminate chiamato | Broadcast ACTION_SHUTDOWN | No — evento di sistema |
| Aggiornamento dell’app | Non si verifica (iOS Sandbox) | Processo terminato all’aggiornamento APK | No — aggiornamento di sistema |
Per iOS, utilizza la registrazione console in applicationWillTerminate e applicationDidFinishLaunching. Aggiungi un flag in UserDefaults ad ogni avvio — se il flag manca al prossimo avvio, l’applicazione è stata terminata in modo improprio. Su Android, usa ActivityManager.isBackgroundRestricted() per verificare se l’applicazione può eseguire attività in background. Inoltre, monitora onTrimMemory(TRIM_MEMORY_COMPLETE) — questo è un segnale che il processo verrà terminato.
Prima regola — non dare mai per scontato che applicationWillTerminate o onDestroy vengano chiamati. Salva i dati di importanza critica ad ogni transizione da Active a Background. Utilizza archivi chiave-valore per le impostazioni semplici e SQLite/Room per i dati strutturati.
Seconda regola — misura il tempo di avvio a freddo e ottimizzalo. Inizializzazione pigra, minimizzazione del lavoro nel thread principale, precaricamento delle risorse, utilizzo dell’API SplashScreen — tutto ciò migliora la percezione del tempo di avvio. Google raccomanda un avvio a freddo inferiore a 200 ms per un’eccellente esperienza utente.
Terza regola — implementa il ripristino dello stato. Su iOS, usa UIApplication.stateRestorationIdentifier e NSUserActivity. Su Android, usa SavedStateHandle in ViewModel combinato con onSaveInstanceState. Ciò consentirà all’utente di continuare a lavorare dallo stesso punto dopo un riavvio dell’app.
Quarta regola — gestisci launchOptions e Intent con cui l’applicazione è stata avviata dopo Not Running. Deep link, notifiche push, link universali — tutti vengono passati attraverso i parametri di avvio. Lo sviluppatore deve estrarre correttamente questi dati e indirizzare l’utente alla schermata appropriata.
// Gestione del deep link dopo l’avvio a freddo
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Verificare se è arrivata una notifica
if let notification = launchOptions?[.remoteNotification] as? [String: Any] {
handleNotification(notification)
}
// Verificare il deep link
if let url = launchOptions?[.url] as? URL {
handleDeepLink(url)
}
return true
}
private func handleDeepLink(_ url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let screenId = components.queryItems?.first(where: { $0.name == "screen" })?.value
else { return }
openScreen(screenId)
}Il codice mostra la gestione dei parametri di avvio in un avvio a freddo iOS. launchOptions contiene i dati con cui il sistema ha avviato l’applicazione. Le notifiche, i deep link e i link universali vengono passati attraverso questo dizionario. Lo sviluppatore deve gestire correttamente tutti i possibili scenari di avvio per garantire un’esperienza utente fluida.
Domande frequenti
I dati salvati nell’archivio persistente (UserDefaults, Core Data, SharedPreferences, Room) vengono conservati. I dati nella RAM — variabili, cache, stato del ViewModel senza SavedStateHandle — vengono persi irrevocabilmente. Pertanto, è di fondamentale importanza salvare lo stato dell’applicazione ad ogni transizione in Background.
Durante un avvio a freddo, viene chiamato application(_:didFinishLaunchingWithOptions:). Durante un avvio a caldo (ritorno da Suspended), questo metodo non viene chiamato — vengono attivati solo applicationWillEnterForeground e applicationDidBecomeActive. Se devi eseguire un’azione solo in un avvio a freddo, imposta un flag in didFinishLaunchingWithOptions.
Sì. Un Foreground Service con una notifica persistente impedisce al sistema di terminare il processo, anche se tutte le Activities vengono distrutte. Un Background Service (startService senza foreground) può essere interrotto dal sistema in qualsiasi momento. Un Service in esecuzione significa che il processo esiste, e questo non è più Not Running.
Sul simulatore iOS, termina l’app tramite App Switcher (Cmd+Shift+H due volte, swipe verso l’alto). Sull’emulatore Android, usa adb shell am force-stop com.example.app o il pulsante Stop in Logcat. Dopodiché, avvia di nuovo l’applicazione — sarà un avvio a freddo pulito da Not Running.
Kill-switch è un comando server per la terminazione di emergenza dell’applicazione. Viene utilizzato in applicazioni bancarie e aziendali per il blocco dell’accesso remoto. Se l’applicazione riceve un comando kill, al successivo avvio a freddo blocca l’interfaccia utente e richiede la riautenticazione. Su iOS, un kill-switch viene implementato tramite notifiche remote con un flag di blocco.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche