1. diel - Úvod do viacvláknových aplikácií vo Swift
Vitajte u prvej lekcie Swift kurzu, ktorý vás naučí, ako vo vašich iOS, MacOS či ďalších aplikáciách napísaných v programovacom jazyku Swift využiť maximálny potenciál zariadenia. Súčasné iPhone aj iPad modely disponujú niekoľkými procesorovými jadrami, nemluvňa o strojoch s MacOS. V predvolenom stave ale náš program vždy využije iba jedno jadro. To môže znamenať napríklad štvrtinu dostupného výkonu.
Predsa mega výkon A12 Bionic nechcete nechať ležať ladom, nie?
Využitie viac vlákien (multithreading) je ale dôležité z oveľa podstatnejšieho dôvodu, než zapriahnuté všetkých dostupných jadier procesora. Aplikácia majú štandardne jedno hlavné vlákno (označované ako Main alebo UI thread), ktoré skrátka vykonáva náš všetok kód.
Toto hlavné vlákno má za úlohu aplikáciu spustiť, vykresliť užívateľské rozhranie, vykonať naše metódy a veľa ďalšieho. Rovnako tak užívateľské rozhranie následne obsluhuje, takže ak napríklad v iOS aplikácii chceme scrollovať komponentom TableView, na pozadí je to opäť práca pre hlavné vlákno, aby všetko korektne obstaralo.
Čo myslíte, že sa stane s aplikáciou, keď bude toto hlavné vlákno zaneprázdnené čímkoľvek iným? Skúste porozmýšľať ...
Aplikácia sa jednoducho bude tváriť zaseknutá do doby, než hlavné vlákno dokončí predchádzajúcej práci a vráti sa na obsluhu užívateľského rozhrania. Asi nie je potrebné diskutovať nad tým, že túto situáciu v aplikáciách jednoducho mať nechceme. Užívateľom je nepríjemné, keď na ne aplikácie nereaguje
Asi najčastejšie sa nám podobná situácia môže stať, keď budeme sťahovať dáta z internetu. Pokojne to pri vývoji ani nemusíme zistiť, pretože sťahujeme pár kilobajtov JSON dát a máme vždy rýchly a spoľahlivý internet. Hlavné vlákno tak dáta stiahne prakticky okamžite k zaseknutiu aplikácie nedôjde.
Lenže v praxi môžeme naraziť na zahltený server, ktorému bude odpoveď trvať napríklad dve sekundy. Alebo zrovna mobil užívateľa bude pripojený k preťažené WiFi či na EDGE mobilné pripojenie a razom máme zaseknutú alebo zasekanie aplikácii (ak sťahujeme viac dát).
Hello GCD!
Vo Swift nie je viacvláknové programovanie tak komplikované, ako môže z
názvu znieť. V množstve jazykov musí programátori vytvárať vlákna ako
objekty, potom sa starať o ich spustenie, dokončenie a ktovie čo ešte. Vo
Swift síce možno triedu Thread
vyhrabať, ale prakticky sa
nepoužíva. Máme totiž oveľa lepšie riešenie.
Zoznámte sa s GCD, už čoskoro vašim pomocníkom pre krotenie vlákien. Za skratkou sa ukrýva Grand Central Dispatcher a jeho úlohou je spravovať vlákna a zbaviť programátora zložitosťou. Vo väčšine prípadov budeme len GCD úkolovať, že určitý kód chceme vykonať na pozadí (background thread) a nejaký iný zas na hlavnom vlákne. To je celé.
K hlavným vláknu si musíme povedať ešte jednu dôležitú vec. Hovorí sa mu tiež UI thread, pretože má na starosť užívateľské rozhranie. To už vieme. Dôležité ale je, že iba toto hlavné vlákno môže "na užívateľské rozhranie siahať".
Jednoducho nemôžeme meniť vlastnosti UI komponentov, animovať je alebo ovládať z ostatných vlákien. Aplikácia jednoducho spadne. Často vás ale Xcode upozorní práve na tento problém, takže nie je príliš zaludny na odhalenie.
DispatchQueue
Teória už bolo dosť, tak si poďme ukázať skutočný kód pre prácu s GCD. Najzákladnejšie prevedenie kódu na pozadí môže vyzerať takto:
DispatchQueue.global().async {
// náš kód
}
A to je celé! DispatchQueue
slúži na komunikáciu s GCD a
pomocou global().async
za nás automaticky prevedie akýkoľvek
kód vo vlákne na pozadí. Nás vlastne ani nezaujíma, aké vlákno to bude,
či si GCD vytvorí nové alebo využije nejaké už vytvorené. Proste
vykonáme kód na pozadí, nezablokuje užívateľské rozhranie a nenašteveme
užívateľa
DispatchQueue.main
Vyššie spomínaný kód je síce veľmi jednoduchý, v praxi ale veľakrát stačiť nebude. Často totiž chceme nejaké dáta stiahnuť a následne je užívateľovi zobraziť. A ako už vieme, manipulovať s užívateľským rozhraním môže iba hlavné vlákno aplikácie.
To nie je problém, jednoducho opäť využijeme DispatchQueue
,
avšak tentoraz DispatchQueue.main
a potrebné úpravy vykonáme
tu. Vyzerať to môže nasledovne:
DispatchQueue.global().async { // stažení obrázku DispatchQueue.main.async { // aktualizace UIImageView } }
Týmto sa "prepneme" späť na hlavnú vlákno a upravíme treba
UIImageView
, do ktorého načítame novo stiahnuté dáta.
QoS čiže kvalita služby
GCD tiež môžeme povedať, ako je pre nás vykonanie daného kódu
dôležité a ten sa podľa toho zariadi. Opäť sa používa metóda
global()
, ale s parametrom určujúcim práve QoS. Používajú sa
štyri voľby v závislosti na tom, čo chceme urobiť:
.userInteractive
- najvýkonnejší variant, používa sa v prípade interaktívnej práce s aplikáciou..userInitiated
- pre prevedenie akcií, ktoré si vyžiadal používateľ. Napríklad export dát z aplikácie, načítanie ďalších informácií.utility
- pre činnosti, ktoré môžu zabrať dlhší čas a nevyžaduje okamžitej prevedení. Často sa používa na sťahovanie / nahrávanie dát..background
- najnižšia priorita, typicky pre činnosti, o ktorých užívateľ nevie. To môže byť indexovanie alebo napr. Vykonávanie zálohy.
Tieto voľby potom použijeme nasledovne:
DispatchQueue.global(qos: .userInitiated).async { }
Môže byť lákavé vždy použiť najvýkonnejší
.userInteractive
, ale nerobte to. Ak totiž zrovna užívateľ bude
vo vašej aplikácii robiť niečo náročného a vy na pozadí spustíte treba
indexovanie s .userInteractive
, môžete aplikáciu ľahko výrazne
spomaliť.
Pozor na retain cycle
Teraz je potrebné vykonať dlhší odbočku od samotného GCD a prebrať
potrebné základy o tzv. Closure capturing. Už vieme, že closure
je jednoducho kus kódu, ktorý môžeme uložiť do premennej a tak ho
odovzdávať a spúšťať. Tak si môžeme pripraviť činnosti na vykonanie na
neskôr, napr. Obsluhu nejakej udalosti atď.
Problém môže nastať, keď naše closure potrebuje k fungovaniu externej
premennej, treba celý ViewController
, pretože potrebuje zavolať
niektorú z jeho metód. Dôjde ku spomínanému closure capturing, čo
znamená, že si closure
tieto premenné "chytí" a drží si ich,
aby ich mohla následne použiť.
Pozrime sa na nasledujúce kód:
var pocitadlo = 1 let closure = { let pocitadloPrictene = pocitadlo + 5 print("Výstup closure: \(pocitadloPrictene)") } print(pocitadlo) pocitadlo += 1 pocitadlo += 1 closure() print(pocitadlo)
Aký podľa vás bude výsledok? Koľko vypíše print()
schovaný v closure
? Ak ste odpovedali, že 6
, tak to
nie je správne. Správna odpoveď je 8
, pretože sa premenná
counter
pred zavolaním closure zmenila. Môžete si príklad
skúsiť v Playground.
Retain cycle
Closure capturing môže veľmi ľahko vytvoriť retain cycle, čo znamená,
že dva objekty drží referencie na seba a tým pádom nikdy nedôjde k ich
delokácia. Veľký počet takýchto objektov môže potom spôsobiť
vyčerpanie pamäte a pád aplikácie. Normálne je objekt dealokován
(uvoľnený z pamäte), akonáhle na neho neukazujú žiadne referencie.
Respektíve tie silné (čítajte predvolené). Vo Swiftu používame tiež
weak
referencie, ktoré sa v prípade delokácia nepočítajú.
Toto by sme mali poznať a ide len o opakovanie.
Retain cycle sa môže veľmi ľahko stať dosť nepríjemným problémom a
treba ho ani nemusíte objaviť. Riešenie je však jednoduché, nazýva sa weak
capturing. Skrátka povieme našej closure, aby capturing vykonávala cez
weak
referencie, vďaka čomu nemôže retain cycle nastať.
V kóde to vyzerá nasledovne:
DispatchQueue.global().async { [weak self] in }
V CLOSURES sú pred in
typicky vypísané parametre, ak je
closure prijíma. Do hranatých zátvoriek môžeme špecifikovať, akým
štýlom chceme capturing vykonať. V tomto prípade máme na self
iba weak
referenciu, takže pri použití je nutné použiť
self?
, Pretože self
môže byť nil
.
Podobne môžeme vykonať capturing s akoukoľvek ďalšie premennou, len pozor
na to, že weak
treba špecifikovať pre každú premennú v
hranatých zátvorkách, inak Swift použije tradičné strong capturing.
S [weak self]
sa nám pri použití zmení iba nutnosť využiť
?
operátor, pre prípad, že by už self
bolo
nil
. Kód pre obdobné obnovenie UIImageView
by teda
mohol vyzerať nasledovne:
DispatchQueue.global().async { [weak self] in self?.downloadData() DispatchQueue.main.async { // aktualizace UIImageView // [weak self] podruhé není nutné } }
Mohli by sme tiež využiť konštrukciu guard
a to
nasledovne:
DispatchQueue.global().async { [weak self] in guard self = self else { return } self.downloadData() }
Weak vs. unowned
Okrem weak
ste možno zahliadli tiež kľúčové slovíčko
unowned
. To sa používa rovnako, ale funguje podobne ako
!
s Optional
. K premenným môžeme pristupovať
rovnako, ako keby unowned
modifikátor nemali, ak je ale niektorá
nil
, tak nám aplikácie jednoducho spadne. Za seba by som
odporučil vždy používať weak
.
A sme u konca prvej lekcie. V tej ďalšej, PerformSelector (), run loop a paralelné cyklus vo Swift , si ukážeme druhú a
jednoduchšia možnosť, ako vykonať kód na pozadí. Vysvetlíme si, čo je
run loop v aplikácii, k čomu je dobré o ňom mať potuchy a ďalšie
špeciality DispatchQueue
.