In einer iOS-App tritt ein Speicherleck auf, wenn nicht mehr verwendeter Speicher nicht ordnungsgemäß gelöscht wird und weiterhin Speicherplatz beansprucht. Dies kann die App-Leistung beeinträchtigen und schließlich zu einem Absturz führen, wenn der App der verfügbare Speicher ausgeht. Um besser zu verstehen, wie Speicherlecks auftreten, ist es wichtig zu wissen, wie iOS-Apps ihren Speicher verwalten. Werfen wir einen Blick darauf, wie Speicherlecks in Swift-Schließungen verhindert werden können.

Automatische Referenzzählung

In iOS wird der gemeinsam genutzte Speicher verwaltet, indem jedes Objekt verfolgt, wie viele andere Objekte einen Verweis darauf haben. Sobald dieser Referenzzähler 0 erreicht hat, was bedeutet, dass keine Referenzen mehr auf das Objekt vorhanden sind, kann es sicher aus dem Speicher gelöscht werden. Wie der Name schon sagt, ist diese Referenzzählung nur für Referenztypen erforderlich, während Werttypen keine Speicherverwaltung erfordern. Die Referenzzählung wurde in der Vergangenheit manuell durchgeführt, indem retain für ein Objekt aufgerufen wurde, um die Referenzzählung zu erhöhen, und dann release für das Objekt aufgerufen wurde, um die Referenzzählung zu verringern. Dieser Code war fast ausschließlich Kesselplatte, langweilig zu schreiben und fehleranfällig. Wie die meisten einfachen Aufgaben wurde es durch die automatische Referenzzählung (ARC) automatisiert, die zur Kompilierungszeit die erforderlichen Retain- und Release-Aufrufe in Ihrem Namen vornimmt.

Obwohl ARC die Notwendigkeit, sich um die Speicherverwaltung zu kümmern, größtenteils verringert hat, kann es immer noch zu schnellen Speicherlecks kommen, wenn zirkuläre Referenzen vorhanden sind. Angenommen, wir haben eine Person -Klasse mit einer Eigenschaft für eine apartment und die Apartment -Klasse hat eine Person -Eigenschaft mit dem Namen 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

Wenn wir eine neue Person und Apartment erstellen und sie den Eigenschaften der anderen zuweisen, verweisen sie jetzt kreisförmig aufeinander. Zirkelverweise können auch bei mehr als zwei Objekten vorkommen. In dieser Situation halten sowohl Person als auch Apartment einen Verweis auf den anderen, so dass in diesem Fall keiner jemals auf einen Referenzzähler von 0 kommt und sich gegenseitig im Speicher hält, obwohl keine anderen Objekte Verweise auf eines von ihnen haben. Dies wird als Retain-Zyklus bezeichnet und verursacht einen Speicherverlust.

Related: Drei Funktionen, die wirklich schnell wären

Die Art und Weise, wie ARC mit Retain-Zyklen umgeht, besteht darin, verschiedene Arten von Referenzen zu haben: stark, schwach und nicht besessen. Starke Referenzen sind die Art, über die wir bereits gesprochen haben, und diese Referenzen erhöhen die Referenzanzahl eines Objekts. Schwache Referenzen hingegen erhöhen die Referenzanzahl nicht, während Sie weiterhin auf das Objekt verweisen. Wenn wir also die Eigenschaft tenant auf Apartment nehmen und sie stattdessen zu einer schwachen Referenz machen, würde dies unseren Aufbewahrungszyklus unterbrechen:

class Apartment { let unit: String init(unit: String) {…} weak var tenant: Person?}

Jetzt hält unser Person -Objekt immer noch Apartment im Speicher, aber das Gegenteil ist nicht mehr der Fall. Wenn also der letzte starke Verweis auf Person weg ist, sinkt sein Referenzzähler auf 0 und er gibt seinen Apartment frei, dessen Referenzzähler ebenfalls auf 0 fallen gelassen wird, und beide können korrekt aus dem Speicher gelöscht werden.

In Swift müssen schwache Referenzen optional var sein, da Sie nicht garantieren können, dass sich das Objekt nicht ändert oder den Speicher verlässt, wenn Sie keine Verantwortung für das Speichern eines Objekts übernehmen. Hier kommt die dritte Art der Referenz ins Spiel. nicht besessene Referenzen sind wie schwache Referenzen, außer dass sie nicht optionale let s sein können, aber diese sollten nur verwendet werden, wenn Sie sicher sind, dass das Objekt niemals nil sein sollte. Wie force unwrapped Optionals sagen Sie dem Compiler: „Keine Sorge, ich habe das verstanden. Vertrau mir.“ Aber wie schwache Referenzen tut die nicht besessene Referenz nichts, um das Objekt im Speicher zu halten, und wenn es den Speicher verlässt und Sie versuchen, darauf zuzugreifen, stürzt Ihre App ab. Wieder, genau wie Force unwrapped optionals.

Während Retain-Zyklen leicht zu sehen sind, wenn zwei Objekte aufeinander zeigen, sind sie schwieriger zu sehen, wenn Closures in Swift beteiligt sind, und hier habe ich die meisten Retain-Zyklen gesehen.

Vermeiden von Retain-Zyklen in Closures

Es ist wichtig, sich daran zu erinnern, dass Closures in Swift Referenztypen sind und Retain-Zyklen genauso leicht, wenn nicht sogar leichter verursachen können wie Klassen. Damit ein Abschluss später ausgeführt werden kann, muss er alle Variablen beibehalten, die er zum Ausführen benötigt. Ähnlich wie bei Klassen erfasst ein Abschluss Referenzen standardmäßig als stark. Ein Retain-Zyklus mit einem closure würde ungefähr so aussehen:

class SomeObject { var aClosure = { self.doSomething() } ...}

Related: Wann sollte ich Blöcke und Closures oder Delegaten für Rückrufe verwenden?

Neuer Call-to-Action

In diesem Fall hat die SomeObject -Klasse einen starken Verweis auf aClosure und aClosure hat auch self (die SomeObject -Instanz) stark erfasst. Aus diesem Grund müssen Sie bei Swift immer explizit self. hinzufügen, um zu verhindern, dass Programmierer versehentlich self erfassen, ohne es zu merken, und daher höchstwahrscheinlich einen Beibehaltungszyklus verursachen.

Damit ein Abschluss Variablen als schwach oder nicht besessen erfasst, können Sie dem Abschluss Anweisungen zum Erfassen bestimmter Variablen geben:

class SomeObject { var aClosure = { in self.doSomething() delegate?.doSomethingElse() } ...}

Mehr zur Schließungssyntax hier .

Die meisten Schließungsbeispiele, die ich in Tutorials oder Beispielen gesehen habe, scheinen self als nicht besessen zu erfassen und nennen es einen Tag, da das Erfassen von self als schwach es optional macht, was unbequemer sein kann. Aber wie wir bereits gelernt haben, ist dies von Natur aus unsicher, da es zu einem Absturz kommt, wenn self nicht mehr im Speicher ist. Dies ist nicht viel anders, als nur das Auspacken aller optionalen Variablen zu erzwingen, da Sie nicht die Arbeit zum sicheren Auspacken ausführen möchten. Wenn Sie nicht sicher sein können, dass self so lange vorhanden ist, wie Ihr Verschluss ist, sollten Sie stattdessen versuchen, ihn schwach zu erfassen. Wenn Sie ein nicht optionales Selbst innerhalb Ihres Verschlusses benötigen, sollten Sie ein if let oder guard let verwenden, um ein starkes, nicht optionales self innerhalb des Verschlusses zu erhalten. Da Sie diese neue, starke Referenz innerhalb des Swift-Abschlusses erstellt haben, erstellen Sie keinen Aufbewahrungszyklus, da diese Referenz am Ende des Abschlusses freigegeben wird:

var aClosure = { in if let strongSelf = self { doSomethingWithNonOptional(strongSelf) doSomethingElseNonOptional(strongSelf) }}

oder noch besser:

var aClosure = { in guard let strongSelf = self else { return } doSomethingWithNonOptional(strongSelf) doSomethingElseNonOptional(strongSelf)}

Selbst stark erfassen

Obwohl es eine gute Praxis ist, self in Verschlüssen schwach zu erfassen, ist dies nicht immer erforderlich. Verschlüsse, die nicht von self beibehalten werden, können es stark erfassen, ohne einen Beibehaltungszyklus zu verursachen. Einige häufige Beispiele hierfür sind:

Arbeiten mit DispatchQueue s in GCD

DispatchQueue.main.async { self.doSomething() // Not a retain cycle}

Arbeiten mit UIView.animate(withDuration:)

UIView.animate(withDuration: 1) { self.doSomething() // Not a retain cycle}

Der erste ist kein Retain-Zyklus, da self den DispatchQueue.main Singleton nicht beibehält. Im zweiten Beispiel ist UIView.animate(withDuration:) eine Klassenmethode, an deren Beibehaltung self ebenfalls nicht beteiligt ist.

Das Erfassen von self Daten in diesen Situationen führt nicht zu einem Beibehaltungszyklus, ist aber möglicherweise auch nicht das, was Sie möchten. Zum Beispiel zurück zu GCD:

DispatchQueue.main.asyncAfter(deadline: .now() + 60) { self.doSomething()}

Diese Schließung wird nicht für weitere 60 Sekunden ausgeführt und behält self, bis dies der Fall ist. Dies mag das gewünschte Verhalten sein, aber wenn Sie möchten, dass self während dieser Zeit den Speicher verlassen kann, ist es besser, ihn schwach zu erfassen und nur auszuführen, wenn self noch vorhanden ist:

DispatchQueue.main.asyncAfter(deadline: .now() + 60) { in self?.doSomething()}

Ein weiterer interessanter Ort, an dem self nicht stark erfasst werden müsste, sind lazy Variablen, die keine Schließungen sind, da sie einmal (oder nie) ausgeführt und anschließend freigegeben werden:

lazy var fullName = { return self.firstName + " " + self.lastName }()

Wenn eine Variable lazy jedoch ein Abschluss ist, müsste sie sich selbst schwach erfassen. Ein gutes Beispiel dafür ist der 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) />" } }}

Dies ist auch ein gutes Beispiel für eine Instanz, die sinnvoll ist, eine nicht eigene Referenz zu verwenden, da die asHTML Schließung so lange existieren sollte wie die HTMLElement, aber nicht mehr.

TL; DR

Wenn Sie mit Verschlüssen in Swift arbeiten, achten Sie darauf, wie Sie Variablen erfassen, insbesondere selfs. Wenn self den Verschluss in irgendeiner Weise beibehält, stellen Sie sicher, dass Sie ihn schwach erfassen. Erfassen Sie Variablen nur dann als nicht besessen, wenn Sie sicher sein können, dass sie sich bei jeder Ausführung des Abschlusses im Speicher befinden, nicht nur, weil Sie nicht mit einem optionalen self arbeiten möchten. Auf diese Weise können Sie Speicherlecks bei Swift-Schließungen verhindern, was zu einer besseren App-Leistung führt.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.