19. Apr 2022iOS

Swift: Zlepšite správu pamäte – Strong, weak and unowned

V tomto článku si ukážeme a vysvetlíme, ako funguje správa pamäte v praxi a ako správne spravovať pamäťový priestor vo Swifte. Pochopením tohto mechanizmu môžeme ovplyvniť životnosť objektov v aplikácii.

Oleksandr LunevskyiiOS developer

Swift ako moderný programovací jazyk sa v podstate stará o správu pamäte v iOS aplikáciách prideľovaním a uvoľňovaním pamäte. Je to spôsobené mechanizmom nazývaným Automatické Počítanie Referencií alebo skrátene ARC.

Swift sa snaží predchádzať situáciám, ktoré môžu viesť k úniku pamäte. Môžeme si predstaviť scenár, kde sa objekty, ktoré zaberajú nejakú časť pamäte, nebudú alokovať, ale budú zaberať voľné miesto v pamäti, ktoré potrebujeme. V konečnom dôsledku kvôli použitiu veľkého množstva pamäte aplikácia jednoducho spadne.

Silné, slabé alebo nevlastnené referencie

Pri písaní nášho kódu sa často obávame prítomnosti retain cyklov v našom kóde. Veľmi často sa stretávame s nepochopením účelu použitia silných, slabých alebo nevlastných referencií. Podstatou používania týchto kľúčových slov je vyhnúť sa retain cyklom, ale niekedy úplne nerozumieme, kde a akú metódu použiť.

Aby sme vyriešili problém, keď Swift nevie, ktoré z referencií možno odstrániť a ktoré nie, existuje na to istý spôsob nazývaný silné a slabé referencie. Všetky vytvorené instančné referencie sú štandardne silné. V situácii, keď dva objekty ukazujú na seba silnými referenciami, Swift sa nevie rozhodnúť, ktorý z referencií možno odstrániť.

Na vyriešenie tohto problému je možné niektoré odkazy transformovať na slabé odkazy. Slabé referencie sú definované pomocou kľúčových slov Weak & Unowned.

Closure Escaping

V praxi najčastejšie pracujeme so spracovaním údajov zo servera. V tomto prípade sa všetci stretávame s closures častejšie. Práca s closures je dosť špecifická vzhľadom na to, že v tomto prípade môže dôjsť k úniku pamäte. Na  príkladoch sa pokúsime vysvetliť princíp interakcie s closures a správne používanie pamäte.

Closure môže zachytiť hodnotu zo svojho rozsahu tromi rôznymi spôsobmi: pomocou silných, slabých alebo nevlastnených referencií. Tieto kľúčové slová často používame v praxi, hlavne preto, aby sme sa vyhli silným referenčným cyklom.

Objekt existuje, pokiaľ si ho pamätáme.

Strong capturing

Prvým krokom je pochopiť pôvod silného odkazu. Zhruba, je to odkaz, ktorý chráni objekty, na ktoré odkazuje, pred uvoľnením zvyšovaním počtu zachovaní. Ako už vieme, pokiaľ má jeden objekt silný odkaz na iný objekt, nebude uvoľnený. Toto je dôležité mať na pamäti, aby ste pochopili použitie referenčných typov. Dá sa povedať, že silné referencie sa v jazyku Swift používajú takmer všade.

Kým explicitne nešpecifikujeme metódu zachytávania, Swift používa zachytávanie Strong. To znamená, že closure zachytáva použité externé hodnoty (hodnoty zo svojho rozsahu) a nikdy nedovolí ich uvoľnenie.

‍DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
		print(self.greetingValue)
}

V našom prípade je konštanta použitá vo vnútri closure, čo znamená, že Swift automaticky zabezpečí jej existenciu, pokiaľ bude existovať samotný closure. Takto funguje zachytenie Strong.

Príklad z projektu:

UIView.animate(withDuration: 0.2) {
		self.collectionView.contentInset.bottom = self.collectionViewBottomInset
}

Weak capturing

