Swift gegen Kotlin | Komentor

Müllabfuhr

Der Garbage Collection-Prozess, auch bekannt als automatische Speicherverwaltung, ist die automatische Wiederverwendung von dynamisch zugewiesenem Speicher, wie hier erläutert. Immer noch nicht verstanden? Ok, lassen Sie mich das anhand einiger Bilder und eines vereinfachten Beispiels erklären.

Dies ist Ihr Speicherhaufen (nicht zu verwechseln mit Stack), der Ihrer App zugewiesen ist. Es ist gerade leer.

  1. Leerer Speicherhaufen
    Wenn Ihre App startet, weist sie Objekten (Referenztypen) zur Laufzeit Speicher zu.

  2. Speicherheap mit zugewiesenen Speicherblöcken (blaue Rechtecke)
    Nach einer Weile kommen einige Objekte für die Garbage Collection in Frage und werden dann aus dem Speicherhaufen entfernt.

  3. Die dunkelblauen Rechtecke zeigen Objekte an, die für die Garbage Collection geeignet sind, die dann aus dem Speicherhaufen entfernt werden

Ein Objekt wird zu einem guten Kandidaten für die Garbage Collection, wenn es keine Verweise darauf gibt. Sie haben beispielsweise eine TODO-App. Es gibt einen Aufgabenlistenbildschirm und einen Aufgabendetailbildschirm. Zunächst weist Ihre App Speicher zum Anzeigen der Aufgabenliste beim Start zu. Wenn der Benutzer auf ein Element in der Liste klickt, wird der Detailbildschirm angezeigt. Für diese Aktion weist Ihre App dem Heap dynamisch zusätzlichen Speicher zu. Wenn der Benutzer den Bildschirm mit den Aufgabendetails schließt, sollten alle damit verbundenen Objekte entfernt werden. Der Grund ist einfach. Sie haben keinen unbegrenzten Speicher und müssen ihn zurückfordern. Deshalb entfernt das System unerreichbare Objekte. Die Vorteile liegen ohne Zweifel auf der Hand. Das bedeutet, dass der Programmierer nicht für das Löschen ungenutzter Objekte verantwortlich ist. Sowohl Kotlin als auch Swift haben diesen Prozess im Gegensatz zu C, C++ automatisiert. Also, wenn wir diese Funktion kostenlos haben und alles auf magische Weise passiert, sollten Sie sich darum kümmern? Ja, das sollten Sie, denn das bedeutet nicht, dass Sie keine Fehler machen dürfen. Darüber hinaus können Sie mit diesem Wissen bessere Apps schreiben, die viel reibungsloser funktionieren. Wenn Sie ein großartiger Android- oder iOS-Entwickler werden möchten, sollten Sie sich mit Speicherverwaltung auskennen.

Obwohl Swift und Kotlin Programmierern die Aufgabe abnehmen, Speicherplatz für eine App freizugeben, tun sie dies auf unterschiedliche Weise. Ich werde versuchen, dieses fortgeschrittene Thema so einfach wie möglich zu beschreiben. Ich werde mich nicht auf die Details konzentrieren, da ich diesen Artikel für alle verständlich halten möchte. Für diejenigen, die mehr erfahren möchten, habe ich einige Referenzen hinterlassen. Beginnen wir mit Kotlin.

Speicherverwaltung in Kotlin (Android)

Android verwendet die häufigste Art der Garbage Collection, die als Tracing Garbage Collection mit dem CMS-Algorithmus bezeichnet wird. CMS steht für Concurrent Mark Sweep. Für unsere Zwecke können Sie den Buchstaben „C“ ignorieren. Hier ist es wichtiger zu verstehen, wie ein grundlegender Mark-and-Sweep-Algorithmus funktioniert.

