IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 2 - PerformSelector(), run loop a paralelní cyklus ve Swift

V minulé lekci, Úvod do vícevláknových aplikací ve Swift, jsme si vysvětlili proč potřebujeme multithreading. Také již víme, že bychom pro něj měli využívat primárně GCD a zapomenout, že existuje něco jako třída Thread, kterou můžeme využít a spustit pomocí ní něco na pozadí.

Dnes budeme pokračovat se základními konstrukcemi okolo paralelního programování ve Swift.

Za dobu, co se iOS věnuji, jsem přístup pomocí Thread nikdy nepotkal a často blogové příspěvky o GCD zkušených vývojářů přímo varovaly před používáním Thread.

performSelector()

Existuje ještě jedna alternativní možnost, jak na multithreading ve Swift. A je velmi, velmi jednoduchá. Skrývá se v globálních metodách performSelector(), jejichž signatura určí, jestli se provedou na pozadí nebo na hlavním vlákně.

Můžeme si rovnou ukázat příklad na kódu:

performSelector(inBackground: #selector(fetchData), with: nil)

Protože používáme #selector, je nutné, aby volaná metoda, v tomto případě fetchData(), byla označena modifikátorem @objc. Ten ji zpřístupní pro Objective-C runtime, tedy operační systém a ten se o zavolání postará. To si hned ukážeme.

Ačkoliv se jedná o řešení na jeden řádek kódu, není příliš flexibilní. Posílání parametrů není přímočaré a pokud chceme s výsledkem pracovat na hlavním vlákně, musíme toto "přepnutí" umístit do volané metody. fetchData() by mohlo tedy vypadat následovně:

@objc func fetchData() {
        let newData = apiService.getNewData()
        tableView.performSelector(onMainThread: #selector(UITableView.reloadData), with: nil, waitUntilDone: false)
}

V této fiktivní ukázce slouží metoda fetchData() ke stažení dat z nějaké webové služby a tato data chceme uživateli následně ukázat v komponentě UITableView, takže ji musíme aktualizovat. Protože se jedná o práci s UI, je třeba dostat se zpět na hlavní vlákno. Volání performSelector() je kapku odlišné, protože nevoláme metodu z naší třídy (tedy našeho ViewControlleru), ale chceme zavolat metodu komponenty UITableView, takže performSelector() voláme na této komponentě a pomocí #selector vybereme metodu pro refresh dat. Fungovat to bude korektně.

Teď si vzpomeňme na požadavek s modifikátorem @objc. Bez něj nebude #selector fungovat. Jakmile bychom tak chtěli volat metodu, která není naše (takže se nedá modifikátor přidat) a nebo ho již neobsahuje, tak tento přístup fungovat nebude. performSelector() může být fajn volbou pro jednoduché provedení metody na jiném než hlavním vlákně. Já bych doporučil využívat spíše služby DispatchQueue, častěji si ji díky tomu připomenete a nabízí mnohonásobně větší flexibilitu.

Run loop

Když už se bavíme o DispatchQueue a performSelector(), vysvětlíme si rovnou drobnost zvanou application run loop. Run loop si můžete představit jako nekonečný cyklus, který běží společně s aplikací s provádí veškerý kód na hlavním vlákně.

Proč o tom mluvíme, když jsme mohli až do teď nerušeně programovat aplikace, aniž by nás run loop zajímal? Občas se při řešení problémů hodí vědět, že něco takového existuje. Když např. dojde ke stisknutí tlačítka, tak pro uživatele a i programátora je spuštění akce okamžité, pokud nejde o nějaký asynchronní kód. Uvnitř aplikace se ale děje něco trochu jiného. Kód navázaný na tlačítko poběží až v novém "kole" run loop. Ve zbytku současného, kdy bylo tlačítko spuštěno, totiž mimo jiné dojde k dokreslení animace stisku tlačítka. V opačném případě by mohlo dojít k zaseknutí.

Teď se právě dostáváme k DispatchQueue a performSelector(). Jejich okamžité varianty totiž spustí jakýkoliv kód až v následujícím "kole" run loop. Může to vypadat následovně:

DispatchQueue.main.async {
            // kód poběží v novém run loop
}

Ukažme si i příklad pro performSelector(), metoda someWork() opět poběží až v následujícím run loop:

performSelector(onMainThread: #selector(someWork), with: nil, waitUntilDone: false)

Run loop se tu věnujeme primárně z důvodu, abyste minimálně tušili, o co se jedná a měli představu, proč třeba cizí kód používá DispatchQueue.main.async, i když už běží na hlavním vlákně.

S tímto problémem se můžeme často setkat u animací, které běží po načtení aplikace. Třeba jsem narazil na situaci, že jsem komponentě nastavil alpha = 0 v Interface Builder, abych mohl animovat fade-in efekt při zapnutí aplikace a tuto animaci spustil v metodě viewDidAppear(). Může se stát, že k nastavení původní alpha na nula ještě nedošlo, UIKit uvidí, že nemá co animovat, protože komponenta má stále hodnotu alpha na 1 (tedy plně viditelná) a animace ji má posunout do stejného stavu, takže animaci přeskočí. Není legrace něco takového hledat. A právě zde vám posunutí kódu na další run loop pomůže.

Provedení kódu po uplynulé době

Občas v aplikacích potřebujeme provést nějaký kód po určitém časovém intervalu.

DispatchQueue

DispatchQueue nám mimo jiné nabízí jednoduchý mechanismus, jak něco provést až po uplynutí stanovené doby. Slouží k tomu metoda asyncAfter(), což je prakticky async, ale s parametrem určujícím jak dlouho se má před provedením čekat.

Její použití může vypadat následovně:

DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
      print("Uběhly 2 vteřiny!")
}

Kromě .seconds lze použít také .nanoseconds, .microseconds a .miliseconds. Nebo můžeme napsat jednoduše .now() + 0.5, kde 0.5 je počet sekund. Tento zápis je častý, .seconds je ale bez přemýšlení mnohem jasnější. Samozřejmě asyncAfter() lze použít také na jakékoliv jiném queue, nemusí jít nutně o main.

perform()

Využít můžeme rovněž metodu perform() a v parametru zadat v sekundách, jak dlouho má před provedením dané metody čekat. Nevýhodou je, že musíme mít připravenou @objc metodu, zatímco asyncAfter() na DispatchQueue zvládne pracovat s jakýmkoliv kódem.

Provedení metody po uplynulém čase s perform() může vypadat následovně:

perform(#selector(printMessage), with: nil, afterDelay: 2)

@objc func printMessage() {
        print("Uběhly 2 vteřiny!")
}

Paralelní cyklus jednoduše

DispatchQueue nám nabízí snadnou možnost, jak libovolný počet iterací provést paralelně. Příslušná metoda se jmenuje concurrentPerform() a jako parametr očekává pouze počet iterací. K číslu iterace navíc dostaneme přístup v closure, takže můžeme snadno existující for cyklus převést na concurrentPerform.

Zápis vypadá třeba takto:

DispatchQueue.concurrentPerform(iterations: 100) { (i) in
      print(i)
}

concurrentPerform() může být skvělé v situacích, kdy na hlavním vláknu potřebujeme provést složitější for cyklus, který má buď mnoho iterací nebo je každá iterace náročnějšího charakteru. Pomocí této metody automaticky využijete maximum možností procesoru.

Jen pozor na případ, kdy byste v iteracích potřebovali přistupovat ke sdíleným proměnným, protože to bude brzdit, jelikož vlákna budou muset čekat, až dojde k jejich uvolnění.

Tímto máme teoretickou část vícevláknového programování ve Swiftu za sebou.

Na praktickou ukázku se vrhneme v další lekci, Vytvoření iOS aplikace pro demonstraci GCD.


 

Předchozí článek
Úvod do vícevláknových aplikací ve Swift
Všechny články v sekci
Paralelní programování a vícevláknové aplikace ve Swift
Přeskočit článek
(nedoporučujeme)
Vytvoření iOS aplikace pro demonstraci GCD
Článek pro vás napsal Filip Němeček
Avatar
Uživatelské hodnocení:
1 hlasů
Autor se věnuje vývoji iOS aplikací (občas macOS)
Aktivity