Swift nám dáva možnosť vytvoriť typ zachytávania, aby sme presne definovali, ako budú použité hodnoty zachytené. Alternatívou k silnému zachyteniu je slabé.

Slabá referencia je ukazovateľ na objekt, ktorý umožňuje uvoľnenie objektu pomocou ARC. Slabá referencia tiež resetuje ukazovateľ na objekt, keď bol úspešne uvoľnený z pamäte. To znamená, že pri prístupe k slabej referencii bude výsledkom buď objekt, alebo nula.

V Swifte sú všetky slabé referencie voliteľné. Vychádza to zo skutočnosti, že voliteľný odkaz sa môže zmeniť a zmení sa na nulu, keď sa na objekt už silne neodkazuje. Jeho použitie vedie k nasledujúcim výsledkom:

  • slabé zachytené hodnoty nie sú držané cez closure, a preto môžu byť uvoľnené a transformované na nulu
  • na základe prvého bodu budú slabé zachytené hodnoty v Swift vždy voliteľné

Najvhodnejším miestom pre slabé premenné je miesto, kde môže nastať cyklus uchovávania. Stáva sa to vtedy, keď majú dva objekty na seba silné odkazy. Ak je premenná deklarovaná mimo rozsahu uzáveru, odkaz na premennú v rámci rozsahu uzáveru vytvára silný odkaz na daný objekt. Takže upravíme náš príklad, aby sme použili slabé zachytenie a uvidíme, čo presne sa zmení:

DispatchQueue.main.asyncAfter(deadline: .now() + 4) { [weak self] in
		print(self?.greetingValue ?? "Hello World")
}

[weak self] poskytuje capture type. Toto je časť syntaxe capture, kde definujeme spôsob, akým by sa mali zachytávať hodnoty. Tu hovoríme, že self (náš ViewController) by mal byť zachytený slabo, to znamená, že hodnota môže byť kedykoľvek zmenená na nulu, takže môžeme definovať predvolenú hodnotu - "Hello World". Týmto postupom sme prelomili silný referenčný cyklus.

Príklad z projektu:

DispatchQueue.main.async { [weak self] in
		self?.showMatchDetail(with: deepLinkValue.itemId)
}

Unowned capturing

Alternatívou slabého capture je capture bez vlastníctva. Princíp fungovania slabých a nevlastnených referencií je podobný, ale nie rovnaký. Nevlastnené referencie, rovnako ako tie slabé, nezvyšujú počet zachovania objektu.

Rovnako ako slabé referencie, aj nevlastnené referencie majú svoje výhody: umožňujú nemennosť a keďže ich nemožno manuálne nastaviť na nulu, náš kód nebude fungovať v neočakávané spôsoby.

Použitie unowned capturing je úplne bezpečné, iba ak sa používa správne.

‍profileButton.publisher(for: .touchUpInside)
		.sink { [unowned self] _ in showProfile() }
		.store(in: &cancellables)

Ako vidíme, rozdiel je v tom, že v Swift nie je neznáma referencia voliteľná. To uľahčuje správu objektov s neznámou referenciou. Ale ako vieme, jednoduchá cesta nie je vždy tá správna. Tento princíp je veľmi podobný použitiu force-unwrap s voliteľnými hodnotami. Rovnako ako pri vynútenom rozbalení, ak sme si 100% istí, že referencia nebude nulová, možno použiť unowned. Ak nie, tak slabý.

Príklad z projektu:

ErrorHandler.showCommonEmptyView(on: error, in: self) { [unowned self] in
		refreshCart()
}

Záver

Bolo by veľmi dobré, keby sa vývojári zaoberali neobmedzenou pamäťou a nikdy sa nebáli o jej racionálne využitie. Žiaľ, zďaleka to tak nie je a sme nútení správať sa ako dočasní používatelia pamäte, použiť ju a následne vrátiť späť...

Ďakujem za prečítanie a dúfam, že ste sa naučili niečo nové a užitočné pre seba.

Oleksandr LunevskyiiOS developer