Der erste Schritt besteht darin, die Garbage Collection Roots zu definieren. Dies können statische Variablen, aktive Threads (z. B. ein UI-Thread in Android) oder andere sein, die hier aufgelistet sind. Wenn dies geschehen ist, startet GC eine Markierungsphase. Zu diesem Zweck durchläuft der GC einen ganzen Objektbaum. Jedes erstellte Objekt hat ein Markierungsbit, das standardmäßig auf 0 gesetzt ist. Wenn ein Objekt in der Markierungsphase besucht wird, wird sein Markierungsbit auf 1 gesetzt – es bedeutet, dass es erreichbar ist.

Im obigen Bild sind die Objekte, die nach dieser Phase grau geblieben sind, unzugänglich, daher benötigt unsere App sie nicht mehr. Aber bevor Sie fortfahren, sehen Sie sich das Bild bitte noch einmal an. Bemerken Sie die Objekte, die aufeinander zeigen? iOS-Entwickler nennen sie Retention-Zyklen. Dieses Problem existiert nicht in der Android-Welt. Die „Zyklen“ werden entfernt, wenn es keinen Pfad zum GC-Root gibt.

Eine weitere erwähnenswerte Sache in der Mark-Phase sind die versteckten Kosten. Der Begriff Stop The World sollte Ihnen bekannt sein. Vor jedem Sammelzyklus pausiert der GC unsere App, um zu verhindern, dass beim Durchlaufen des Objektbaums neue Objekte zugewiesen werden. Die Pausendauer hängt von der Anzahl der erreichbaren Objekte ab. Die Gesamtzahl der Objekte oder die Heap-Größe spielt keine Rolle. Aus diesem Grund ist es schmerzhaft, viele „lebendige“ unnötige Objekte zu erstellen – zum Beispiel Autoboxing innerhalb einer Schleife. Der GC startet den Vorgang, wenn der Speicherheap fast voll ist. Wenn Sie also viele unnötige Objekte erstellen, füllen Sie den Speicherhaufen schneller auf, was wiederum mehr GC-Zyklen und mehr Frame-Drops erzeugt, da jede Pause die Zeit Ihrer App verbraucht.

Lassen Sie uns nun über das Entfernen sprechen. Um jeglichen Abfall loszuwerden, führt der GC die nächste Phase aus – Sweeping. In diesem Fall durchsucht der GC den Memory Heap, um alle Objekte zu finden, deren Mark-Bit auf 0 gesetzt ist. Im letzten Schritt werden sie entfernt und die Mark-Bits aller erreichbaren Objekte auf 0 zurückgesetzt. So einfach ist das . Diese Lösung hat jedoch einen Nachteil. Dies kann zu einer Fragmentierung des Speicherheaps führen. Das bedeutet, dass Ihr Heap insgesamt ziemlich viel Platz (freier Speicher) haben kann, aber dieser Platz ist in kleine Blöcke unterteilt. Daher können Probleme auftreten, wenn Sie versuchen, ein 2-MB-Objekt 4 MB freien Speicher zuzuweisen. Wieso den? Denn der größte einzelne Block reicht möglicherweise nicht aus, um 2 MB aufzunehmen. Aus diesem Grund verwendet Android eine verbesserte Version namens Compact. Die Compact-Variante hat eine zusätzliche Stufe. Die Objekte, die die Sweep-Phase überstanden haben, werden an den Anfang des Speicherhaufens verschoben – überprüfen Sie das Bild.

Es gibt kein kostenloses Mittagessen. Diese Verbesserung verlängert die GC-Pause.

Das ist also die Speicherverwaltung in Android auf den Punkt gebracht. Natürlich habe ich nicht alle Aspekte abgedeckt. Zum Beispiel habe ich das Thema Heap-Generierung übersprungen. Wenn Sie eifrig sind und mehr darüber erfahren möchten, klicken Sie hier und sehen Sie sich den Abschnitt „Der Generationen-Garbage-Collection-Prozess“ an.

Ok, jetzt ist es Zeit für Swift.

Speicherverwaltung in Swift (iOS)

