een geheugenlek treedt op in een iOS-app wanneer geheugen dat niet meer in gebruik is niet goed wordt gewist en ruimte blijft innemen. Dit kan de prestaties van de app schaden en uiteindelijk, wanneer de app geen Beschikbaar geheugen meer heeft, zal het een crash veroorzaken. Om beter te begrijpen hoe geheugenlekken gebeuren, is het belangrijk om eerst te weten hoe iOS-apps hun geheugen beheren. Laten we eens kijken hoe je geheugenlekken in snelle sluitingen kunt voorkomen.
automatische referentietelling
in iOS wordt gedeeld geheugen beheerd door elk object bij te houden hoeveel andere objecten ernaar verwijzen. Zodra deze referentietelling 0 bereikt, wat betekent dat er geen verwijzingen meer zijn naar het object, kan het veilig uit het geheugen worden gewist. Zoals de naam zou impliceren, is deze referentietelling alleen nodig voor referentietypen, terwijl waardetypen geen geheugenbeheer vereisen. Referentietelling werd in het verleden handmatig gedaan door retain
op een object aan te roepen om zijn referentietelling te verhogen en vervolgens release
op het object aan te roepen om zijn referentietelling te verlagen. Deze code was bijna volledig ketelplaat, saai om te schrijven en fout gevoelig. Dus zoals de meeste ondergeschikte taken, het werd geautomatiseerd door middel van automatische referentie tellen (ARC) die de nodige te behouden en vrij te geven oproepen namens u bij het compileren maakt.
hoewel ARC meestal minder zorgen hoeft te maken over geheugenbeheer, kan het nog steeds snelle geheugenlekken creëren wanneer er cirkelreferenties zijn. Bijvoorbeeld, stel dat we een Person
klasse hebben die een eigenschap heeft voor een apartment
en de Apartment
klasse heeft een Person
eigenschap genaamd tenant
:
class Person { var name: String var age: Int init(name: String, age: Int) {…} var apartment: Apartment?}class Apartment { let unit: String init(unit: String) {…} var tenant: Person?}let person = Person(name: "Bob", age: 30)let unit4A = Apartment(unit: "4A")person.apartment = unit4Aunit4A.tenant = person
wanneer we een nieuwe Person
en Apartment
maken en deze toewijzen aan elkaars eigenschappen, verwijzen ze nu op een cirkelvormige manier naar elkaar. Circulaire verwijzingen kunnen ook gebeuren met meer dan twee objecten. In deze situatie bevatten zowel de Person
als de Apartment
een verwijzing naar de andere, dus in dit geval zal geen van beide ooit een referentietelling van 0 krijgen en elkaar in het geheugen houden, ook al hebben geen andere objecten een verwijzing naar een van hen. Dit staat bekend als een retain-cyclus en veroorzaakt een geheugenlek.
gerelateerd: drie kenmerken die echt Swifty zouden zijn
de manier waarop ARC omgaat met retain cycles is om verschillende soorten referenties te hebben: sterk, zwak en niet-eigendom. Sterke referenties zijn het soort waar we het al over hebben gehad, en deze referenties verhogen het aantal referenties van een object. Zwakke referenties, aan de andere kant, terwijl ze je nog steeds verwijzen naar het object, verhogen de referentietelling niet. Dus als we de eigenschap tenant
op Apartment
zouden nemen en het in plaats daarvan een zwakke referentie zouden maken, zou het onze retain-cyclus breken:
class Apartment { let unit: String init(unit: String) {…} weak var tenant: Person?}
nu houdt ons Person
object nog steeds zijn Apartment
in het geheugen, maar het omgekeerde is niet langer waar. Dus als de laatste sterke verwijzing naar de Person
weg is, zal zijn referentietelling dalen naar 0 en het zal zijn Apartment
vrijgeven, waarvan het referentietelling ook wordt gedaald naar 0, en ze kunnen beide correct worden gewist uit het geheugen.
in Swift, zwakke referenties moeten optioneel var
s zijn, omdat, als u geen verantwoordelijkheid neemt voor het bewaren van een object in het geheugen, u niet kunt garanderen dat het object het geheugen niet zal veranderen of verlaten. Dit is waar het derde type referentie in het spel komt. referenties zonder eigendom zijn als zwakke referenties, behalve dat ze niet-optioneel let
s kunnen zijn, maar deze moeten alleen worden gebruikt als u er zeker van bent dat het object nooit nil
mag zijn. Zoals force unwrapped optionals, zeg je tegen de compiler ” maak je geen zorgen, ik regel dit. Vertrouw me.”Maar net als zwakke referenties, de unowned referentie is niets te doen om het object in het geheugen te houden en als het laat het geheugen en je probeert om toegang te krijgen, uw app zal crashen. Nogmaals, net als force unwrapped optionals.
hoewel bewaarcycli gemakkelijk te zien zijn met twee objecten die naar elkaar wijzen, zijn ze moeilijker te zien wanneer sluitingen in Swift betrokken zijn, en dit is waar ik de meeste bewaarcycli heb zien gebeuren.
vermijden Houdcycli in sluitingen
het is belangrijk te onthouden dat sluitingen referentietypen zijn in Swift en kan leiden tot houdcycli net zo gemakkelijk, zo niet gemakkelijker, als klassen. Om een afsluiting later uit te voeren, moet het alle variabelen behouden die het nodig heeft om het te laten draaien. Op dezelfde manier als klassen, een sluiting vangt referenties als sterk standaard. Een retain cyclus met een sluiting zou er ongeveer zo uitzien:
class SomeObject { var aClosure = { self.doSomething() } ...}
verwant: Wanneer moet Ik gebruik maken van blokken en sluitingen of Afgevaardigden voor Callbacks?
in dit geval heeft de SomeObject
klasse een sterke verwijzing naar aClosure
en aClosure
heeft self
(de SomeObject
instantie) ook sterk gevangen. Dit is de reden waarom Swift je altijd self.
expliciet laat toevoegen terwijl je in een afsluiting zit om te voorkomen dat programmeurs per ongeluk self
vastleggen zonder het te beseffen, en dus hoogstwaarschijnlijk een retain-cyclus veroorzaakt.
om closure capture-variabelen als zwak of niet-eigendom te hebben, kunt u de closure-instructies geven over het vastleggen van bepaalde variabelen:
class SomeObject { var aClosure = { in self.doSomething() delegate?.doSomethingElse() } ...}
meer over sluiting syntaxis hier.
de meeste afsluitingsvoorbeelden die ik in tutorials of voorbeelden heb gezien lijken self
als unowned te vangen en noemen het een dag, omdat het vastleggen van self
als zwak het optioneel maakt, wat meer lastig kan zijn om mee te werken. Maar zoals we eerder hebben geleerd, is dit inherent onveilig omdat er een crash zal zijn als self
niet meer in het geheugen is. Dit is niet veel anders dan gewoon forceren uitpakken van al uw optionele variabelen, omdat je niet wilt om het werk te doen om ze veilig uit te pakken. Tenzij u er zeker van kunt zijn dat self
rond zal zijn zolang uw afsluiting is, moet u proberen om het zwak te vangen in plaats daarvan. Als u een niet-optionele zelf in uw sluiting nodig hebt, overweeg dan een if let
of guard let
te gebruiken om een sterke, niet-optionele self
in de sluiting te krijgen. Omdat je deze nieuwe, sterke referentie hebt gemaakt binnen de Swift closure, creëer je geen retain cycle omdat deze referentie aan het einde van de closure wordt vrijgegeven:
var aClosure = { in if let strongSelf = self { doSomethingWithNonOptional(strongSelf) doSomethingElseNonOptional(strongSelf) }}
of nog beter:
var aClosure = { in guard let strongSelf = self else { return } doSomethingWithNonOptional(strongSelf) doSomethingElseNonOptional(strongSelf)}
het vangen van Self sterk
hoewel het een goede gewoonte is om self
zwak te vangen in sluitingen, is het niet altijd nodig. Sluitingen die niet door de self
worden vastgehouden, kunnen deze sterk opvangen zonder een retain-cyclus te veroorzaken. Enkele veel voorkomende voorbeelden hiervan zijn:
werken met DispatchQueue
s in GCD
DispatchQueue.main.async { self.doSomething() // Not a retain cycle}
werken met UIView.animate(withDuration:)
UIView.animate(withDuration: 1) { self.doSomething() // Not a retain cycle}
de eerste is geen bewaarcyclus omdat self
de DispatchQueue.main
singleton niet behoudt. In het tweede voorbeeld is UIView.animate(withDuration:)
een klassemethode, die self
ook geen rol speelt bij het behouden.
het vastleggen van self
sterk in deze situaties zal geen retain-cyclus veroorzaken, maar het kan ook niet zijn wat u wilt. Bijvoorbeeld, teruggaan naar GCD:
DispatchQueue.main.asyncAfter(deadline: .now() + 60) { self.doSomething()}
deze sluiting zal niet langer duren dan 60 seconden en zal self
behouden totdat dit gebeurt. Dit kan het gedrag zijn dat je wilt, maar als je wilt dat self
in staat is om geheugen te verlaten gedurende deze tijd, zou het beter zijn om het zwak op te vangen en alleen te draaien als self
nog steeds rond is:
DispatchQueue.main.asyncAfter(deadline: .now() + 60) { in self?.doSomething()}
een andere interessante plaats waar self
niet sterk hoeft te worden vastgelegd is in lazy
variabelen, die geen sluitingen zijn, omdat ze eenmaal (of nooit) worden uitgevoerd en daarna worden vrijgegeven:
lazy var fullName = { return self.firstName + " " + self.lastName }()
als een lazy
variabele echter een closure is, moet deze zichzelf zwak vastleggen. Een goed voorbeeld hiervan komt uit de Swift Programming Language guide:
class HTMLElement { let name: String let text: String? lazy var asHTML: () -> String = { in if let text = self.text { return "<(self.name)>(text)</(self.name)>" } else { return "<(self.name) />" } }}
Dit is ook een goed voorbeeld van een instantie die redelijk is om een referentie zonder eigendom te gebruiken, aangezien de sluiting asHTML
zou moeten bestaan zolang de sluiting HTMLElement
bestaat, maar niet langer.
TL; DR
wanneer u met sluitingen in Swift werkt, moet u er rekening mee houden hoe u variabelen vastlegt, in het bijzonder self
s. als self
de afsluiting op enigerlei wijze behoudt, zorg er dan voor dat u deze zwak opneemt. Capture variabelen alleen als unowned wanneer u er zeker van kunt zijn dat ze in het geheugen zullen zijn wanneer de afsluiting wordt uitgevoerd, niet alleen omdat u niet wilt werken met een optionele self
. Dit zal u helpen geheugenlekken in snelle sluitingen te voorkomen, wat leidt tot betere app-prestaties.