Animație și reglaj iOS pentru eficiență
Publicat: 2022-03-11Construirea unei aplicații grozave nu înseamnă doar aspect sau funcționalitate, ci și cât de bine funcționează. Deși specificațiile hardware ale dispozitivelor mobile se îmbunătățesc într-un ritm rapid, aplicațiile care funcționează slab, bâlbâie la fiecare tranziție de ecran sau derulează ca o prezentare de diapozitive pot strica experiența utilizatorului și pot deveni o cauză de frustrare. În acest articol vom vedea cum să măsuram performanța unei aplicații iOS și să o reglam pentru eficiență. În scopul acestui articol, vom construi o aplicație simplă cu o listă lungă de imagini și texte.
În scopul testării performanței, aș recomanda utilizarea dispozitivelor reale. Dacă sunteți serios să construiți aplicații și să le optimizați pentru o animație iOS fluidă, simulatoarele pur și simplu nu o fac. Simulările pot fi uneori în deplasare cu realitatea. De exemplu, simulatorul poate rula pe Mac-ul dvs., ceea ce înseamnă probabil că procesorul (unitatea centrală de procesare) este mult mai puternic decât procesorul de pe iPhone. Dimpotrivă, GPU (Unitatea de procesare grafică) este atât de diferită între dispozitivul dvs. și Mac, încât Mac-ul dvs. emulează de fapt GPU-ul dispozitivului. Ca rezultat, operațiunile legate de CPU tind să fie mai rapide pe simulator, în timp ce operațiunile legate de GPU tind să fie mai lente.
Animație la 60 FPS
Un aspect cheie al performanței percepute este să vă asigurați că animațiile dvs. rulează la 60 FPS (cadre pe secundă), care este rata de reîmprospătare a ecranului. Există câteva animații bazate pe cronometru, despre care nu le vom discuta aici. În general, dacă rulați la ceva mai mare de 50 FPS, aplicația dvs. va arăta netedă și performantă. Dacă animațiile tale sunt blocate între 20 și 40 FPS, va exista o bâlbâială vizibilă, iar utilizatorul va detecta o „asperitate” în tranziții. Orice sub 20 FPS va afecta grav capacitatea de utilizare a aplicației dvs.
Înainte de a începe, probabil că merită să discutăm despre diferența dintre operațiunile legate de CPU și cele legate de GPU. GPU-ul este un cip specializat care este optimizat pentru desenarea grafică. În timp ce procesorul poate și el, este mult mai lent. Acesta este motivul pentru care dorim să descarcăm cât mai mult din randarea noastră grafică, procesul de generare a unei imagini dintr-un model 2D sau 3D, în GPU. Dar trebuie să fim atenți, deoarece atunci când GPU-ul rămâne fără putere de procesare, performanța legată de grafică se va degrada chiar dacă procesorul este relativ liber.
Core Animation este un cadru puternic care gestionează animația atât în interiorul aplicației, cât și în afara acesteia. Acesta descompune procesul în 6 pași cheie:
Aspect: unde vă aranjați straturile și le setați proprietățile, cum ar fi culoarea și poziția lor relativă
Afișare: Aici sunt desenate imaginile de suport într-un context. Orice rutină pe care ați scris-o în
drawRect:
saudrawLayer:inContext:
este accesată aici.Pregătiți: În această etapă Core Animation, deoarece este pe cale să trimită context la redare pentru a se inspira, efectuează unele sarcini necesare, cum ar fi decomprimarea imaginilor.
Commit: Aici Core Animation trimite toate aceste date către serverul de randare.
Deserializare: cei 4 pași anteriori au fost toți în aplicația dvs., acum animația este procesată în afara aplicației dvs., straturile împachetate sunt deserializate într-un arbore pe care serverul de randare îl poate înțelege. Totul este convertit în geometrie OpenGL.
Desenează: Redă formele (de fapt triunghiuri).
S-ar putea să fi ghicit că procesele 1-4 sunt operațiuni CPU și 5-6 sunt operațiuni GPU. În realitate ai controlul doar asupra primilor 2 pași. Cel mai mare ucigaș al GPU-ului sunt straturile semi-transparente în care GPU-ul trebuie să umple același pixel de mai multe ori pe cadru. De asemenea, orice desen în afara ecranului (mai multe efecte de straturi, cum ar fi umbre, măști, colțuri rotunjite sau rasterizarea stratului va forța Core Animation să deseneze în afara ecranului) va afecta, de asemenea, performanța. Imaginile care sunt prea mari pentru a fi procesate de GPU vor fi procesate de procesorul mult mai lent. În timp ce umbrele pot fi obținute cu ușurință prin setarea a două proprietăți direct pe strat, ele pot distruge cu ușurință performanța dacă aveți multe obiecte pe ecran cu umbre. Uneori, merită să luați în considerare adăugarea acestor umbre ca imagini.
Măsurarea performanței animației iOS
Vom începe cu o aplicație simplă cu 5 imagini PNG și o vizualizare de tabel. În această aplicație, vom încărca în esență 5 imagini, dar o vom repeta pe 10.000 de rânduri. Vom adăuga umbre atât imaginilor, cât și etichetelor de lângă imagini:
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[CustomTableViewCell getCustomCellIdentifier] forIndexPath:indexPath]; NSInteger index = (indexPath.row % [self.images count]); NSString *imageName = [self.images objectAtIndex:index]; NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"]; UIImage *image = [UIImage imageWithContentsOfFile:filePath]; cell.customCellImageView.image = image; cell.customCellImageView.layer.shadowOffset = CGSizeMake(0, 5); cell.customCellImageView.layer.shadowOpacity = 0.8f; cell.customCellMainLabel.text = [NSString stringWithFormat:@"Row %li", (long)(indexPath.row + 1)]; cell.customCellMainLabel.layer.shadowOffset = CGSizeMake(0, 3); cell.customCellMainLabel.layer.shadowOpacity = 0.5f; return cell; }
Imaginile sunt pur și simplu reciclate, în timp ce etichetele sunt întotdeauna diferite. Rezultatul este:
După ce glisați pe verticală, este foarte probabil să observați bâlbâială pe măsură ce vizualizarea se derulează. În acest moment, s-ar putea să vă gândiți că faptul că încărcăm imagini în firul principal este problema. Poate că dacă am muta acest lucru în firul de fundal, toate problemele noastre ar fi rezolvate.
În loc să facem presupuneri oarbe, să încercăm și să măsurăm performanța. E timpul pentru Instrumente.
Pentru a utiliza Instrumente, trebuie să schimbați de la „Run” la „Profil”. Și ar trebui să fii conectat și la dispozitivul tău real, nu toate instrumentele sunt disponibile pe simulator (un alt motiv pentru care nu ar trebui să optimizezi performanța pe simulator!). Vom folosi în primul rând șabloanele „Driver GPU”, „Animție de bază” și „Profilator de timp”. Un fapt puțin cunoscut este că, în loc să vă opriți și să rulați pe un alt instrument, puteți glisa și plasa mai multe instrumente și puteți rula mai multe în același timp.
Acum că avem instrumentele configurate, să măsurăm. Mai întâi să vedem dacă într-adevăr avem o problemă cu FPS-ul nostru.
Da, cred că avem 18 FPS aici. Încărcarea imaginilor din pachetul de pe firul principal este într-adevăr atât de costisitoare și costisitoare? Observați că utilizarea rendererului nostru este aproape la maximum. La fel și utilizarea plăcilor noastre. Ambele sunt peste 95%. Și asta nu are nimic de-a face cu încărcarea unei imagini din pachet pe firul principal, așa că să nu căutăm soluții aici.
Reglaj pentru eficiență
Există o proprietate numită shouldRasterize și probabil că oamenii vă vor recomanda să o utilizați aici. Ce face mai exact shouldRasterize? Îți memorează în cache stratul ca o imagine aplatizată. Toate aceste desene scumpe de straturi trebuie să se întâmple o dată. În cazul în care cadrul dvs. se schimbă frecvent, nu este folositor cache-ul, deoarece va trebui oricum regenerat de fiecare dată.
Făcând o modificare rapidă a codului nostru, obținem:
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[CustomTableViewCell getCustomCellIdentifier] forIndexPath:indexPath]; NSInteger index = (indexPath.row % [self.images count]); NSString *imageName = [self.images objectAtIndex:index]; NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"]; UIImage *image = [UIImage imageWithContentsOfFile:filePath]; cell.customCellImageView.image = image; cell.customCellImageView.layer.shadowOffset = CGSizeMake(0, 5); cell.customCellImageView.layer.shadowOpacity = 0.8f; cell.customCellMainLabel.text = [NSString stringWithFormat:@"Row %li", (long)(indexPath.row + 1)]; cell.customCellMainLabel.layer.shadowOffset = CGSizeMake(0, 3); cell.customCellMainLabel.layer.shadowOpacity = 0.5f; cell.layer.shouldRasterize = YES; cell.layer.rasterizationScale = [UIScreen mainScreen].scale; return cell; }
Și măsurăm din nou:

Cu doar două linii, ne-am îmbunătățit FPS-ul de 2 ori. Acum avem o medie de peste 40 FPS. Dar ar fi de ajutor dacă am fi mutat încărcarea imaginii într-un fir de fundal?
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[CustomTableViewCell getCustomCellIdentifier] forIndexPath:indexPath]; NSInteger index = (indexPath.row % [self.images count]); NSString *imageName = [self.images objectAtIndex:index]; cell.tag = indexPath.row; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"]; UIImage *image = [UIImage imageWithContentsOfFile:filePath]; dispatch_async(dispatch_get_main_queue(), ^{ if (indexPath.row == cell.tag) { cell.customCellImageView.image = image; } }); }); cell.customCellImageView.layer.shadowOffset = CGSizeMake(0, 5); cell.customCellImageView.layer.shadowOpacity = 0.8f; cell.customCellMainLabel.text = [NSString stringWithFormat:@"Row %li", (long)(indexPath.row + 1)]; cell.customCellMainLabel.layer.shadowOffset = CGSizeMake(0, 3); cell.customCellMainLabel.layer.shadowOpacity = 0.5f; // cell.layer.shouldRasterize = YES; // cell.layer.rasterizationScale = [UIScreen mainScreen].scale; return cell; }
La măsurare, vedem că performanța este în medie de aproximativ 18 FPS:
Nimic care merită sărbătorit. Cu alte cuvinte, nu a adus nicio îmbunătățire ratei noastre de cadre. Asta pentru că, deși înfundarea firului principal este greșită, nu a fost blocajul nostru, ci redarea.
Revenind la exemplul mai bun, unde am avut o medie de peste 40 FPS, performanța este considerabil mai fluidă. Dar de fapt putem face mai bine.
Bifând „Straturi amestecate de culori” pe Instrumentul de animație de bază, vedem:
„Color Blended Layers” se afișează pe ecran unde GPU-ul dvs. face o mulțime de randări. Verdele indică cea mai mică activitate de randare, în timp ce roșu indică cea mai mare. Dar am setat shouldRasterize la YES
. Merită subliniat faptul că „Straturi amestecate de culori” nu este același lucru cu „Color Hits Green and Misses Red”. Mai târziu, practic, evidențiază straturi rasterizate în roșu pe măsură ce memoria cache este regenerată (un instrument bun pentru a vedea dacă nu utilizați cache-ul corect). Setarea shouldRasterize la YES
nu are niciun efect asupra redării inițiale a straturilor neopace.
Acesta este un punct important și trebuie să facem o pauză pentru a gândi. Indiferent dacă shouldRasterize este setat la YES
sau nu, pentru a reda cadrul trebuie să verifice toate vizualizările și să se amestece (sau nu) în funcție de faptul că subvizualizările sunt transparente sau opace. Deși ar putea avea sens ca UILabel-ul dvs. să fie neopac, poate că nu are valoare și vă distruge performanța. De exemplu, o etichetă UILabel transparentă pe un fundal alb este probabil fără valoare. Să-l facem opac:
Acest lucru oferă o performanță mai bună, dar aspectul și aspectul aplicației s-au schimbat. Acum, pentru că eticheta și imaginile noastre sunt opace, umbra s-a mutat în jurul imaginii noastre. Probabil că nimeni nu va fi îndrăgostit de această schimbare, iar dacă vrem să păstrăm aspectul și senzația originală cu performanțe de top, nu suntem pierduti de speranță.
Pentru a scoate niște FPS în plus, păstrând în același timp aspectul original, este important să revizuim două dintre fazele noastre de animație de bază pe care le-am neglijat până acum.
- A pregati
- Angajează-te
Acestea pot părea că sunt complet din mâinile noastre, dar nu este chiar adevărat. Știm că o imagine pentru a fi încărcată trebuie decomprimată. Timpul de decompresie se modifică în funcție de formatul imaginii. Pentru PNG-urile, decompresia este mult mai rapidă decât JPEG-urile (deși încărcarea este mai lungă, iar acest lucru depinde și de dimensiunea imaginii), așa că am fost oarecum pe drumul cel bun să folosim PNG-urile, dar nu facem nimic în ceea ce privește procesul de decompresie și această decompresie. se întâmplă la „punctul de desen”! Acesta este cel mai rău loc posibil în care putem ucide timpul - pe firul principal.
Există o modalitate de a forța la decompresie. L-am putea seta imediat la proprietatea imagine a unui UIImageView. Dar asta încă decomprimă imaginea de pe firul principal. Există vreo modalitate mai bună?
Există unul. Desenați-o într-un CGContext, unde imaginea trebuie decomprimată înainte de a putea fi desenată. Putem face acest lucru (folosind CPU) într-un thread de fundal și îi dăm limite după cum este necesar, în funcție de dimensiunea imaginii noastre. Acest lucru va optimiza procesul nostru de desenare a imaginii făcându-l din firul principal și ne va salva de la „pregătirea” calculelor inutile pe firul principal.
În timp ce suntem la asta, de ce să nu adăugați umbre în timp ce desenăm imaginea? Apoi putem captura imaginea (și o putem stoca în cache) ca o singură imagine statică, opacă. Codul este următorul:
- (UIImage*)generateImageFromName:(NSString*)imageName { //define a boudns for drawing CGRect imgVwBounds = CGRectMake(0, 0, 48, 48); //get the image NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"]; UIImage *image = [UIImage imageWithContentsOfFile:filePath]; //draw in the context UIGraphicsBeginImageContextWithOptions(imgVwBounds.size, NO, 0); { //get context CGContextRef context = UIGraphicsGetCurrentContext(); //shadow CGContextSetShadowWithColor(context, CGSizeMake(0, 3.0f), 3.0f, [UIColor blackColor].CGColor); CGContextBeginTransparencyLayer (context, NULL); [image drawInRect:imgVwBounds blendMode:kCGBlendModeNormal alpha:1.0f]; CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 1.0); CGContextEndTransparencyLayer(context); } image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; }
Și, în sfârșit:
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[CustomTableViewCell getCustomCellIdentifier] forIndexPath:indexPath]; // NSInteger index = (indexPath.row % [self.images count]); // NSString *imageName = [self.images objectAtIndex:index]; // // cell.tag = indexPath.row; // // dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"]; // UIImage *image = [UIImage imageWithContentsOfFile:filePath]; // // dispatch_async(dispatch_get_main_queue(), ^{ // if (indexPath.row == cell.tag) { // cell.customCellImageView.image = image; // } // }); // }); cell.customCellImageView.image = [self getImageByIndexPath:indexPath]; cell.customCellImageView.clipsToBounds = YES; // cell.customCellImageView.layer.shadowOffset = CGSizeMake(0, 5); // cell.customCellImageView.layer.shadowOpacity = 0.8f; cell.customCellMainLabel.text = [NSString stringWithFormat:@"Row %li", (long)(indexPath.row + 1)]; cell.customCellMainLabel.layer.shadowOffset = CGSizeMake(0, 3); cell.customCellMainLabel.layer.shadowOpacity = 0.5f; cell.layer.shouldRasterize = YES; cell.layer.rasterizationScale = [UIScreen mainScreen].scale; return cell; }
Iar rezultatele sunt:
Acum avem o medie de peste 55 FPS, iar utilizarea noastră de randare și utilizarea plăcilor sunt aproape jumătate față de ceea ce erau inițial.
Învelire
Doar în cazul în care vă întrebați ce altceva putem face pentru a da mai multe cadre pe secundă, nu căutați mai departe. UILabel folosește HTML WebKit pentru a reda textul. Putem merge direct la CATextLayer și poate ne jucăm și cu umbrele de acolo.
Poate ați observat în implementarea noastră de mai sus, nu făceam încărcarea imaginii într-un thread de fundal, ci în schimb o puneam în cache. Deoarece erau doar 5 imagini, acest lucru a funcționat foarte rapid și nu părea să afecteze performanța generală (mai ales că toate cele 5 imagini au fost încărcate pe ecran înainte de defilare). Dar poate doriți să încercați să mutați această logică într-un thread de fundal pentru o performanță suplimentară.
Reglarea pentru eficiență este diferența dintre o aplicație de clasă mondială și una pentru amatori. Optimizarea performanței, mai ales când vine vorba de animația iOS, poate fi o sarcină descurajantă. Dar cu ajutorul Instruments, se pot diagnostica cu ușurință blocajele în performanța animației pe iOS.