Swift verwendet einen einfachen Garbage-Collection-Mechanismus. Es heißt ARC (Automatic Reference Counting). Dieser Ansatz basiert auf dem Verfolgen der Anzahl starker Verweise auf ein Objekt, das von anderen Objekten gehalten wird. Jede neu erstellte Instanz einer Klasse speichert zusätzliche Informationen – einen Referenzzähler. Immer wenn Sie ein Objekt einer Eigenschaft, Variablen oder Konstante zuweisen (indem Sie stark darauf verweisen), erhöhen Sie den Referenzzählerwert. Bis dieser Wert ungleich 0 ist, ist Ihr Objekt sicher und kann nicht freigegeben werden. Aber sobald der Referenzzähler auf 0 geht, wird das Objekt sofort* zurückgefordert, ohne Pause und ohne den GC-Erfassungszyklus einzuleiten. Dies ist ein großer Vorteil gegenüber dem Tracing-Garbage-Collection-Typ.

Ok, ich habe ein Sternchen neben „sofort“ gesetzt. Ich habe dies getan, weil Sie einige Informationen im Internet finden können, dass dies einer der Mythen der Speicherverwaltung ist. Ich ermutige Sie, sich diese interessante Diskussion anzusehen 😃

Natürlich gibt es auch die andere Seite der Medaille – die zuvor erwähnten Haltezyklen. Schauen wir uns zunächst einmal an, wie man einen Retain-Zyklus erstellt und was iOS-Entwickler tun müssen, um Memory Leaks zu vermeiden. Betrachten wir zwei ähnliche Klassen:

    let name: String
    var dog: Dog?
    init(name: String) {
        self.name = name
    }
}
class Dog {
 
    let name: String
    var owner: Person?
    
    init(name: String) {
        self.name = name
    }
 }

Beide Klassen haben einen Namen und eine optionale Eigenschaft – ein Hund für eine Person, weil eine Person möglicherweise nicht immer einen Hund hat, und eine Person für einen Hund, weil ein Hund möglicherweise nicht immer einen Besitzer hat – so traurig 😦

Das nächste Code-Snippet erstellt Instanzen jeder Klasse und setzt gleichzeitig den Referenzzähler für beide auf 1:

var joe: Person? = Person(Name: “Joe”)

var lassie: Dog? = Hund(Name: “Lassie”)

Joe und Lassie sind starke Verweise auf die Personen- bzw. Hundeinstanzen. So weit, ist es gut. Wenn Sie der joe-Variablen nil zuweisen, gewinnen Sie Speicher zurück, da es keinen starken Verweis mehr auf die Person-Instanz gibt.

Um einen starken Referenzzyklus, auch Retain-Zyklus genannt, zu erstellen, verknüpfen Sie einfach die beiden Instanzen miteinander.

joe!.dog = Mädchen

lassie!.owner = Joe

Bitte beachten Sie die Referenzzähler. Beide haben den gleichen Wert von 2.

Wenn Sie jetzt die starken Referenzen von Joe und Lassie unterbrechen, werden die Referenzzähler nicht auf 0 zurückgesetzt.

joe = null
Mädchen = null

ARC ist aufgrund des Aufbewahrungszyklus nicht in der Lage, die Zuordnung der Instanzen aufzuheben.

Natürlich gibt es dafür eine Lösung. Um den starken Referenzzyklus aufzulösen, sollten Sie eine schwache oder unbesessene Referenz verwenden. Sie fügen einfach ein spezielles Schlüsselwort vor einer Variablen hinzu, und wenn Sie dieser Variablen dann ein Objekt zuweisen, wird der Referenzzähler des Objekts nicht erhöht.

Wie sich die Speicherverwaltung auf die Art und Weise auswirkt, wie wir codieren
Ehrlich gesagt, als ich anfing, iOS zu lernen, dachte ich, dass eine schwache Referenz in iOS und Android auf die gleiche Weise funktioniert. Das stimmt natürlich nicht.

