Cele mai frecvente 10 greșeli pe care dezvoltatorii iOS nu știu că le fac
Publicat: 2022-03-11Care este singurul lucru mai rău decât respingerea unei aplicații cu erori de către App Store? Avand-o acceptata. Odată ce recenziile de o stea încep să apară, este aproape imposibil de recuperat. Acest lucru costă bani companiilor și dezvoltatorii lor locurile de muncă.
iOS este acum al doilea cel mai mare sistem de operare mobil din lume. Are, de asemenea, o rată de adoptare foarte mare, cu peste 85% dintre utilizatori pe cea mai recentă versiune. După cum v-ați putea aștepta, utilizatorii foarte implicați au așteptări mari – dacă aplicația sau actualizarea dvs. nu este perfectă, veți auzi despre asta.
Cu cererea de dezvoltatori iOS care continuă să crească vertiginos, mulți ingineri au trecut la dezvoltarea mobilă (mai mult de 1.000 de aplicații noi sunt trimise la Apple în fiecare zi). Dar adevărata expertiză iOS se extinde cu mult dincolo de codificarea de bază. Mai jos sunt 10 greșeli comune cărora dezvoltatorii iOS cad pradă și cum le puteți evita.
Greșeala comună nr. 1: Nu înțelegerea proceselor asincrone
Un tip foarte comun de greșeală în rândul programatorilor noi este manipularea incorectă a codului asincron. Să luăm în considerare un scenariu tipic: un utilizator deschide un ecran cu vizualizarea tabelului. Unele date sunt preluate de la server și afișate într-o vizualizare de tabel. O putem scrie mai formal:
@property (nonatomic, strong) NSArray *dataFromServer; - (void)viewDidLoad { __weak __typeof(self) weakSelf = self; [[ApiManager shared] latestDataWithCompletionBlock:^(NSArray *newData, NSError *error){ weakSelf.dataFromServer = newData; // 1 }]; [self.tableView reloadData]; // 2 } // and other data source delegate methods - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.dataFromServer.count; }
La prima vedere, totul pare corect: preluăm date de pe server și apoi actualizăm interfața de utilizare. Cu toate acestea, problema este că preluarea datelor este un proces asincron și nu va returna date noi imediat, ceea ce înseamnă că reloadData
va fi apelat înainte de a primi datele noi. Pentru a remedia această greșeală, ar trebui să mutăm linia #2 imediat după linia #1 în interiorul blocului.
@property (nonatomic, strong) NSArray *dataFromServer; - (void)viewDidLoad { __weak __typeof(self) weakSelf = self; [[ApiManager shared] latestDataWithCompletionBlock:^(NSArray *newData, NSError *error){ weakSelf.dataFromServer = newData; // 1 [weakSelf.tableView reloadData]; // 2 }]; } // and other data source delegate methods - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.dataFromServer.count; }
Cu toate acestea, pot exista situații în care acest cod încă nu se comportă conform așteptărilor, ceea ce ne duce la...
Greșeala comună nr. 2: Rularea codului legat de interfața de utilizare pe un fir altul decât coada principală
Să ne imaginăm că am folosit un exemplu de cod corectat din greșeala comună anterioară, dar vizualizarea noastră tabel încă nu este actualizată cu noile date chiar și după ce procesul asincron s-a finalizat cu succes. Ce ar putea fi în neregulă cu un cod atât de simplu? Pentru a înțelege, putem seta un punct de întrerupere în interiorul blocului și putem afla în ce coadă este numit acest bloc. Există o șansă mare ca comportamentul descris să se întâmple deoarece apelul nostru nu se află în coada principală, unde ar trebui să fie efectuat tot codul legat de interfața de utilizare.
Cele mai populare biblioteci, cum ar fi Alamofire, AFNetworking și Haneke, sunt concepute pentru a apela completionBlock
în coada principală după efectuarea unei sarcini asincrone. Cu toate acestea, nu vă puteți baza întotdeauna pe acest lucru și este ușor să uitați să trimiteți codul la coada potrivită.
Pentru a vă asigura că tot codul dvs. legat de UI se află în coada principală, nu uitați să-l trimiteți în acea coadă:
dispatch_async(dispatch_get_main_queue(), ^{ [self.tableView reloadData]; });
Greșeala comună nr. 3: înțelegerea greșită a concurenței și a multithreading-ului
Concurența ar putea fi comparată cu un cuțit cu adevărat ascuțit: te poți tăia cu ușurință dacă nu ești suficient de atent sau experimentat, dar este extrem de util și eficient odată ce știi cum să-l folosești corect și în siguranță.
Puteți încerca să evitați utilizarea concurenței, dar indiferent de ce fel de aplicații construiți, există șanse foarte mari să nu vă puteți descurca fără ea. Concurența poate avea beneficii semnificative pentru aplicația dvs. În special:
- Aproape fiecare aplicație are apeluri la servicii web (de exemplu, pentru a efectua niște calcule grele sau pentru a citi date dintr-o bază de date). Dacă aceste sarcini sunt efectuate în coada principală, aplicația se va bloca pentru o perioadă de timp, făcând-o să nu răspundă. În plus, dacă acest lucru durează prea mult, iOS va închide complet aplicația. Mutarea acestor sarcini într-o altă coadă permite utilizatorului să continue să utilizeze aplicația în timp ce operațiunea este efectuată fără ca aplicația să pară să se înghețe.
- Dispozitivele moderne iOS au mai mult de un nucleu, așa că de ce ar trebui utilizatorul să aștepte ca sarcinile să se termine secvențial când acestea pot fi efectuate în paralel?
Dar avantajele concurenței nu vin fără complexitate și potențialul de a introduce bug-uri noduroase, cum ar fi condițiile de cursă care sunt cu adevărat greu de reprodus.
Să luăm în considerare câteva exemple din lumea reală (rețineți că un anumit cod este omis pentru simplitate).
Cazul 1
final class SpinLock { private var lock = OS_SPINLOCK_INIT func withLock<Return>(@noescape body: () -> Return) -> Return { OSSpinLockLock(&lock) defer { OSSpinLockUnlock(&lock) } return body() } } class ThreadSafeVar<Value> { private let lock: ReadWriteLock private var _value: Value var value: Value { get { return lock.withReadLock { return _value } } set { lock.withWriteLock { _value = newValue } } } }
Codul cu mai multe fire:
let counter = ThreadSafeVar<Int>(value: 0) // this code might be called from several threads counter.value += 1 if (counter.value == someValue) { // do something }
La prima vedere, totul este sincronizat și pare că ar trebui să funcționeze conform așteptărilor, deoarece ThreadSaveVar
înfășoară counter
și îl face sigur pentru fire. Din păcate, acest lucru nu este adevărat, deoarece două fire de execuție ar putea ajunge la linia de creștere simultan și counter.value == someValue
nu va deveni niciodată adevărată ca rezultat. Ca o soluție, putem face ThreadSafeCounter
care își returnează valoarea după incrementare:
class ThreadSafeCounter { private var value: Int32 = 0 func increment() -> Int { return Int(OSAtomicIncrement32(&value)) } }
Cazul 2
struct SynchronizedDataArray { private let synchronizationQueue = dispatch_queue_create("queue_name", nil) private var _data = [DataType]() var data: [DataType] { var dataInternal = [DataType]() dispatch_sync(self.synchronizationQueue) { dataInternal = self._data } return dataInternal } mutating func append(item: DataType) { appendItems([item]) } mutating func appendItems(items: [DataType]) { dispatch_barrier_sync(synchronizationQueue) { self._data += items } } }
În acest caz, dispatch_barrier_sync
a fost folosit pentru a sincroniza accesul la matrice. Acesta este un model preferat pentru a asigura sincronizarea accesului. Din păcate, acest cod nu ține cont de faptul că struct face o copie de fiecare dată când anexăm un element la el, având astfel o nouă coadă de sincronizare de fiecare dată.
Aici, chiar dacă pare corect la prima vedere, s-ar putea să nu funcționeze așa cum era de așteptat. De asemenea, necesită multă muncă pentru a-l testa și a depana, dar în cele din urmă, puteți îmbunătăți viteza și capacitatea de răspuns a aplicației.
Greșeala comună nr. 4: Nu cunoașteți capcanele obiectelor mutabile
Swift este foarte util în a evita greșelile cu tipurile de valori, dar există încă o mulțime de dezvoltatori care folosesc Objective-C. Obiectele mutabile sunt foarte periculoase și pot duce la probleme ascunse. Este o regulă binecunoscută că obiectele imuabile ar trebui returnate din funcții, dar majoritatea dezvoltatorilor nu știu de ce. Să luăm în considerare următorul cod:
// Box.h @interface Box: NSObject @property (nonatomic, readonly, strong) NSArray <Box *> *boxes; @end // Box.m @interface Box() @property (nonatomic, strong) NSMutableArray <Box *> *m_boxes; - (void)addBox:(Box *)box; @end @implementation Box - (instancetype)init { self = [super init]; if (self) { _m_boxes = [NSMutableArray array]; } return self; } - (void)addBox:(Box *)box { [self.m_boxes addObject:box]; } - (NSArray *)boxes { return self.m_boxes; } @end
Codul de mai sus este corect, deoarece NSMutableArray
este o subclasă a NSArray
. Deci, ce poate merge prost cu acest cod?
Primul și cel mai evident lucru este că un alt dezvoltator ar putea veni și face următoarele:
NSArray<Box *> *childBoxes = [box boxes]; if ([childBoxes isKindOfClass:[NSMutableArray class]]) { // add more boxes to childBoxes }
Acest cod vă va da peste cap clasa. Dar în acest caz, este un miros de cod și este lăsat la latitudinea acelui dezvoltator să ridice piesele.
Iată, totuși, cazul, care este mult mai rău și demonstrează un comportament neașteptat:
Box *box = [[Box alloc] init]; NSArray<Box *> *childBoxes = [box boxes]; [box addBox:[[Box alloc] init]]; NSArray<Box *> *newChildBoxes = [box boxes];
Asteptarea aici este ca [newChildBoxes count] > [childBoxes count]
, dar ce se întâmplă dacă nu este? Atunci clasa nu este bine concepută, deoarece modifică o valoare care a fost deja returnată. Dacă credeți că inegalitatea nu ar trebui să fie adevărată, încercați să experimentați cu UIView și [view subviews]
.
Din fericire, ne putem remedia cu ușurință codul, rescriind getter-ul din primul exemplu:
- (NSArray *)boxes { return [self.m_boxes copy]; }
Greșeala comună nr. 5: Nu înțelegeți cum funcționează iOS NSDictionary
intern
Dacă ați lucrat vreodată cu o clasă personalizată și NSDictionary
, s-ar putea să realizați că nu vă puteți folosi clasa dacă nu este conformă cu NSCopying
ca cheie de dicționar. Majoritatea dezvoltatorilor nu s-au întrebat niciodată de ce Apple a adăugat această restricție. De ce copia Apple cheia și folosește acea copie în loc de obiectul original?
Cheia pentru înțelegerea acestui lucru este să descoperi cum funcționează NSDictionary
intern. Din punct de vedere tehnic, este doar o masă hash. Să recapitulăm rapid cum funcționează la un nivel înalt în timp ce adăugați un obiect pentru o cheie (redimensionarea tabelului și optimizarea performanței sunt omise aici pentru simplitate):
Pasul 1: calculează
hash(Key)
. Pasul 2: Pe baza hashului, caută un loc pentru a pune obiectul. De obicei, acest lucru se face luând modulul valorii hash cu lungimea dicționarului. Indexul rezultat este apoi folosit pentru a stoca perechea Cheie/Valoare. Pasul 3: Dacă nu există niciun obiect în acea locație, creează o listă legată și stochează înregistrarea noastră (obiect și cheie). În caz contrar, adaugă înregistrarea la sfârșitul listei.
Acum, să descriem cum este preluată o înregistrare din dicționar:
Pasul 1: calculează
hash(Key)
. Pasul 2: caută o cheie după hash. Dacă nu există date, se returneazănil
. Pasul 3: Dacă există o listă legată, aceasta iterează prin Obiect până la[storedkey isEqual:Key]
.
Cu această înțelegere a ceea ce se întâmplă sub capotă, se pot trage două concluzii:
- Dacă hash-ul cheii se modifică, înregistrarea ar trebui mutată într-o altă listă conectată.
- Cheile ar trebui să fie unice.
Să examinăm acest lucru pe o clasă simplă:
@interface Person @property NSMutableString *name; @end @implementation Person - (BOOL)isEqual:(id)object { if (self == object) { return YES; } if (![object isKindOfClass:[Person class]]) { return NO; } return [self.name isEqualToSting:((Person *)object).name]; } - (NSUInteger)hash { return [self.name hash]; } @end
Acum imaginați-vă că NSDictionary
nu copie cheile:
NSMutableDictionary *gotCharactersRating = [[NSMutableDictionary alloc] init]; Person *p = [[Person alloc] init]; p.name = @"Job Snow"; gotCharactersRating[p] = @10;
Oh! Avem o greșeală de scriere acolo! Hai să o reparăm!
p.name = @"Jon Snow";
Ce ar trebui să se întâmple cu dicționarul nostru? Deoarece numele a fost modificat, acum avem un hash diferit. Acum obiectul nostru se află în locul greșit (are încă vechea valoare hash, deoarece dicționarul nu știe despre modificarea datelor) și nu este chiar clar ce hash ar trebui să folosim pentru a căuta date în dicționar. Ar putea fi un caz și mai rău. Imaginați-vă dacă am avea deja „Jon Snow” în dicționarul nostru cu un rating de 5. Dicționarul ar avea două valori diferite pentru aceeași cheie.
După cum puteți vedea, există multe probleme care pot apărea din a avea chei mutabile în NSDictionary
. Cea mai bună practică pentru a evita astfel de probleme este să copiați obiectul înainte de a-l stoca și să marcați proprietățile ca copy
. Această practică vă va ajuta, de asemenea, să vă mențineți clasa constantă.
Greșeala comună nr. 6: Folosirea Storyboard-urilor în loc de XIB-uri
Majoritatea dezvoltatorilor iOS noi urmează sugestia Apple și folosesc scenarii în mod implicit pentru interfața de utilizare. Cu toate acestea, există o mulțime de dezavantaje și doar câteva avantaje (discutabile) în utilizarea storyboard-urilor.
Dezavantajele storyboard-ului includ:
- Este foarte greu să modifici un storyboard pentru mai mulți membri ai echipei. Din punct de vedere tehnic, puteți folosi multe storyboard-uri, dar singurul avantaj, în acest caz, este că face posibilă existența unor secvențe între controlere de pe storyboard.
- Numele controlerelor și segurilor din storyboard-uri sunt șiruri, așa că trebuie fie să reintroduceți toate acele șiruri în codul dvs. (și într-o zi îl veți sparge), fie să mențineți o listă imensă de constante de storyboard. Ai putea folosi SBConstants, dar redenumirea pe storyboard nu este încă o sarcină ușoară.
- Storyboard-urile te forțează într-un design non-modular. În timp ce lucrați cu un storyboard, există foarte puține stimulente pentru a vă face vizualizările reutilizabile. Acest lucru poate fi acceptabil pentru produsul minim viabil (MVP) sau pentru prototiparea rapidă a interfeței de utilizare, dar în aplicațiile reale ar putea fi necesar să utilizați aceeași vizualizare de mai multe ori în aplicația dvs.
Avantaje storyboard (discutabil):
- Întreaga navigare a aplicației poate fi văzută dintr-o privire. Cu toate acestea, aplicațiile reale pot avea mai mult de zece controlere, conectate în direcții diferite. Storyboard-urile cu astfel de conexiuni arată ca un ghem și nu oferă nicio înțelegere la nivel înalt a fluxurilor de date.
- Tabele statice. Acesta este singurul avantaj real la care mă pot gândi. Problema este că 90% dintre tabelele statice tind să se transforme în tabele dinamice în timpul evoluției aplicației, iar un tabel dinamic poate fi gestionat mai ușor de XIB-uri.
Greșeala comună nr. 7: Compararea obiectelor și a indicatorului confuz
În timp ce comparăm două obiecte, putem lua în considerare două egalități: egalitatea pointerului și a obiectelor.
Egalitatea pointerului este o situație în care ambii pointeri indică același obiect. În Objective-C, folosim operatorul ==
pentru a compara doi pointeri. Egalitatea obiectelor este o situație în care două obiecte reprezintă două obiecte identice din punct de vedere logic, ca același utilizator dintr-o bază de date. În Objective-C, folosim isEqual
, sau chiar mai bine, tip specific isEqualToString
, isEqualToDate
etc. pentru a compara două obiecte.
Luați în considerare următorul cod:
NSString *a = @"a"; // 1 NSString *b = @"a"; // 2 if (a == b) { // 3 NSLog(@"%@ is equal to %@", a, b); } else { NSLog(@"%@ is NOT equal to %@", a, b); }
Ce va fi imprimat pe consolă când rulăm acel cod? Vom obține a is equal to b
, deoarece ambele obiecte a
și b
indică același obiect din memorie.
Dar acum să schimbăm linia 2 în:
NSString *b = [[@"a" mutableCopy] copy];
Acum obținem că a is NOT equal to b
, deoarece acești indicatori indică acum obiecte diferite, chiar dacă acele obiecte au aceleași valori.
Această problemă poate fi evitată bazându-ne pe isEqual
sau pe funcții specifice de tip. În exemplul nostru de cod, ar trebui să înlocuim linia 3 cu următorul cod pentru ca acesta să funcționeze întotdeauna corect:
if ([a isEqual:b]) {
Greșeala comună nr. 8: Utilizarea valorilor codificate
Există două probleme principale cu valorile codificate:
- De multe ori nu este clar ce reprezintă.
- Acestea trebuie reintroduse (sau copiate și lipite) atunci când trebuie utilizate în mai multe locuri din cod.
Luați în considerare următorul exemplu:
if ([[NSDate date] timeIntervalSinceDate:self.lastAppLaunch] < 172800) { // do something } or [self.tableView registerNib:nib forCellReuseIdentifier:@"SimpleCell"]; ... [self.tableView dequeueReusableCellWithIdentifier:@"SimpleCell"];
Ce reprezintă 172800? De ce este folosit? Probabil că nu este evident că acesta corespunde numărului de secunde în 2 zile (sunt 24 x 60 x 60, sau 86.400 de secunde într-o zi).
În loc să utilizați valori codificate, puteți defini o valoare folosind instrucțiunea #define
. De exemplu:
#define SECONDS_PER_DAY 86400 #define SIMPLE_CELL_IDENTIFIER @"SimpleCell"
#define
este o macrocomandă de preprocesor care înlocuiește definiția numită cu valoarea acesteia din cod. Deci, dacă aveți #define
într-un fișier antet și îl importați undeva, toate aparițiile valorii definite în acel fișier vor fi și ele înlocuite.
Funcționează bine, cu excepția unei probleme. Pentru a ilustra problema rămasă, luați în considerare următorul cod:
#define X = 3 ... CGFloat y = X / 2;
La ce te-ai aștepta să fie valoarea lui y
după executarea acestui cod? Daca ai spus 1.5, esti gresit. y
va fi egal cu 1 ( nu 1,5) după executarea acestui cod. De ce? Răspunsul este că #define
nu are informații despre tip. Deci, în cazul nostru, avem o împărțire a două valori Int
(3 și 2), care are ca rezultat un Int
(adică, 1) care este apoi transformat într-un Float
.
Acest lucru poate fi evitat prin utilizarea constantelor care sunt, prin definiție, tastate:
static const CGFloat X = 3; ... CGFloat y = X / 2; // y will now equal 1.5, as expected
Greșeala comună nr. 9: Utilizarea cuvântului cheie implicit într-o declarație Switch
Utilizarea cuvântului cheie default
într-o declarație switch poate duce la erori și la un comportament neașteptat. Luați în considerare următorul cod în Objective-C:
typedef NS_ENUM(NSUInteger, UserType) { UserTypeAdmin, UserTypeRegular }; - (BOOL)canEditUserWithType:(UserType)userType { switch (userType) { case UserTypeAdmin: return YES; default: return NO; } }
Același cod scris în Swift:
enum UserType { case Admin, Regular } func canEditUserWithType(type: UserType) -> Bool { switch(type) { case .Admin: return true default: return false } }
Acest cod funcționează conform intenției, permițând doar utilizatorilor administratori să poată schimba alte înregistrări. Cu toate acestea, ce s-ar putea întâmpla să adăugăm un alt tip de utilizator, „manager”, care ar trebui să poată edita și înregistrările? Dacă uităm să actualizăm această instrucțiune switch
, codul se va compila, dar nu va funcționa așa cum era de așteptat. Cu toate acestea, dacă dezvoltatorul a folosit valorile enumerare în loc de cuvântul cheie implicit de la bun început, supravegherea va fi identificată în timpul compilării și ar putea fi remediată înainte de a merge la testare sau producție. Iată o modalitate bună de a gestiona acest lucru în Objective-C:
typedef NS_ENUM(NSUInteger, UserType) { UserTypeAdmin, UserTypeRegular, UserTypeManager }; - (BOOL)canEditUserWithType:(UserType)userType { switch (userType) { case UserTypeAdmin: case UserTypeManager: return YES; case UserTypeRegular: return NO; } }
Același cod scris în Swift:
enum UserType { case Admin, Regular, Manager } func canEditUserWithType(type: UserType) -> Bool { switch(type) { case .Manager: fallthrough case .Admin: return true case .Regular: return false } }
Greșeala comună nr. 10: Utilizarea NSLog
pentru înregistrare
Mulți dezvoltatori iOS folosesc NSLog
în aplicațiile lor pentru înregistrare, dar de cele mai multe ori aceasta este o greșeală teribilă. Dacă verificăm documentația Apple pentru descrierea funcției NSLog
, vom vedea că este foarte simplu:
void NSLog(NSString *format, ...);
Ce ar putea merge prost cu el? De fapt, nimic. Cu toate acestea, dacă vă conectați dispozitivul la organizatorul Xcode, veți vedea acolo toate mesajele de depanare. Numai din acest motiv, nu ar trebui să utilizați niciodată NSLog
pentru înregistrare: este ușor să afișați unele date interne nedorite, plus că pare neprofesional.
O abordare mai bună este înlocuirea NSLogs
-urilor cu CocoaLumberjack configurabil sau un alt cadru de înregistrare.
Învelire
iOS este o platformă foarte puternică și care evoluează rapid. Apple face un efort continuu masiv pentru a introduce hardware și funcții noi pentru iOS însuși, extinzând în același timp limbajul Swift.
Îmbunătățirea abilităților Objective-C și Swift vă va face un excelent dezvoltator iOS și vă va oferi oportunități de a lucra la proiecte provocatoare folosind tehnologii de ultimă oră.