Trucuri de performanță iOS pentru a face aplicația să se simtă mai performantă
Publicat: 2022-03-10Deși hardware-ul iOS modern este suficient de puternic pentru a gestiona multe sarcini intensive și complexe, dispozitivul ar putea să nu mai răspundă dacă nu ești atent la modul în care funcționează aplicația ta. În acest articol, vom analiza cinci trucuri de optimizare care vor face aplicația dvs. să se simtă mai receptivă.
1. Scoateți din coadă celula reutilizabilă
Probabil ați folosit tableView.dequeueReusableCell(withIdentifier:for:)
în interiorul tableView(_:cellForRowAt:)
înainte. V-ați întrebat vreodată de ce trebuie să urmați acest API incomodă, în loc să treceți doar o serie de celule? Să trecem prin raționamentul acestui lucru.
Să presupunem că aveți o vizualizare de tabel cu o mie de rânduri. Fără a folosi celule reutilizabile, ar trebui să creăm o nouă celulă pentru fiecare rând, astfel:
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 }
După cum ați fi crezut, acest lucru va adăuga o mie de celule în memoria dispozitivului pe măsură ce derulați până în jos. Imaginați-vă ce s-ar întâmpla dacă fiecare celulă ar conține un UIImageView
și o mulțime de text: încărcarea acestora pe toate odată ar putea duce la epuizarea memoriei aplicației! În afară de aceasta, fiecare celulă ar necesita o nouă memorie pentru a fi alocată în timpul derulării. Dacă derulați rapid o vizualizare a tabelului, o mulțime de bucăți mici de memorie vor fi alocate din mers, iar acest proces va face interfața de utilizator neplăcută!
Pentru a rezolva acest lucru, Apple ne-a oferit metoda dequeueReusableCell(withIdentifier:for:)
. Reutilizarea celulelor funcționează prin plasarea celulei care nu mai este vizibilă pe ecran într-o coadă, iar atunci când o celulă nouă este pe cale să fie vizibilă pe ecran (să zicem, celula ulterioară de mai jos pe măsură ce utilizatorul derulează în jos), vizualizarea tabelului va preluați o celulă din această coadă și modificați-o în metoda cellForRowAt indexPath:

Folosind o coadă pentru a stoca celule, vizualizarea tabelului nu trebuie să creeze o mie de celule. În schimb, are nevoie de suficiente celule pentru a acoperi zona vizualizării tabelului.
Utilizând dequeueReusableCell
, putem reduce memoria utilizată de aplicație și o facem mai puțin predispusă la epuizarea memoriei!
2. Utilizarea unui ecran de lansare care arată ca ecranul inițial
După cum se menționează în Ghidul pentru interfața umană (HIG) de la Apple, ecranele de lansare pot fi folosite pentru a îmbunătăți percepția asupra capacității de răspuns a unei aplicații:
„Este destinat exclusiv să îmbunătățească percepția aplicației dvs. ca fiind rapidă de lansat și imediat gata de utilizare. Fiecare aplicație trebuie să furnizeze un ecran de lansare.”
Este o greșeală comună să folosiți un ecran de lansare ca ecran de introducere pentru a afișa branding sau pentru a adăuga o animație de încărcare. Proiectați ecranul de lansare astfel încât să fie identic cu primul ecran al aplicației dvs., așa cum a menționat Apple:
„Proiectează un ecran de lansare care este aproape identic cu primul ecran al aplicației tale. Dacă includeți elemente care arată diferit când se termină lansarea aplicației, oamenii pot experimenta un fulger neplăcut între ecranul de lansare și primul ecran al aplicației.
„Ecranul de lansare nu este o oportunitate de branding. Nu proiectați o experiență de intrare care arată ca un ecran de deschidere sau o fereastră „Despre”. Nu includeți sigle sau alte elemente de branding decât dacă sunt o parte statică a primului ecran al aplicației dvs..”
Utilizarea unui ecran de lansare în scopuri de încărcare sau de branding ar putea încetini timpul de prima utilizare și poate face utilizatorul să simtă că aplicația este lenta.
Când începeți un nou proiect iOS, va fi creat un LaunchScreen.storyboard
gol. Acest ecran va fi afișat utilizatorului în timp ce aplicația încarcă controlerele de vizualizare și aspectul.
Pentru ca aplicația să se simtă mai rapidă, puteți proiecta ecranul de lansare astfel încât să fie similar cu primul ecran (controler de vizualizare) care va fi afișat utilizatorului.
De exemplu, ecranul de lansare al aplicației Safari este similar cu prima sa vizualizare:

Storyboard-ul ecranului de lansare este ca orice alt fișier storyboard, cu excepția faptului că puteți utiliza numai clasele UIKit standard, cum ar fi UIViewController, UITabBarController și UINavigationController. Dacă încercați să utilizați orice alte subclase personalizate (cum ar fi UserViewController), Xcode vă va anunța că utilizarea numelor personalizate de clase este interzisă.

Un alt lucru de remarcat este că UIActivityIndicatorView
nu se anime atunci când este plasat pe ecranul de lansare, deoarece iOS va genera o imagine statică din scenariul ecranului de lansare și o va afișa utilizatorului. (Acest lucru este menționat pe scurt în prezentarea WWDC 2014 „Platforms State of the Union”, în jurul orei 01:21:56
.)
De asemenea, HIG de la Apple ne sfătuiește să nu includem text pe ecranul nostru de lansare, deoarece ecranul de lansare este static și nu puteți localiza textul pentru a răspunde diferitelor limbi.
Lectură recomandată : Aplicație mobilă cu funcție de recunoaștere facială: Cum să o faci reală
3. Restaurarea stării pentru controlerele de vizualizare
Conservarea și restaurarea stării permit utilizatorului să revină la exact aceeași stare a interfeței de utilizare chiar înainte de a părăsi aplicația. Uneori, din cauza memoriei insuficiente, este posibil ca sistemul de operare să fie nevoie să șteargă aplicația din memorie în timp ce aplicația este în fundal, iar aplicația ar putea pierde evidența ultimei sale stări de interfață dacă nu este păstrată, ceea ce poate duce la pierderea activității utilizatorilor. în curs!
În ecranul multitasking, putem vedea o listă de aplicații care au fost puse în fundal. Am putea presupune că aceste aplicații încă rulează în fundal; în realitate, unele dintre aceste aplicații ar putea fi ucise și repornite de sistem din cauza solicitărilor de memorie. Instantaneele aplicației pe care le vedem în vizualizarea multitasking sunt de fapt capturi de ecran făcute de sistem chiar din momentul în care am ieșit din aplicație (adică pentru a merge la ecranul de pornire sau multitasking).

iOS folosește aceste capturi de ecran pentru a da iluzia că aplicația încă rulează sau afișează încă această vizualizare specială, în timp ce aplicația ar fi putut fi deja terminată sau repornită în fundal, în timp ce încă afișează aceeași captură de ecran.
Ați experimentat vreodată, la reluarea unei aplicații din ecranul multitasking, că aplicația arată o interfață de utilizator diferită de instantaneul afișat în vizualizarea multitasking? Acest lucru se datorează faptului că aplicația nu a implementat mecanismul de restabilire a stării, iar datele afișate s-au pierdut când aplicația a fost oprită în fundal. Acest lucru poate duce la o experiență proastă, deoarece utilizatorul se așteaptă ca aplicația dvs. să fie în aceeași stare ca atunci când a părăsit-o.
Din articolul Apple:
„Se așteaptă ca aplicația ta să fie în aceeași stare ca atunci când au părăsit-o. Conservarea și restaurarea stării asigură că aplicația dvs. revine la starea anterioară atunci când se lansează din nou.”
UIKit face o mulțime de muncă pentru a simplifica conservarea și restaurarea stării pentru noi: se ocupă de salvarea și încărcarea stării unei aplicații în mod automat la momentele adecvate. Tot ce trebuie să facem este să adăugăm o configurație pentru a spune aplicației să accepte conservarea și restaurarea stării și pentru a spune aplicației ce date trebuie păstrate.
Pentru a activa salvarea și restabilirea stării, putem implementa aceste două metode în AppDelegate.swift
:
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool { return true }
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool { return true }
Acest lucru va spune aplicației să salveze și să restabilească automat starea aplicației.
În continuare, vom spune aplicației ce controlere de vizualizare trebuie păstrate. Facem acest lucru specificând „ID-ul de restaurare” în storyboard:

Puteți, de asemenea, să bifați „Utilizare Storyboard ID” pentru a utiliza ID-ul storyboard-ului ca ID de restaurare.
Pentru a seta ID-ul restaurării în cod, putem folosi proprietatea restorationIdentifier
a controlerului de vizualizare.
// ViewController.swift self.restorationIdentifier = "MainVC"
În timpul păstrării stării, orice controler de vizualizare sau vizualizare căruia i-a fost atribuit un identificator de restaurare va avea starea salvată pe disc.
Identificatorii de restaurare pot fi grupați împreună pentru a forma o cale de restaurare. Identificatorii sunt grupați folosind ierarhia de vizualizare, de la controlerul de vizualizare rădăcină la controlerul de vizualizare activ curent. Să presupunem că un MyViewController este încorporat într-un controler de navigare, care este încorporat într-un alt controler cu bară de file. Presupunând că folosesc propriile nume de clasă ca identificatori de restaurare, calea de restaurare va arăta astfel:
TabBarController/NavigationController/MyViewController
Când utilizatorul părăsește aplicația cu MyViewController fiind controlerul de vizualizare activ, această cale va fi salvată de aplicație; apoi aplicația își va aminti ierarhia de vizualizare anterioară afișată ( Controler bară de tabele → Controler de navigare → Controler de vizualizare meu ).
După ce am atribuit identificatorul de restaurare, va trebui să implementăm metodele encodeRestorableState (cu codificator:) și decodeRestorableState (cu codificator:) pentru fiecare dintre controlerele de vizualizare păstrate. Aceste două metode ne permit să specificăm ce date trebuie să fie salvate sau încărcate și cum să le codificăm sau să le decodăm.

Să vedem controlerul de vizualizare:
// 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) } }
Nu uitați să apelați implementarea superclasei din partea de jos a propriei metode. Acest lucru asigură că clasa părinte are șansa de a salva și de a restabili starea.
Odată ce obiectele au terminat decodificarea, applicationFinishedRestoringState()
va fi apelată pentru a spune controlerului de vizualizare că starea a fost restaurată. Putem actualiza interfața de utilizare pentru controlerul de vizualizare în această metodă.
// 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 } }
Iată-l! Acestea sunt metodele esențiale pentru a implementa conservarea și restaurarea stării pentru aplicația dvs. Rețineți că sistemul de operare va elimina starea salvată atunci când aplicația este închisă forțat de către utilizator, pentru a evita blocarea într-o stare defectă în cazul în care ceva nu merge bine în conservarea și restaurarea stării.
De asemenea, nu stocați date de model (adică date care ar fi trebuit salvate în UserDefaults sau Core Data) în stare, chiar dacă ar putea părea convenabil să faceți acest lucru. Datele de stat vor fi eliminate atunci când forța utilizatorului părăsește aplicația dvs. și cu siguranță nu doriți să pierdeți datele modelului în acest fel.
Pentru a testa dacă conservarea și restaurarea stării funcționează bine, urmați pașii de mai jos:
- Creați și lansați o aplicație folosind Xcode.
- Navigați la ecranul cu starea de conservare și restaurare pe care doriți să îl testați.
- Reveniți la ecranul de pornire (prin glisarea în sus sau făcând dublu clic pe butonul de pornire sau apăsând Shift ⇧ + Cmd ⌘ + H în simulator) pentru a trimite aplicația în fundal.
- Opriți aplicația în Xcode apăsând butonul.
- Lansați din nou aplicația și verificați dacă starea a fost restabilită cu succes.
Deoarece această secțiune acoperă doar elementele de bază ale conservării și restaurării statului, recomand următoarele articole de la Apple Inc. pentru cunoștințe mai aprofundate despre restaurarea statului:
- Păstrarea și restaurarea stării
- Procesul de conservare a UI
- Procesul de restaurare a interfeței de utilizare
4. Reduceți utilizarea vizualizărilor non-opace cât mai mult posibil
O vedere opac este o vedere care nu are transparență, ceea ce înseamnă că orice element UI plasat în spatele ei nu este vizibil deloc. Putem seta ca o vizualizare să fie opaca în Interface Builder:

Sau o putem face programatic cu proprietatea isOpaque
a UIView:
view.isOpaque = true
Setarea unei vizualizări la opac va face ca sistemul de desen să optimizeze o anumită performanță a desenului în timp ce redă ecranul.
Dacă o vizualizare are transparență (adică alpha este sub 1,0), atunci iOS va trebui să facă o muncă suplimentară pentru a calcula ce ar trebui să fie afișat prin amestecarea diferitelor straturi de vizualizări în ierarhia vizualizării. Pe de altă parte, dacă o vedere este setată la opac, atunci sistemul de desen va pune această vedere în față și va evita munca suplimentară de amestecare a mai multor straturi de vizualizare în spatele ei.
Puteți verifica ce straturi sunt amestecate (neopace) în Simulatorul iOS, bifând Debug → Color Blended Layers .

După ce ați verificat opțiunea Straturi amestecate de culori , puteți vedea că unele vizualizări sunt roșii, iar altele sunt verzi. Roșu indică faptul că vizualizarea nu este opac și că afișarea sa de ieșire este rezultatul straturilor amestecate în spatele acesteia. Verdele indică faptul că vederea este opac și nu a fost efectuată nicio amestecare.

Etichetele afișate mai sus („Vedeți prietenii”, etc.) sunt evidențiate cu roșu, deoarece atunci când o etichetă este trasă în storyboard, culoarea de fundal este setată implicit la transparent. Când sistemul de desen alcătuiește afișajul în apropierea zonei etichetei, va cere stratul din spatele etichetei și va face niște calcule.
O modalitate prin care puteți optimiza performanța aplicației este să reduceți cât mai mult posibil câte vizualizări sunt evidențiate cu roșu.
Schimbând label.backgroundColor = UIColor.clear
în label.backgroundColor = UIColor.white
, putem reduce amestecarea stratului dintre etichetă și stratul de vizualizare din spatele acesteia.

S-ar putea să fi observat că, chiar dacă ați setat un UIImageView la opac și i-ați atribuit o culoare de fundal, simulatorul va afișa totuși roșu în vizualizarea imaginii. Acest lucru se datorează probabil că imaginea pe care ați folosit-o pentru vizualizarea imaginii are un canal alfa.
Pentru a elimina canalul alfa pentru o imagine, puteți utiliza aplicația Previzualizare pentru a face o copie a imaginii ( Shift ⇧ + Cmd ⌘ + S ) și debifați caseta de selectare „Alpha” când salvați.