Die Verwendung des schwachen Schlüsselworts in iOS ist normal und kann sogar als bewährte Methode angesehen werden, wenn Sie das Delegationsmuster häufig verwenden. Wenn es um Android geht, ist es keine gängige Praxis, es sei denn, Sie verwenden immer noch AsyncTasks (ich hoffe nicht).

Aufgrund der Retain-Zyklen müssen iOS-Entwickler für einfache Dinge manchmal komplexeren Code schreiben als Android-Entwickler. Das beste Beispiel dafür ist die Verwendung einer Schließung (Swift) und Lambda (Android).

Android:

class UpdateHandler {
    var actionAfterUpdate: (() -> Unit) = {} // Lambda
fun update() {
        // do work
        actionAfterUpdate()
    }
}

iOS:

class UpdateHandler {
    var actionAfterUpdate: () -> Void = {} // Closure
    
    func update() {
        // do work
        actionAfterUpdate()
    }
}
The actionAfterUpdate will execute when the update() method is complete. Now, let’s check how to use the UpdateHandler.

Android:

class MyObject {
    val updateHandler = UpdateHandler()

    fun doSomething() {
        // do important thing
    }

    fun timeToUpdate() {
        updateHandler.actionAfterUpdate = { doSomething() }
        updateHandler.update()
    }
}

iOS:

class MyObject {
    let updateHandler = UpdateHandler()
    
    func doSomething() {
        // do important thing
    }
    
    func timeToUpdate() {
        updateHandler.actionAfterUpdate = { self.doSomething() }
        updateHandler.update()
    }
}

Wie Sie sehen können, ist die Verwendung des UpdateHandlers einfach. Bevor Sie die Methode update() aufrufen, deklarieren Sie, was nach dieser Aktualisierung geschehen soll. Alles scheint in Ordnung zu sein, aber… die iOS-Version hat einen schrecklichen Fehler… Es ist schrecklich, weil es ein Speicherleck verursacht. Was ist das Problem? Es ist die actionAfterUpdate-Closure, die einen starken Bezug zu sich selbst enthält. Self ist eine MyObject-Instanz, die auch einen Verweis auf den UpdateHandler enthält – einen Retain-Zyklus! Um ein Speicherleck zu verhindern, müssen wir ein schwaches (oder unbesessenes, was in diesem Fall ausreicht) Schlüsselwort innerhalb der Closure verwenden:

updateHandler.actionAfterUpdate = { [weak self] in self?.doSomething() }
Ein weiteres Problem ist das Lapsed-Listener-Problem. Kurz gesagt, wenn Sie einen Zuhörer registrieren und vergessen, ihn abzumelden, endet dies mit einem Speicherleck in Ihrer App.

Ich habe Beispiele modifiziert, die ich zuvor verwendet habe, um diese Wurmkiste ausführlicher zu diskutieren.

Im Moment ist der UpdateHandler ein Singleton, dessen Lebensdauer so lang ist wie die Lebensdauer unserer App. Um ein Update zu erhalten, müssen Sie zuerst einen Listener registrieren.

Android:

object UpdateHandler {
    private var listener: OnUpdateListener? = null

    fun registerUpdateListener(listener: OnUpdateListener) {
        this.listener = listener
    }

    fun update() {
        // do work
        listener?.onUpdateComplete()
    }
}

interface OnUpdateListener {
    fun onUpdateComplete()
}

iOS:

class UpdateHandler {
    
    static let sharedInstance = UpdateHandler()
    
    private var listener: OnUpdateListener? = nil
    
    func registerUpdateListener(listener: OnUpdateListener) {
        self.listener = listener
    }
    
    func update() {
        // do work
        listener?.onUpdateComplete()
    }
}

protocol OnUpdateListener: class {
    func onUpdateComplete()
}

Und einige Modifikationen in der MyObject-Klasse.

