Trucchi per le prestazioni di iOS per rendere la tua app più performante
Pubblicato: 2022-03-10Sebbene l'hardware iOS moderno sia abbastanza potente da gestire molte attività intensive e complesse, il dispositivo potrebbe comunque non rispondere se non si presta attenzione alle prestazioni dell'app. In questo articolo, esamineremo cinque trucchi di ottimizzazione che renderanno la tua app più reattiva.
1. Elimina dalla coda la cella riutilizzabile
Probabilmente hai già usato tableView.dequeueReusableCell(withIdentifier:for:)
all'interno di tableView(_:cellForRowAt:)
. Ti sei mai chiesto perché devi seguire questa API imbarazzante, invece di passare semplicemente una matrice di celle? Esaminiamo il ragionamento di questo.
Supponiamo di avere una vista tabella con mille righe. Senza utilizzare celle riutilizzabili, dovremmo creare una nuova cella per ogni riga, in questo modo:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // Create a new cell whenever cellForRowAt is called. let cell = UITableViewCell() cell.textLabel?.text = "Cell \(indexPath.row)" return cell }
Come potresti aver pensato, questo aggiungerà mille celle alla memoria del dispositivo mentre scorri verso il basso. Immagina cosa accadrebbe se ogni cella contenesse un UIImageView
e molto testo: caricarli tutti in una volta potrebbe causare l'esaurimento della memoria dell'app! A parte questo, ogni singola cella richiederebbe l'allocazione di nuova memoria durante lo scorrimento. Se scorri rapidamente una visualizzazione tabella, molti piccoli blocchi di memoria verranno allocati al volo e questo processo renderà l'interfaccia utente janky!
Per risolvere questo problema, Apple ci ha fornito il dequeueReusableCell(withIdentifier:for:)
. Il riutilizzo delle celle funziona inserendo la cella che non è più visibile sullo schermo in una coda e quando una nuova cella sta per essere visibile sullo schermo (ad esempio, la cella successiva in basso mentre l'utente scorre verso il basso), la vista tabella sarà recuperare una cella da questa coda e modificarla nel metodo cellForRowAt indexPath:

Utilizzando una coda per archiviare le celle, la visualizzazione tabella non deve creare mille celle. Invece, ha bisogno di un numero sufficiente di celle per coprire l'area della visualizzazione tabella.
Usando dequeueReusableCell
, possiamo ridurre la memoria utilizzata dall'app e renderla meno incline a esaurire la memoria!
2. Utilizzo di una schermata di avvio che assomigli alla schermata iniziale
Come menzionato nelle Linee guida per l'interfaccia umana (HIG) di Apple, le schermate di avvio possono essere utilizzate per migliorare la percezione della reattività di un'app:
“Ha il solo scopo di migliorare la percezione della tua app come veloce da avviare e immediatamente pronta per l'uso. Ogni app deve fornire una schermata di avvio".
È un errore comune utilizzare una schermata di avvio come schermata iniziale per mostrare il marchio o per aggiungere un'animazione di caricamento. Progetta la schermata di avvio in modo che sia identica alla prima schermata della tua app, come menzionato da Apple:
“Progetta una schermata di avvio quasi identica alla prima schermata della tua app. Se includi elementi che hanno un aspetto diverso al termine dell'avvio dell'app, le persone possono sperimentare uno spiacevole flash tra la schermata di avvio e la prima schermata dell'app.
“La schermata di avvio non è un'opportunità di branding. Non progettare un'esperienza di ingresso che assomigli a una schermata iniziale o a una finestra "Informazioni". Non includere loghi o altri elementi di branding a meno che non siano una parte statica della prima schermata della tua app".
L'utilizzo di una schermata di avvio per scopi di caricamento o di branding potrebbe rallentare il tempo del primo utilizzo e far sentire all'utente che l'app è lenta.
Quando avvii un nuovo progetto iOS, verrà creato un LaunchScreen.storyboard
vuoto. Questa schermata verrà mostrata all'utente mentre l'app carica i controller di visualizzazione e il layout.
Per rendere la tua app più veloce, puoi progettare la schermata di avvio in modo che sia simile alla prima schermata (controller di visualizzazione) che verrà mostrata all'utente.
Ad esempio, la schermata di avvio dell'app Safari è simile alla sua prima visualizzazione:

Lo storyboard della schermata di avvio è come qualsiasi altro file di storyboard, tranne per il fatto che puoi usare solo le classi UIKit standard, come UIViewController, UITabBarController e UINavigationController. Se tenti di utilizzare qualsiasi altra sottoclasse personalizzata (come UserViewController), Xcode ti avviserà che l'uso di nomi di classe personalizzati è proibito.

Un'altra cosa da notare è che UIActivityIndicatorView
non si anima quando viene posizionato nella schermata di avvio, perché iOS genererà un'immagine statica dallo storyboard della schermata di avvio e la visualizzerà all'utente. (Questo è menzionato brevemente nella presentazione del WWDC 2014 "Platforms State of the Union", intorno alle 01:21:56
.)
HIG di Apple ci consiglia inoltre di non includere il testo nella nostra schermata di avvio, perché la schermata di avvio è statica e non è possibile localizzare il testo per soddisfare lingue diverse.
Letture consigliate : App mobile con funzione di riconoscimento facciale: come renderla reale
3. Ripristino dello stato per i controller di visualizzazione
La conservazione e il ripristino dello stato consentono all'utente di tornare allo stesso identico stato dell'interfaccia utente appena prima di lasciare l'app. A volte, a causa di memoria insufficiente, il sistema operativo potrebbe dover rimuovere l'app dalla memoria mentre l'app è in background e l'app potrebbe perdere traccia del suo ultimo stato dell'interfaccia utente se non viene preservato, causando la perdita del lavoro degli utenti in corso!
Nella schermata multitasking, possiamo vedere un elenco di app che sono state messe in background. Potremmo presumere che queste app siano ancora in esecuzione in background; in realtà, alcune di queste app potrebbero essere uccise e riavviate dal sistema a causa delle richieste di memoria. Le istantanee dell'app che vediamo nella visualizzazione multitasking sono in realtà screenshot presi dal sistema da subito quando siamo usciti dall'app (cioè per andare alla schermata iniziale o multitasking).

iOS utilizza questi screenshot per dare l'illusione che l'app sia ancora in esecuzione o stia ancora visualizzando questa visualizzazione particolare, mentre l'app potrebbe essere già stata terminata o riavviata in background mentre continuava a visualizzare lo stesso screenshot.
Hai mai sperimentato, riprendendo un'app dalla schermata multitasking, che l'app mostra un'interfaccia utente diversa dall'istantanea mostrata nella vista multitasking? Ciò è dovuto al fatto che l'app non ha implementato il meccanismo di ripristino dello stato e i dati visualizzati sono andati persi quando l'app è stata interrotta in background. Questo può portare a una brutta esperienza perché l'utente si aspetta che la tua app sia nello stesso stato di quando l'ha lasciata.
Dall'articolo di Apple:
"Si aspettano che la tua app sia nello stesso stato di quando l'hanno lasciata. La conservazione e il ripristino dello stato assicurano che la tua app torni allo stato precedente quando viene avviata di nuovo".
UIKit fa molto lavoro per semplificare la conservazione e il ripristino dello stato per noi: gestisce automaticamente il salvataggio e il caricamento dello stato di un'app nei momenti appropriati. Tutto ciò che dobbiamo fare è aggiungere una configurazione per indicare all'app di supportare la conservazione e il ripristino dello stato e per indicare all'app quali dati devono essere conservati.
Per abilitare il salvataggio e il ripristino dello stato, possiamo implementare questi due metodi in AppDelegate.swift
:
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool { return true }
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool { return true }
Questo dirà all'app di salvare e ripristinare automaticamente lo stato dell'applicazione.
Successivamente, diremo all'app quali controller di visualizzazione devono essere conservati. Lo facciamo specificando il "Restoration ID" nello storyboard:

Puoi anche selezionare "Usa ID storyboard" per utilizzare l'ID storyboard come ID di ripristino.
Per impostare l'ID di ripristino nel codice, possiamo utilizzare la proprietà restorationIdentifier
del controller di visualizzazione.
// ViewController.swift self.restorationIdentifier = "MainVC"
Durante la conservazione dello stato, qualsiasi controller di visualizzazione o visualizzazione a cui è stato assegnato un identificatore di ripristino avrà il proprio stato salvato su disco.
Gli identificatori di restauro possono essere raggruppati per formare un percorso di restauro. Gli identificatori sono raggruppati utilizzando la gerarchia di visualizzazione, dal controller di visualizzazione radice al controller di visualizzazione attivo corrente. Si supponga che un MyViewController sia incorporato in un controller di navigazione, che è incorporato in un altro controller della barra delle schede. Supponendo che stiano usando i propri nomi di classe come identificatori di ripristino, il percorso di ripristino sarà simile al seguente:
TabBarController/NavigationController/MyViewController
Quando l'utente lascia l'app con MyViewController come controller di visualizzazione attivo, questo percorso verrà salvato dall'app; quindi l'app ricorderà la gerarchia di visualizzazione precedente mostrata ( Controller della barra delle schede → Controller di navigazione → Controller di visualizzazione personale ).
Dopo aver assegnato l'identificatore di ripristino, sarà necessario implementare i metodi encodeRestorableState(with coder:) e decodeRestorableState(with coder:) per ciascuno dei controller di visualizzazione conservati. Questi due metodi ci consentono di specificare quali dati devono essere salvati o caricati e come codificarli o decodificarli.

Vediamo il controller di visualizzazione:
// MyViewController.swift // MARK: State restoration // UIViewController already conforms to UIStateRestoring protocol by default extension MyViewController { // will be called during state preservation override func encodeRestorableState(with coder: NSCoder) { // encode the data you want to save during state preservation coder.encode(self.username, forKey: "username") super.encodeRestorableState(with: coder) } // will be called during state restoration override func decodeRestorableState(with coder: NSCoder) { // decode the data saved and load it during state restoration if let restoredUsername = coder.decodeObject(forKey: "username") as? String { self.username = restoredUsername } super.decodeRestorableState(with: coder) } }
Ricorda di chiamare l'implementazione della superclasse in fondo al tuo metodo. Ciò garantisce che la classe padre abbia la possibilità di salvare e ripristinare lo stato.
Al termine della decodifica degli oggetti, verrà chiamato applicationFinishedRestoringState()
per comunicare al controller di visualizzazione che lo stato è stato ripristinato. Possiamo aggiornare l'interfaccia utente per il controller di visualizzazione in questo metodo.
// MyViewController.swift // MARK: State restoration // UIViewController already conforms to UIStateRestoring protocol by default extension MyViewController { ... override func applicationFinishedRestoringState() { // update the UI here self.usernameLabel.text = self.username } }
Ecco qua! Questi sono i metodi essenziali per implementare la conservazione e il ripristino dello stato per la tua app. Tieni presente che il sistema operativo rimuoverà lo stato salvato quando l'app viene chiusa forzatamente dall'utente, al fine di evitare di rimanere bloccata in uno stato interrotto nel caso in cui qualcosa vada storto nella conservazione e ripristino dello stato.
Inoltre, non archiviare nello stato i dati del modello (ovvero i dati che avrebbero dovuto essere salvati in UserDefaults o Core Data), anche se potrebbe sembrare conveniente farlo. I dati di stato verranno rimossi quando la forza dell'utente esce dall'app e sicuramente non vuoi perdere i dati del modello in questo modo.
Per verificare se la conservazione e il restauro dello stato funzionano correttamente, attenersi alla seguente procedura:
- Crea e avvia un'app usando Xcode.
- Passare alla schermata con lo stato di conservazione e restauro che si desidera testare.
- Torna alla schermata principale (scorrendo verso l'alto o facendo doppio clic sul pulsante Home, oppure premendo Maiusc ⇧ + Cmd ⌘ + H nel simulatore) per inviare l'app in background.
- Arresta l'app in Xcode premendo il pulsante.
- Avvia nuovamente l'app e verifica se lo stato è stato ripristinato correttamente.
Poiché questa sezione copre solo le basi della conservazione e del restauro dello stato, consiglio i seguenti articoli di Apple Inc. per una conoscenza più approfondita del restauro dello stato:
- Conservazione e ripristino dello stato
- Processo di conservazione dell'interfaccia utente
- Processo di ripristino dell'interfaccia utente
4. Ridurre il più possibile l'utilizzo di viste non opache
Una vista opaca è una vista priva di trasparenza, il che significa che qualsiasi elemento dell'interfaccia utente posizionato dietro non è affatto visibile. Possiamo impostare una vista in modo che sia opaca in Interface Builder:

Oppure possiamo farlo a livello di codice con la proprietà isOpaque
di UIView:
view.isOpaque = true
L'impostazione di una vista su opaca farà sì che il sistema di disegno ottimizzi alcune prestazioni di disegno durante il rendering dello schermo.
Se una vista ha trasparenza (cioè l'alfa è inferiore a 1.0), iOS dovrà fare un lavoro extra per calcolare ciò che dovrebbe essere visualizzato fondendo diversi livelli di viste nella gerarchia delle viste. D'altra parte, se una vista è impostata su opaca, il sistema di disegno metterà semplicemente questa vista in primo piano ed eviterà il lavoro extra di fondere i livelli multipli della vista dietro di essa.
Puoi controllare quali livelli vengono miscelati (non opachi) nel simulatore iOS selezionando Debug → Livelli sfumati di colore .

Dopo aver controllato l'opzione Livelli sfumati di colore , puoi vedere che alcune viste sono rosse e altre verdi. Il rosso indica che la vista non è opaca e che la visualizzazione dell'output è il risultato di livelli fusi dietro di essa. Il verde indica che la vista è opaca e non è stata eseguita alcuna fusione.

Le etichette mostrate sopra ("Visualizza amici", ecc.) sono evidenziate in rosso perché quando un'etichetta viene trascinata nello storyboard, il suo colore di sfondo è impostato su trasparente per impostazione predefinita. Quando il sistema di disegno sta componendo il display vicino all'area dell'etichetta, chiederà il livello dietro l'etichetta ed eseguirà dei calcoli.
Un modo per ottimizzare le prestazioni dell'app è ridurre il più possibile il numero di visualizzazioni evidenziate in rosso.
Modificando label.backgroundColor = UIColor.clear
in label.backgroundColor = UIColor.white
, possiamo ridurre la fusione dei livelli tra l'etichetta e il livello di visualizzazione dietro di essa.

Potresti aver notato che, anche se hai impostato un UIImageView su opaco e gli hai assegnato un colore di sfondo, il simulatore mostrerà comunque il rosso nella visualizzazione dell'immagine. Ciò è probabilmente dovuto al fatto che l'immagine utilizzata per la visualizzazione dell'immagine ha un canale alfa.
Per rimuovere il canale alfa per un'immagine, puoi utilizzare l'app Anteprima per creare un duplicato dell'immagine ( Maiusc ⇧ + Cmd ⌘ + S ) e deselezionare la casella di controllo "Alfa" durante il salvataggio.

5. Passa le funzioni di elaborazione pesanti ai thread in background (GCD)
Poiché UIKit funziona solo sul thread principale, l'esecuzione di un'elaborazione pesante sul thread principale rallenterà l'interfaccia utente. Il thread principale viene utilizzato da UIKit non solo per gestire e rispondere all'input dell'utente, ma anche per disegnare lo schermo.
La chiave per rendere reattiva un'app è spostare il maggior numero possibile di attività di elaborazione pesanti sui thread in background. Evitare di eseguire calcoli complessi, reti e operazioni di I/O pesanti (ad es. lettura e scrittura su disco) sul thread principale.
Potresti aver usato una volta un'app che all'improvviso non rispondeva al tuo input tattile e sembra che l'app si sia bloccata. Ciò è probabilmente causato dall'app che esegue attività di calcolo pesanti sul thread principale.
Il thread principale di solito alterna le attività UIKit (come la gestione dell'input dell'utente) e alcune attività leggere a piccoli intervalli. Se un'attività pesante è in esecuzione sul thread principale, UIKit dovrà attendere fino al termine dell'attività pesante prima di poter gestire l'input tattile.

Per impostazione predefinita, il codice all'interno dei metodi del ciclo di vita del controller di visualizzazione (come viewDidLoad) e le funzioni IBOutlet vengono eseguite sul thread principale. Per spostare le attività di elaborazione pesanti su un thread in background, possiamo utilizzare le code Grand Central Dispatch fornite da Apple.
Ecco il modello per cambiare le code:
// Switch to background thread to perform heavy task. DispatchQueue.global(qos: .default).async { // Perform heavy task here. // Switch back to main thread to perform UI-related task. DispatchQueue.main.async { // Update UI. } }
Il qos
sta per "qualità del servizio". Diversi valori di qualità del servizio indicano priorità diverse per le attività specificate. Il sistema operativo assegnerà più tempo della CPU e più potenza della CPU I/O throughput per le attività allocate in code con valori di QoS più elevati, il che significa che un'attività finirà più velocemente in una coda con valori di QoS più elevati. Un valore QoS più elevato consumerà anche più energia a causa dell'utilizzo di più risorse.
Di seguito è riportato l'elenco dei valori QoS dalla priorità più alta a quella più bassa:

Apple ha fornito una pratica tabella con esempi dei valori QoS da utilizzare per diverse attività.
Una cosa da tenere a mente è che tutto il codice UIKit dovrebbe essere sempre eseguito sul thread principale. La modifica di oggetti UIKit (come UILabel
e UIImageView
) sul thread in background potrebbe avere una conseguenza indesiderata, ad esempio l'interfaccia utente non si aggiorna effettivamente, si verifica un arresto anomalo e così via.
Dall'articolo di Apple:
"L'aggiornamento dell'interfaccia utente su un thread diverso dal thread principale è un errore comune che può causare aggiornamenti dell'interfaccia utente mancati, difetti visivi, danneggiamenti dei dati e arresti anomali".
Consiglio di guardare il video WWDC 2012 di Apple sulla concorrenza dell'interfaccia utente per capire meglio come creare un'app reattiva.
Appunti
Il compromesso dell'ottimizzazione delle prestazioni è che devi scrivere più codice o configurare impostazioni aggiuntive oltre alle funzionalità dell'app. Ciò potrebbe far sì che la tua app venga consegnata più tardi del previsto e avrai più codice da mantenere in futuro e più codice significa potenzialmente più bug.
Prima di dedicare tempo all'ottimizzazione della tua app, chiediti se l'app è già fluida o se ha qualche parte che non risponde che deve davvero essere ottimizzata. Trascorrere molto tempo a ottimizzare un'app già fluida per ridurre 0,01 secondi potrebbe non valerne la pena, poiché il tempo potrebbe essere speso meglio per sviluppare funzionalità migliori o altre priorità.
Ulteriori risorse
- "Una suite di deliziosi iOS Eye Candy", Tim Oliver, Tokyo iOS Meetup 2018 (video)
- "Costruzione di interfacce utente simultanee su iOS", Andy Matuschak, WWDC 2012 (video)
- "Preservare l'interfaccia utente della tua app durante i lanci", Apple
- "Guida alla programmazione simultanea: code di spedizione", Archivio documentazione, Apple
- "Controllo filo principale", Apple