5. Transmiteți funcțiile grele de procesare la firele de fundal (GCD)
Deoarece UIKit funcționează numai pe firul principal, efectuarea unei procesări grele pe firul principal va încetini interfața de utilizare. Firul principal este folosit de UIKit nu numai pentru a gestiona și a răspunde la intrarea utilizatorului, dar și pentru a desena ecranul.
Cheia pentru a face o aplicație receptivă este să mutați cât mai multe sarcini de procesare grele în firele de execuție în fundal. Evitați să faceți calcule complexe, rețele și operațiuni grele de IO (de exemplu, citirea și scrierea pe disc) pe firul principal.
Este posibil să fi folosit odată o aplicație care a devenit brusc fără răspuns la intrarea ta tactilă și se simte ca și cum aplicația s-a blocat. Acest lucru este cel mai probabil cauzat de aplicația care rulează sarcini de calcul grele pe firul principal.
Firul principal alternează de obicei între sarcini UIKit (cum ar fi gestionarea intrărilor utilizatorului) și unele sarcini ușoare la intervale mici. Dacă o sarcină grea rulează pe firul principal, atunci UIKit va trebui să aștepte până când sarcina grea se va termina înainte de a putea gestiona introducerea tactilă.

În mod implicit, codul din interiorul metodelor ciclului de viață al controlerului de vizualizare (cum ar fi viewDidLoad) și funcțiile IBOutlet sunt executate pe firul principal. Pentru a muta sarcinile grele de procesare într-un fir de execuție de fundal, putem folosi cozile Grand Central Dispatch oferite de Apple.
Iată șablonul pentru comutarea cozilor:
// 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. } }
qos
înseamnă „calitatea serviciului”. Diferite valori ale calității serviciului indică priorități diferite pentru sarcinile specificate. Sistemul de operare va aloca mai mult timp și putere CPU I/O pentru sarcinile alocate în cozi cu valori mai mari QoS, ceea ce înseamnă că o sarcină se va termina mai repede într-o coadă cu valori mai mari QoS. O valoare mai mare a QoS va consuma, de asemenea, mai multă energie datorită utilizării mai multor resurse.
Iată lista valorilor QoS de la cea mai mare la cea mai mică prioritate:

Apple a oferit un tabel la îndemână cu exemple de valori QoS de utilizat pentru diferite sarcini.
Un lucru de reținut este că tot codul UIKit ar trebui să fie întotdeauna executat pe firul principal. Modificarea obiectelor UIKit (cum ar fi UILabel
și UIImageView
) pe firul de execuție de fundal ar putea avea o consecință neintenționată, cum ar fi interfața de utilizare care nu se actualizează efectiv, apariția unui blocaj și așa mai departe.
Din articolul Apple:
„Actualizarea interfeței de utilizare pe un fir altul decât firul principal este o greșeală comună care poate duce la pierderea actualizărilor UI, defecte vizuale, corupții de date și blocări.”
Recomand să vizionați videoclipul Apple WWDC 2012 despre concurența UI pentru a înțelege mai bine cum să construiți o aplicație receptivă.
Note
Compartimentul optimizării performanței este că trebuie să scrieți mai mult cod sau să configurați setări suplimentare pe lângă funcționalitatea aplicației. Acest lucru ar putea face ca aplicația dvs. să fie livrată mai târziu decât se aștepta și veți avea mai mult cod de întreținut în viitor, iar mai mult cod înseamnă potențial mai multe erori.
Înainte de a petrece timp optimizării aplicației, întreabă-te dacă aplicația este deja fluidă sau dacă are o parte care nu răspunde care chiar trebuie optimizată. Petrecerea mult timp optimizării unei aplicații deja fluide pentru a reduce 0,01 secunde s-ar putea să nu merite, deoarece timpul ar putea fi petrecut mai bine pentru a dezvolta funcții mai bune sau alte priorități.
Resurse suplimentare
- „A Suite of Delicious iOS Eye Candy”, Tim Oliver, Tokyo iOS Meetup 2018 (video)
- „Crearea de interfețe de utilizator simultane pe iOS”, Andy Matuschak, WWDC 2012 (video)
- „Păstrarea interfeței de utilizare a aplicației dvs. în timpul lansărilor”, Apple
- „Ghid de programare simultană: cozi de expediere”, Arhiva de documentație, Apple
- „Main Thread Checker”, Apple