Android:

class MyObject: OnUpdateListener {
    override fun onUpdateComplete() {...}
}
iOS:

class MyObject: OnUpdateListener {
    func onUpdateComplete() {...}
}

Wie Sie sehen können, ist MyObject ein Update-Listener und führt eine bestimmte Aktion aus, wenn das Update abgeschlossen ist.

Um das Problem hervorzuheben, habe ich diesen Code an einer Stelle eingefügt, an der er jedes Mal aufgerufen wird, wenn Sie die App beenden und neu starten. Beachten Sie jedoch, dass dieses Beispiel im Produktionscode keinen Sinn ergibt. Es ist nur ein einfaches Beispiel 😃

Android (MainActivity.kt):

override fun onStart() {
    super.onStart()
    val myObject = MyObject()
    UpdateHandler.registerUpdateListener(myObject)
    UpdateHandler.update()
}

iOS (AppDelegate.swift):

func applicationWillEnterForeground(_ application: UIApplication) {
    let myObject = MyObject()
    UpdateHandler.sharedInstance.registerUpdateListener(listener: myObject)
    UpdateHandler.sharedInstance.update()
}

Ich habe also ein myObject erstellt, es als Update-Listener an den UpdateHandler übergeben und die Methode update() aufgerufen. Die Methode update() benachrichtigt den Listener über die abgeschlossene Arbeit, indem sie die Methode onUpdateComplete() aufruft (die Methode onUpdateComplete() wird innerhalb von MyObject ausgeführt).

Die Instanz der MyObject-Klasse sollte entfernt werden, wenn onStart() / applicationWillEnterForegorund(…) abgeschlossen ist, da innerhalb von Methoden erstellte Objekte so lange am Leben bleiben, wie die Methode ausgeführt wird, und nach dieser Zeit für die Garbage Collection infrage kommen. Aber in diesem Fall hält der UpdateHandler für immer einen Verweis auf die MyObject-Instanz. Wie können Sie mit einem potenziellen Speicherleck umgehen? Wahrscheinlich würden alle iOS-Entwickler sagen – „Verwenden Sie eine schwache Referenz!“, und sie haben Recht. Die Verwendung eines schwachen Schlüsselworts mit einer Listener-Variablen innerhalb der UpdateHandler-Klasse macht den Trick:

class UpdateHandler {
    ...
    private weak var listener: OnUpdateListener? = nil
    ...
}

Dank dessen ist ARC in der Lage, den Listener für uns zu entfernen.

Aber was ist mit Android? Ein paar Android-Entwickler würden dasselbe sagen – „Hold a listener as a WeakReference!“. Ok, es mag helfen … manchmal … aber ich bin sicher, dass es der Anfang Ihrer Probleme ist 😃 Sie sollten wissen, dass jedes Mal, wenn Sie Ihren Rückruf als WeakReference halten, ein Kätzchen stirbt.

Die WeakReference macht Ihr Objekt für die Garbage Collection geeignet. Es kann also früher entfernt werden, als Sie denken. Die einzige Lösung für diesen Fall besteht darin, eine unregisterUpdateListener-Methode hinzuzufügen und den Listener manuell zu löschen.

Ähnlich und doch anders.

Herzlichen Glückwunsch an alle, die bis zum Schluss durchgehalten haben!

Ich freue mich, wenn dieser Artikel jemandem hilft, dieses komplizierte Thema zu verstehen. Ich habe versucht zu erklären, wie du fünf bist, Schritt für Schritt. Scheinbar zwei ähnliche Programmiersprachen verbergen viele Unterschiede unter der Haube, und ich möchte, dass Sie sich dessen bewusst sind. Manchmal funktioniert eine gängige Vorgehensweise von Android nicht auf iOS und umgekehrt. Der Wechsel von einer Plattform zur anderen ist nicht so einfach, wie es scheinen mag.

Similar Posts

Leave a Reply

Your email address will not be published.