2. diel - PerformSelector (), run loop a paralelné cyklus vo Swift
V minulej lekcii, Úvod do viacvláknových aplikácií vo Swift , sme si vysvetlili prečo multithreading
potrebujeme. Tiež už vieme, že by sme pre neho mali využívať primárne GCD
a zabudnúť, že existuje niečo ako trieda Thread
, ktorú
môžeme využiť a spustiť pomocou nej niečo na pozadí.
Za dobu, čo sa iOS venujem, som prístup pomocou
Thread
nikdy nestretol a často blogové príspevky o GCD
skúsených vývojárov priamo varovali pred používaním
Thread
.
PerformSelector ()
Existuje ešte jedna alternatívna možnosť, ako na multithreading vo Swift.
A je veľmi, veľmi jednoduchá. Skrýva sa v globálnych metódach
performSelector()
, ktorých signatúra určí, či sa vykonajú na
pozadí alebo na hlavnom vlákne.
Môžeme si rovno ukázať príklad na kódu:
performSelector(inBackground: #selector(fetchData), with: nil)
Pretože používame #selector
, je nutné, aby volané metóda,
v tomto prípade fetchData()
, bola označená modifikátorom
@objc
. Ten ju sprístupní pre Objective-C runtime, teda operačný
systém a ten sa o zavolanie postará. To si hneď ukážeme.
Hoci sa jedná o riešenie na jeden riadok kódu, nie je príliš
flexibilný. Posielanie parametrov nie je priamočiare a ak chceme s výsledkom
pracovať na hlavnom vlákne, musíme toto "prepnutie" umiestniť do volané
metódy. fetchData()
by mohlo teda vyzerať nasledovne:
@objc func fetchData() { let newData = apiService.getNewData() tableView.performSelector(onMainThread: #selector(UITableView.reloadData), with: nil, waitUntilDone: false) }
V tejto fiktívnej ukážke slúži metóda fetchData()
k
stiahnutiu dát z nejakej webovej služby a tieto dáta chceme užívateľovi
následne ukázať v komponente UITableView
, takže ju musíme
aktualizovať. Pretože sa jedná o prácu s UI, je potrebné dostať sa späť
na hlavnú vlákno. Volanie performSelector()
je kvapku odlišné,
pretože nevoláme metódu z našej triedy (teda nášho ViewControlleru), ale
chceme zavolať metódu komponenty UITableView
, takže
performSelector()
voláme na tejto komponente a pomocou
#selector
vyberieme metódu pre refresh dát. Fungovať to bude
korektne.
Teraz si spomeňme na požiadavku s modifikátorom
@objc
. Bez neho nebude #selector
fungovať. Akonáhle
by sme tak chceli volať metódu, ktorá nie je naša (takže sa nedá
modifikátor pridať) a alebo ho už neobsahuje, tak tento prístup fungovať
nebude. performSelector()
môže byť fajn voľbou pre jednoduché
prevedenie metódy na inom ako hlavnom vlákne. Ja by som odporučil využívať
skôr služby DispatchQueue
, častejšie si ju vďaka tomu
pripomeniete a ponúka mnohonásobne väčšiu flexibilitu.
Run loop
Keď už sa bavíme o DispatchQueue
a
performSelector()
, vysvetlíme si rovno drobnosť zvanú
application run loop. Run loop si môžete predstaviť ako nekonečný cyklus,
ktorý beží spoločne s aplikáciou s vykonáva všetok kód na hlavnom
vlákne.
Prečo o tom hovoríme, keď sme mohli až do teraz nerušene programovať aplikácie, bez toho, aby nás run loop zaujímal? Občas sa pri riešení problémov hodí vedieť, že niečo také existuje. Ak napr. Dôjde ku stlačení tlačidla, tak pre užívateľov a aj programátora je spustenie akcie okamžité, pokiaľ nejde o nejaký asynchrónne kód. Vo vnútri aplikácie sa ale deje niečo trochu iné. Kód naviazaný na tlačidlo pobeží až v novom "kole" run loop. Vo zvyšku súčasného, kedy bolo tlačidlo spustené, totiž okrem iného dôjde k dokreslenie animácie stlačení tlačidla. V opačnom prípade by mohlo dôjsť k zaseknutiu.
Teraz sa práve dostávame k DispatchQueue
a
performSelector()
. Ich okamžité varianty totiž spustí
akýkoľvek kód až v nasledujúcom "kole" run loop. Môže to vyzerať
nasledovne:
DispatchQueue.main.async {
// kód poběží v novém run loop
}
Ukážme si aj príklad pre performSelector()
, metóda
someWork()
opäť pobeží až v nasledujúcom run loop:
performSelector(onMainThread: #selector(someWork), with: nil, waitUntilDone: false)
Run loop sa tu venujeme primárne z dôvodu, aby ste minimálne tušili, o
čo sa jedná a mali predstavu, prečo treba cudzí kód používa
DispatchQueue.main.async
, aj keď už beží na hlavnom
vlákne.
S týmto problémom sa môžeme často stretnúť u animácií, ktoré bežia
po načítaní aplikácie. Trebárs som narazil na situáciu, že som komponente
nastavil alpha = 0
v Interface Builder, aby som mohol animovať
fade-in efekt pri zapnutí aplikácie a túto animáciu spustil v metóde
viewDidAppear()
. Môže sa stať, že na nastavenie pôvodnej
alpha
na nula ešte nedošlo, UIKit uvidí, že nemá čo
animovať, pretože komponenta má stále hodnotu alpha
na
1
(teda plne viditeľná) a animácie ju má posunúť do
rovnakého stavu, takže animáciu preskočí. Nie je sranda niečo také
hľadať. A práve tu vám posunutie kódu na ďalší run loop pomôže.
Prevedenie kódu po uplynulej dobe
Občas v aplikáciách potrebujeme vykonať nejaký kód po určitom časovom intervale.
DispatchQueue
DispatchQueue
nám okrem iného ponúka jednoduchý mechanizmus,
ako niečo vykonať až po uplynutí stanovenej doby. Slúži na to metóda
asyncAfter()
, čo je prakticky async
, ale s parametrom
určujúcim ako dlho sa má pred vykonaním čakať.
Jej použitie môže vyzerať nasledovne:
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { print("Uběhly 2 vteřiny!") }
Okrem .seconds
možno použiť aj .nanoseconds
,
.microseconds
a .miliseconds
. Alebo môžeme napísať
jednoducho .now() + 0.5
, kde 0.5
je počet sekúnd.
Tento zápis je častý, .seconds
je ale bez premýšľania oveľa
jasnejšie. Samozrejme asyncAfter()
možno použiť aj na
akékoľvek inom queue, nemusí ísť nutne o main
.
Perform ()
Využiť môžeme tiež metódu perform()
a v parametri zadať v
sekundách, ako dlho má pred vykonaním danej metódy čakať. Nevýhodou je,
že musíme mať pripravenú @objc
metódu, zatiaľ čo
asyncAfter()
na DispatchQueue
zvládne pracovať s
akýmkoľvek kódom.
Vykonanie metódy po uplynulom čase s perform()
môže vyzerať
nasledovne:
perform(#selector(printMessage), with: nil, afterDelay: 2) @objc func printMessage() { print("Uběhly 2 vteřiny!") }
Paralelné cyklus jednoducho
DispatchQueue
nám ponúka jednoduchú možnosť, ako
ľubovoľný počet iterácií vykonať paralelne. Príslušná metóda sa volá
concurrentPerform()
a ako parameter očakáva iba počet
iterácií. K číslu iterácie navyše dostaneme prístup v closure, takže
môžeme ľahko existujúce for
cyklus previesť na
concurrentPerform
.
Zápis vyzerá napríklad takto:
DispatchQueue.concurrentPerform(iterations: 100) { (i) in print(i) }
concurrentPerform()
môže byť skvelé v situáciách, kedy na
hlavnom vlákne potrebujeme vykonať zložitejšie for
cyklus,
ktorý má buď veľa iterácií alebo je každá iterácia náročnejšieho
charakteru. Pomocou tejto metódy automaticky využijete maximum možností
procesora.
Len pozor na prípad, kedy by ste v opakovaniach potrebovali pristupovať k zdieľaným premenným, pretože to bude brzdiť, pretože vlákna budú musieť čakať, až dôjde k ich uvoľneniu.
Týmto máme teoretickú časť viacvláknového programovania vo Swiftu za sebou a môžeme sa vrhnúť na praktickú ukážku v ďalšej lekcii, Vytvorenie iOS aplikácia pre demonštráciu GCD .