Lekce 1 - Úvod do vícevláknových aplikací ve Swift
Vítejte u první lekce Swift kurzu, který vás naučí, jak ve vašich iOS, macOS či dalších aplikacích napsaných v programovacím jazyce Swift využít maximální potenciál zařízení. Současné iPhone i iPad modely disponují několika procesorovými jádry, nemluvně o strojích s macOS. Ve výchozím stavu ale náš program vždy využije pouze jedno jádro. To může znamenat třeba čtvrtinu dostupného výkonu.

Přeci mega výkon A12 Bionic nechcete nechat ležet ladem, ne?
Využití více vláken (multithreading) je ale důležité z mnohem podstatnějšího důvodu, než zapřáhnutí všech dostupných jader procesoru. Aplikace mají standardně jedno hlavní vlákno (označované jako Main nebo UI thread), které zkrátka provádí náš veškerý kód.
Toto hlavní vlákno má za úkol aplikaci spustit, vykreslit uživatelské rozhraní, provést naše metody a spoustu dalšího. Stejně tak uživatelské rozhraní následně obsluhuje, takže pokud například v iOS aplikaci chceme scrollovat komponentou TableView, na pozadí je to opět práce pro hlavní vlákno, aby vše korektně obstaralo.
Co myslíte, že se stane s aplikací, když bude toto hlavní vlákno zaneprázdněno čímkoliv jiným? Zkuste popřemýšlet...
Aplikace se jednoduše bude tvářit zaseknutá do doby,
než hlavní vlákno dokončí předchozí práci a vrátí se k obsluze
uživatelského rozhraní. Asi není třeba diskutovat nad tím, že tuto
situaci v aplikacích prostě mít nechceme. Uživatelům je nepříjemné,
když na ně aplikace nereaguje
Asi nejčastěji se nám podobná situace může stát, když budeme stahovat data z internetu. Klidně to při vývoji ani nemusíme zjistit, protože stahujeme pár kilobajtů JSON dat a máme vždy rychlý a spolehlivý internet. Hlavní vlákno tak data stáhne prakticky okamžitě k zaseknutí aplikace nedojde.
Jenže v praxi můžeme narazit na zahlcený server, kterému bude odpověď trvat třeba dvě sekundy. Nebo zrovna mobil uživatele bude připojen k přetížené WiFi či na EDGE mobilní připojení a rázem máme zaseknutou nebo zasekanou aplikaci (pokud stahujeme více dat).
Hello GCD!
Ve Swift není vícevláknové programování tak komplikované, jak může z
názvu znít. Ve spoustě jazyků musí programátoři vytvářet vlákna jako
objekty, pak se starat o jejich spuštění, dokončení a kdoví co ještě. Ve
Swift sice lze třídu Thread
vyhrabat, ale prakticky se
nepoužívá. Máme totiž mnohem lepší řešení.
Seznamte se s GCD, již brzy vašim pomocníkem pro krocení vláken. Za zkratkou se ukrývá Grand Central Dispatcher a jeho úkolem je spravovat vlákna a zbavit programátora složitostí. Ve většině případů budeme pouze GCD úkolovat, že určitý kód chceme provést na pozadí (background thread) a nějaký jiný zas na hlavním vlákně. To je celé.
K hlavním vláknu si musíme říci ještě jednu důležitou věc. Říká se mu také UI thread, protože má na starost uživatelské rozhraní. To již víme. Důležité ale je, že pouze toto hlavní vlákno může "na uživatelské rozhraní sahat".
Jednoduše nemůžeme měnit vlastnosti UI komponent, animovat je či ovládat z ostatních vláken. Aplikace prostě spadne. Často vás ale Xcode upozorní právě na tento problém, takže není příliš záludný na odhalení.
DispatchQueue
Teorie už bylo dost, tak si pojďme ukázat skutečný kód pro práci s GCD. Nejzákladnější provedení kódu na pozadí může vypadat takto:
DispatchQueue.global().async {
// náš kód
}
A to je celé! DispatchQueue
slouží ke komunikaci s GCD a
pomocí global().async
za nás automaticky provede jakýkoliv kód
ve vlákně na pozadí. Nás vlastně ani nezajímá, jaké vlákno to bude,
jestli si GCD vytvoří nové nebo využije nějaké již vytvořené. Prostě
provedeme kód na pozadí, nezablokujeme uživatelské rozhraní a nenašteveme
uživatele
DispatchQueue.main
Výše zmíněný kód je sice velmi jednoduchý, v praxi ale mnohdy stačit nebude. Často totiž chceme nějaká data stáhnout a následně je uživateli zobrazit. A jak již víme, manipulovat s uživatelským rozhraním může pouze hlavní vlákno aplikace.
To není problém, jednoduše opět využijeme DispatchQueue
,
ovšem tentokrát DispatchQueue.main
a potřebné úpravy provedeme
zde. Vypadat to může následovně:
DispatchQueue.global().async { // stažení obrázku DispatchQueue.main.async { // aktualizace UIImageView } }
Tímto se "přepneme" zpět na hlavní vlákno a upravíme třeba
UIImageView
, do kterého načteme nově stažená data.
QoS neboli Quality of Service
GCD také můžeme říci, jak je pro nás provedení daného kódu
důležité a ten se podle toho zařídí. Opět se používá metoda
global()
, ale s parametrem určujícím právě QoS. Používají
se čtyři volby v závislosti na tom, co chceme provést:
.userInteractive
- nejvýkonnější varianta, používá se v případě interaktivní práce s aplikací..userInitiated
- pro provedení akcí, které si vyžádal uživatel. Například export dat z aplikace, načtení dalších informací.utility
- pro činnosti, které mohou zabrat delší čas a nevyžaduje se okamžité provedení. Často se používá na stahování/nahrávání dat..background
- nejnižší priorita, typicky pro činnosti, o kterých uživatel neví. To může být indexování nebo např. provádění zálohy.
Tyto volby potom použijeme následovně:
DispatchQueue.global(qos: .userInitiated).async { }
Může být lákavé vždy použít nejvýkonnější
.userInteractive
, ale nedělejte to. Pokud totiž zrovna uživatel
bude ve vaší aplikaci dělat něco náročného a vy na pozadí spustíte
třeba indexování s .userInteractive
, můžete aplikaci snadno
výrazně zpomalit.
Pozor na retain cycle
Teď je třeba provést delší odbočku od samotného GCD a probrat nutné
základy o tzv. closure capturing. Již víme, že closure
je
jednoduše kus kódu, který můžeme uložit do proměnné a tak ho předávat
a spouštět. Tak si můžeme připravit činnosti k provedení na později,
např. obsluhu nějaké události apod.
Problém může nastat, když naše closure potřebuje k fungování externí
proměnné, třeba celý ViewController
, protože potřebuje
zavolat některou z jeho metod. Dojde ke zmiňovanému closure capturing, což
znamená, že si closure
tyto proměnné "chytne" a drží si je,
aby je mohla následně použít.
Podívejme se na následující 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)
Jaký podle vás bude výsledek? Kolik vypíše print()
schovaný v closure
? Pokud jste odpověděli, že 6
,
tak to není správně. Správná odpověď je 8
, protože se
proměnná counter
před zavoláním closure změnila. Můžete si
příklad zkusit v Playground.
Retain cycle
Closure capturing může velmi snadno vytvořit retain cycle, což znamená,
že dva objekty drží reference na sebe a tím pádem nikdy nedojde k jejich
dealokaci. Velký počet takových objektů může pak způsobit vyčerpání
paměti a pád aplikace. Normálně je objekt dealokován (uvolněn z paměti),
jakmile na něj neukazují žádné reference. Respektive ty silné (čtěte
výchozí). Ve Swiftu používáme také weak
reference, které se
v případě dealokace nepočítají. Toto bychom měli znát a jde jen o opakování.
Retain cycle se může velmi snadno stát dost nepříjemným problémem a
třeba ho ani nemusíte objevit. Řešení je ovšem snadné, nazývá se weak
capturing. Zkrátka řekneme naší closure, aby capturing prováděla přes
weak
reference, díky čemuž nemůže retain cycle nastat.
V kódu to vypadá následovně:
DispatchQueue.global().async { [weak self] in }
V closures jsou před in
typicky vypsané parametry, pokud je
closure přijímá. Do hranatých závorek můžeme specifikovat, jakým stylem
chceme capturing provést. V tomto případě máme na self
pouze
weak
referenci, takže při použití je nutné použít
self?
, protože self
může být nil
.
Podobně můžeme provést capturing s jakoukoliv další proměnnou, jen pozor
na to, že weak
je třeba specifikovat pro každou proměnnou v
hranatých závorkách, jinak Swift použije tradiční strong capturing.
S [weak self]
se nám při použití změní pouze nutnost
využít ?
operátor, pro případ, že by již self
bylo nil
. Kód pro obdobné obnovení UIImageView
by
tedy mohl vypadat následovně:
DispatchQueue.global().async { [weak self] in self?.downloadData() DispatchQueue.main.async { // aktualizace UIImageView // [weak self] podruhé není nutné } }
Mohli bychom také využít konstrukci guard
a to
následovně:
DispatchQueue.global().async { [weak self] in guard self = self else { return } self.downloadData() }
Weak vs. unowned
Kromě weak
jste možná zahlédli také klíčové slovíčko
unowned
. To se používá stejně, ale funguje podobně jako
!
s Optional
. K proměnným můžeme přistupovat
stejně, jako kdyby unowned
modifikátor neměly, pokud je ale
některá nil
, tak nám aplikace prostě spadne. Za sebe bych
doporučil vždy používat weak
.
A jsme u konce první lekce.
V další lekci, PerformSelector(), run loop a paralelní cyklus ve Swift, si ukážeme druhou a jednodušší možnost, jak
provést kód na pozadí. Vysvětlíme si, co je run loop v aplikaci, k čemu je
dobré o něm mít ponětí a další speciality DispatchQueue
.