„Verborgene“ Merkmale der Sprache
Dieser Artikel behandelt ein sehr eng gefasstes Thema, das die meisten Entwickler in der Praxis äußerst selten antreffen (und sich seiner Existenz möglicherweise nicht einmal bewusst sind).
Wir empfehlen, dieses Kapitel zu überspringen, wenn Sie gerade erst mit dem Erlernen von JavaScript begonnen haben.
Wenn wir uns an das Grundkonzept des Erreichbarkeitsprinzips aus dem Kapitel über die Garbage-Collection erinnern, können wir feststellen, dass die JavaScript-Engine garantiert Werte im Speicher behält, auf die zugegriffen werden kann oder die gerade verwendet werden.
Zum Beispiel:
// Die Benutzervariable enthält einen starken Verweis auf das Objekt let user = { name: "John" }; // Überschreiben wir den Wert der Benutzervariablen Benutzer = null; // Die Referenz geht verloren und das Objekt wird aus dem Speicher gelöscht
Oder ein ähnlicher, aber etwas komplizierterer Code mit zwei starken Referenzen:
// Die Benutzervariable enthält einen starken Verweis auf das Objekt let user = { name: "John" }; // den starken Verweis auf das Objekt in die Admin-Variable kopiert let admin = user; // Überschreiben wir den Wert der Benutzervariablen Benutzer = null; // Das Objekt ist weiterhin über die Admin-Variable erreichbar
Das Objekt { name: "John" }
würde nur dann aus dem Speicher gelöscht, wenn keine starken Verweise darauf vorhanden wären (wenn wir auch den Wert der admin
Variablen überschrieben hätten).
In JavaScript gibt es ein Konzept namens WeakRef
, das sich in diesem Fall etwas anders verhält.
Begriffe: „Starke Referenz“, „Schwache Referenz“
Starke Referenz – ist eine Referenz auf ein Objekt oder einen Wert, die verhindert, dass diese vom Garbage Collector gelöscht werden. Dadurch bleibt das Objekt oder der Wert im Gedächtnis, auf den es verweist.
Das bedeutet, dass das Objekt oder der Wert im Speicher verbleibt und nicht vom Garbage Collector gesammelt wird, solange aktive starke Referenzen darauf bestehen.
In JavaScript sind gewöhnliche Verweise auf Objekte starke Verweise. Zum Beispiel:
// Die Benutzervariable enthält einen starken Verweis auf dieses Objekt let user = { name: "John" };
Schwache Referenz – ist eine Referenz auf ein Objekt oder einen Wert, die nicht verhindert, dass diese vom Garbage Collector gelöscht werden. Ein Objekt oder Wert kann vom Garbage Collector gelöscht werden, wenn die einzigen verbleibenden Verweise darauf schwache Verweise sind.
Hinweis zur Vorsicht
Bevor wir näher darauf eingehen, ist es erwähnenswert, dass die korrekte Verwendung der in diesem Artikel besprochenen Strukturen sehr sorgfältige Überlegungen erfordert und sie nach Möglichkeit am besten vermieden werden sollten.
WeakRef
– ist ein Objekt, das eine schwache Referenz auf ein anderes Objekt enthält, das target
oder referent
bezeichnet wird.
Die Besonderheit von WeakRef
besteht darin, dass es den Garbage Collector nicht daran hindert, sein Referenzobjekt zu löschen. Mit anderen Worten: Ein WeakRef
-Objekt hält das referent
nicht am Leben.
Nehmen wir nun die user
als „Referenz“ und erstellen daraus eine schwache Referenz auf die admin
Variable. Um einen schwachen Verweis zu erstellen, müssen Sie den WeakRef
Konstruktor verwenden und das Zielobjekt (das Objekt, auf das Sie einen schwachen Verweis wünschen) übergeben.
In unserem Fall ist dies die user
:
// Die Benutzervariable enthält einen starken Verweis auf das Objekt let user = { name: "John" }; // Die Admin-Variable enthält einen schwachen Verweis auf das Objekt let admin = new WeakRef(user);
Das folgende Diagramm zeigt zwei Arten von Referenzen: eine starke Referenz, die die user
verwendet, und eine schwache Referenz, die die admin
Variable verwendet:
Dann hören wir irgendwann auf, die user
zu verwenden – sie wird überschrieben, verlässt den Gültigkeitsbereich usw., während die WeakRef
-Instanz in der admin
Variable bleibt:
// Überschreiben wir den Wert der Benutzervariablen Benutzer = null;
Ein schwacher Verweis auf ein Objekt reicht nicht aus, um es „am Leben“ zu halten. Wenn die einzigen verbleibenden Verweise auf ein Referenzobjekt schwache Verweise sind, steht es dem Garbage Collector frei, dieses Objekt zu zerstören und seinen Speicher für etwas anderes zu verwenden.
Bis zur tatsächlichen Zerstörung des Objekts kann es jedoch vorkommen, dass die schwache Referenz es zurückgibt, selbst wenn keine starken Referenzen mehr auf dieses Objekt vorhanden sind. Das heißt, unser Objekt wird zu einer Art „Schrödingers Katze“ – wir können nicht sicher wissen, ob es „lebendig“ oder „tot“ ist:
Um das Objekt an dieser Stelle von der WeakRef
-Instanz abzurufen, verwenden wir deref()
-Methode.
Die deref()
Methode gibt das Referenzobjekt zurück, auf das die WeakRef
zeigt, wenn sich das Objekt noch im Speicher befindet. Wenn das Objekt vom Garbage Collector gelöscht wurde, gibt die deref()
-Methode undefined
zurück:
let ref = admin.deref(); if (ref) { // Das Objekt ist weiterhin zugänglich: Wir können beliebige Manipulationen daran vornehmen } anders { // Das Objekt wurde vom Garbage Collector gesammelt }
WeakRef
wird normalerweise zum Erstellen von Caches oder assoziativen Arrays verwendet, die ressourcenintensive Objekte speichern. Dadurch kann verhindert werden, dass diese Objekte allein aufgrund ihrer Anwesenheit im Cache oder im assoziativen Array vom Garbage Collector gesammelt werden.
Eines der Hauptbeispiele ist eine Situation, in der wir über zahlreiche binäre Bildobjekte verfügen (z. B. dargestellt als ArrayBuffer
oder Blob
) und jedem Bild einen Namen oder Pfad zuordnen möchten. Bestehende Datenstrukturen sind für diese Zwecke nicht ganz geeignet:
Wenn Sie Map
verwenden, um Zuordnungen zwischen Namen und Bildern oder umgekehrt zu erstellen, bleiben die Bildobjekte im Speicher, da sie in der Map
als Schlüssel oder Werte vorhanden sind.
WeakMap
ist für dieses Ziel ebenfalls nicht geeignet, da die als WeakMap
-Schlüssel dargestellten Objekte schwache Referenzen verwenden und nicht vor dem Löschen durch den Garbage Collector geschützt sind.
In dieser Situation benötigen wir jedoch eine Datenstruktur, die in ihren Werten schwache Referenzen verwendet.
Zu diesem Zweck können wir eine Map
Sammlung verwenden, deren Werte WeakRef
Instanzen sind, die auf die großen Objekte verweisen, die wir benötigen. Folglich werden wir diese großen und unnötigen Objekte nicht länger im Speicher behalten, als sie sollten.
Andernfalls ist dies eine Möglichkeit, das Bildobjekt aus dem Cache abzurufen, sofern es noch erreichbar ist. Wenn es im Müll gesammelt wurde, werden wir es neu generieren oder erneut herunterladen.
Dadurch wird in manchen Situationen weniger Speicher beansprucht.
Unten finden Sie einen Codeausschnitt, der die Technik der Verwendung von WeakRef
demonstriert.
Kurz gesagt, wir verwenden eine Map
mit Zeichenfolgenschlüsseln und WeakRef
-Objekten als Werten. Wenn das WeakRef
Objekt nicht vom Garbage Collector gesammelt wurde, holen wir es aus dem Cache. Andernfalls laden wir es erneut herunter und legen es zur weiteren möglichen Wiederverwendung in den Cache:
Funktion fetchImg() { // abstrakte Funktion zum Herunterladen von Bildern... } Funktion schwachRefCache(fetchImg) { // (1) const imgCache = new Map(); // (2) return (imgName) => { // (3) const zwischengespeichertImg = imgCache.get(imgName); // (4) if (cachedImg?.deref()) { // (5) returncachedImg?.deref(); } const newImg = fetchImg(imgName); // (6) imgCache.set(imgName, new WeakRef(newImg)); // (7) return newImg; }; } const getCachedImg = schwachRefCache(fetchImg);
Schauen wir uns die Einzelheiten dessen an, was hier passiert ist:
weakRefCache
– ist eine Funktion höherer Ordnung, die eine andere Funktion, fetchImg
, als Argument verwendet. In diesem Beispiel können wir eine detaillierte Beschreibung der fetchImg
-Funktion vernachlässigen, da es sich dabei um eine beliebige Logik zum Herunterladen von Bildern handeln kann.
imgCache
– ist ein Bildcache, der zwischengespeicherte Ergebnisse der fetchImg
-Funktion in Form von Zeichenfolgenschlüsseln (Bildname) und WeakRef
Objekten als deren Werte speichert.
Gibt eine anonyme Funktion zurück, die den Bildnamen als Argument verwendet. Dieses Argument wird als Schlüssel für das zwischengespeicherte Bild verwendet.
Es wird versucht, das zwischengespeicherte Ergebnis mithilfe des bereitgestellten Schlüssels (Bildname) aus dem Cache abzurufen.
Wenn der Cache einen Wert für den angegebenen Schlüssel enthält und das WeakRef
Objekt nicht vom Garbage Collector gelöscht wurde, wird das zwischengespeicherte Ergebnis zurückgegeben.
Wenn im Cache kein Eintrag mit dem angeforderten Schlüssel vorhanden ist oder die Methode deref()
undefined
zurückgibt (was bedeutet, dass das WeakRef
Objekt durch Garbage Collection erfasst wurde), lädt die Funktion fetchImg
das Bild erneut herunter.
Legen Sie das heruntergeladene Bild als WeakRef
Objekt in den Cache.
Jetzt haben wir eine Map
Sammlung, in der die Schlüssel – Bildnamen als Strings und Werte – WeakRef
Objekte sind, die die Bilder selbst enthalten.
Diese Technik hilft zu vermeiden, dass viel Speicher für ressourcenintensive Objekte reserviert wird, die niemand mehr nutzt. Es spart außerdem Speicher und Zeit bei der Wiederverwendung zwischengespeicherter Objekte.
Hier ist eine visuelle Darstellung, wie dieser Code aussieht:
Diese Implementierung hat jedoch ihre Nachteile: Mit der Zeit wird Map
mit Strings als Schlüssel gefüllt, die auf eine WeakRef
verweisen, deren Referenzobjekt bereits durch Garbage Collection erfasst wurde:
Eine Möglichkeit, dieses Problem zu lösen, besteht darin, den Cache regelmäßig zu leeren und „tote“ Einträge zu löschen. Eine andere Möglichkeit besteht darin, Finalizer zu verwenden, die wir als Nächstes untersuchen werden.
Ein weiterer Anwendungsfall für WeakRef
ist die Verfolgung von DOM-Objekten.
Stellen wir uns ein Szenario vor, in dem Code oder Bibliotheken von Drittanbietern mit Elementen auf unserer Seite interagieren, solange diese im DOM vorhanden sind. Beispielsweise könnte es sich um ein externes Dienstprogramm zur Überwachung und Benachrichtigung über den Systemstatus handeln (üblicherweise ein sogenannter „Logger“ – ein Programm, das Informationsmeldungen namens „Logs“ sendet).
Interaktives Beispiel:
Ergebnis
index.js
index.css
index.html
const startMessagesBtn = document.querySelector('.start-messages'); // (1) const closeWindowBtn = document.querySelector('.window__button'); // (2) const windowElementRef = new WeakRef(document.querySelector(".window__body")); // (3) startMessagesBtn.addEventListener('click', () => { // (4) startMessages(windowElementRef); startMessagesBtn.disabled = true; }); closeWindowBtn.addEventListener('click', () => document.querySelector(".window__body").remove()); // (5) const startMessages = (element) => { const timerId = setInterval(() => { // (6) if (element.deref()) { // (7) const payload = document.createElement("p"); payload.textContent = „Nachricht: Systemstatus OK: ${new Date().toLocaleTimeString()}“; element.deref().append(payload); } else { // (8) alarm("Das Element wurde gelöscht."); // (9) clearInterval(timerId); } }, 1000); };
.app { Anzeige: Flex; Flexrichtung: Spalte; Lücke: 16px; } .start-messages { Breite: fit-content; } .Fenster { Breite: 100 %; Rand: 2 Pixel durchgehend #464154; Überlauf: versteckt; } .window__header { Position: klebrig; Polsterung: 8px; Anzeige: Flex; justify-content: Leerzeichen dazwischen; align-items: center; Hintergrundfarbe: #736e7e; } .window__title { Rand: 0; Schriftgröße: 24px; Schriftstärke: 700; Farbe: weiß; Buchstabenabstand: 1px; } .window__button { Polsterung: 4px; Hintergrund: #4f495c; Gliederung: keine; Rand: 2 Pixel durchgehend #464154; Farbe: weiß; Schriftgröße: 16px; Cursor: Zeiger; } .window__body { Höhe: 250px; Polsterung: 16px; Überlauf: scrollen; Hintergrundfarbe: #736e7e33; }
<!DOCTYPE HTML> <html lang="de"> <Kopf> <meta charset="utf-8"> <link rel="stylesheet" href="index.css"> <title>WeakRef DOM Logger</title> </head> <Körper> <div class="app"> <button class="start-messages">Mit dem Senden von Nachrichten beginnen</button> <div class="window"> <div class="window__header"> <p class="window__title">Nachrichten:</p> <button class="window__button">Schließen</button> </div> <div class="window__body"> Keine Nachrichten. </div> </div> </div> <script type="module" src="index.js"></script> </body> </html>
Wenn auf die Schaltfläche „Mit dem Senden von Nachrichten beginnen“ geklickt wird, werden im sogenannten „Protokollanzeigefenster“ (ein Element mit der Klasse .window__body
) Nachrichten (Protokolle) angezeigt.
Sobald dieses Element jedoch aus dem DOM gelöscht wird, sollte der Logger keine Nachrichten mehr senden. Um das Entfernen dieses Elements zu reproduzieren, klicken Sie einfach auf die Schaltfläche „Schließen“ in der oberen rechten Ecke.
Um unsere Arbeit nicht zu verkomplizieren und den Code von Drittanbietern nicht jedes Mal zu benachrichtigen, wenn unser DOM-Element verfügbar ist und wenn dies nicht der Fall ist, reicht es aus, mit WeakRef
einen schwachen Verweis darauf zu erstellen.
Sobald das Element aus dem DOM entfernt wird, bemerkt der Logger dies und sendet keine Nachrichten mehr.
Schauen wir uns nun den Quellcode ( tab index.js
) genauer an:
Holen Sie sich das DOM-Element der Schaltfläche „Nachrichten senden starten“.
Holen Sie sich das DOM-Element der Schaltfläche „Schließen“.
Rufen Sie das DOM-Element des Protokollanzeigefensters mit dem new WeakRef()
-Konstruktor ab. Auf diese Weise enthält die Variable windowElementRef
einen schwachen Verweis auf das DOM-Element.
Fügen Sie der Schaltfläche „Mit dem Senden von Nachrichten beginnen“ einen Ereignis-Listener hinzu, der dafür verantwortlich ist, den Logger zu starten, wenn darauf geklickt wird.
Fügen Sie der Schaltfläche „Schließen“ einen Ereignis-Listener hinzu, der dafür verantwortlich ist, das Protokollanzeigefenster zu schließen, wenn darauf geklickt wird.
Verwenden Sie setInterval
, um jede Sekunde eine neue Nachricht anzuzeigen.
Wenn das DOM-Element des Protokollanzeigefensters weiterhin zugänglich ist und im Speicher verbleibt, erstellen und senden Sie eine neue Nachricht.
Wenn die deref()
Methode undefined
zurückgibt, bedeutet dies, dass das DOM-Element aus dem Speicher gelöscht wurde. In diesem Fall stellt der Logger die Anzeige von Meldungen ein und löscht den Timer.
alert
, der aufgerufen wird, nachdem das DOM-Element des Protokollanzeigefensters aus dem Speicher gelöscht wurde (dh nach dem Klicken auf die Schaltfläche „Schließen“). Beachten Sie, dass das Löschen aus dem Speicher möglicherweise nicht sofort erfolgt, da es nur von den internen Mechanismen des Garbage Collectors abhängt.
Wir können diesen Prozess nicht direkt über den Code steuern. Trotzdem haben wir immer noch die Möglichkeit, die Speicherbereinigung im Browser zu erzwingen.
In Google Chrome müssen Sie dazu beispielsweise die Entwicklertools öffnen ( Strg + Umschalt + J unter Windows/Linux oder Option + ⌘ + J unter macOS), zur Registerkarte „Leistung“ gehen und auf klicken Schaltfläche „Mülleimer“ – „Müll sammeln“:
Diese Funktionalität wird in den meisten modernen Browsern unterstützt. Nachdem die Maßnahmen ergriffen wurden, wird die alert
sofort ausgelöst.
Jetzt ist es an der Zeit, über Finalizer zu sprechen. Bevor wir fortfahren, klären wir die Terminologie:
Cleanup-Callback (Finalizer) – ist eine Funktion, die ausgeführt wird, wenn ein in der FinalizationRegistry
registriertes Objekt vom Garbage Collector aus dem Speicher gelöscht wird.
Sein Zweck besteht darin, die Möglichkeit zu bieten, zusätzliche Operationen im Zusammenhang mit dem Objekt auszuführen, nachdem es endgültig aus dem Speicher gelöscht wurde.
Registry (oder FinalizationRegistry
) – ist ein spezielles Objekt in JavaScript, das die Registrierung und Aufhebung der Registrierung von Objekten sowie deren Bereinigungsrückrufe verwaltet.
Dieser Mechanismus ermöglicht die Registrierung eines Objekts, um es zu verfolgen und ihm einen Bereinigungsrückruf zuzuordnen. Im Wesentlichen handelt es sich um eine Struktur, die Informationen über registrierte Objekte und deren Bereinigungsrückrufe speichert und diese Rückrufe dann automatisch aufruft, wenn die Objekte aus dem Speicher gelöscht werden.
Um eine Instanz der FinalizationRegistry
zu erstellen, muss sie ihren Konstruktor aufrufen, der ein einziges Argument benötigt – den Bereinigungsrückruf (Finalizer).
Syntax:
Funktion cleanupCallback(heldValue) { // Rückrufcode bereinigen } const Registry = new FinalizationRegistry(cleanupCallback);
Hier:
cleanupCallback
– ein Bereinigungsrückruf, der automatisch aufgerufen wird, wenn ein registriertes Objekt aus dem Speicher gelöscht wird.
heldValue
– der Wert, der als Argument an den Bereinigungsrückruf übergeben wird. Wenn heldValue
ein Objekt ist, behält die Registrierung einen starken Verweis darauf bei.
registry
– eine Instanz von FinalizationRegistry
.
FinalizationRegistry
-Methoden:
register(target, heldValue [, unregisterToken])
– wird zum Registrieren von Objekten in der Registrierung verwendet.
target
– das Objekt, das für die Verfolgung registriert wird. Wenn das target
Garbage Collection ist, wird der Bereinigungsrückruf mit heldValue
als Argument aufgerufen.
Optionales unregisterToken
– ein Aufhebungstoken. Es kann übergeben werden, um die Registrierung eines Objekts aufzuheben, bevor der Garbage Collector es löscht. Typischerweise wird das target
als unregisterToken
verwendet, was der Standardpraxis entspricht.
unregister(unregisterToken)
– die unregister
-Methode wird verwendet, um die Registrierung eines Objekts aus der Registrierung aufzuheben. Es benötigt ein Argument – unregisterToken
(das Unregister-Token, das bei der Registrierung des Objekts erhalten wurde).
Kommen wir nun zu einem einfachen Beispiel. Lassen Sie uns das bereits bekannte user
verwenden und eine Instanz von FinalizationRegistry
erstellen:
let user = { name: "John" }; const Registry = new FinalizationRegistry((heldValue) => { console.log(`${heldValue} wurde vom Garbage Collector gesammelt.`); });
Anschließend registrieren wir das Objekt, das einen Bereinigungsrückruf erfordert, indem wir die register
aufrufen:
Registry.register(Benutzer, Benutzername);
Die Registrierung behält keinen starken Verweis auf das registrierte Objekt bei, da dies ihren Zweck zunichte machen würde. Wenn die Registrierung einen starken Verweis behalten würde, würde das Objekt niemals durch Garbage Collection erfasst.
Wenn das Objekt vom Garbage Collector gelöscht wird, wird möglicherweise irgendwann in der Zukunft unser Bereinigungsrückruf aufgerufen, wobei der heldValue
an ihn übergeben wird:
// Wenn das Benutzerobjekt vom Garbage Collector gelöscht wird, wird die folgende Meldung in der Konsole ausgegeben: „John wurde vom Müllsammler abgeholt.“
Es gibt auch Situationen, in denen selbst in Implementierungen, die einen Bereinigungsrückruf verwenden, die Möglichkeit besteht, dass dieser nicht aufgerufen wird.
Zum Beispiel:
Wenn das Programm seinen Vorgang vollständig beendet (z. B. beim Schließen eines Tabs in einem Browser).
Wenn die FinalizationRegistry
Instanz selbst für JavaScript-Code nicht mehr erreichbar ist. Wenn das Objekt, das die FinalizationRegistry
Instanz erstellt, den Gültigkeitsbereich verlässt oder gelöscht wird, werden die in dieser Registrierung registrierten Bereinigungsrückrufe möglicherweise auch nicht aufgerufen.
Wenn wir zu unserem schwachen Cache-Beispiel zurückkehren, können wir Folgendes feststellen:
Auch wenn die in WeakRef
eingeschlossenen Werte vom Garbage Collector gesammelt wurden, besteht immer noch das Problem eines „Speicherverlusts“ in Form der verbleibenden Schlüssel, deren Werte vom Garbage Collector gesammelt wurden.
Hier ist ein verbessertes Caching-Beispiel mit FinalizationRegistry
:
Funktion fetchImg() { // abstrakte Funktion zum Herunterladen von Bildern... } Funktion schwachRefCache(fetchImg) { const imgCache = new Map(); const Registry = new FinalizationRegistry((imgName) => { // (1) const zwischengespeichertImg = imgCache.get(imgName); if (cachedImg && !cachedImg.deref()) imgCache.delete(imgName); }); return (imgName) => { const zwischengespeichertImg = imgCache.get(imgName); if (cachedImg?.deref()) { returncachedImg?.deref(); } const newImg = fetchImg(imgName); imgCache.set(imgName, new WeakRef(newImg)); Registry.register(newImg, imgName); // (2) return newImg; }; } const getCachedImg = schwachRefCache(fetchImg);
Um die Bereinigung „toter“ Cache-Einträge zu verwalten, erstellen wir eine FinalizationRegistry
Bereinigungsregistrierung, wenn die zugehörigen WeakRef
Objekte vom Garbage Collector erfasst werden.
Der wichtige Punkt hierbei ist, dass im Cleanup-Callback überprüft werden sollte, ob der Eintrag vom Garbage Collector gelöscht und nicht erneut hinzugefügt wurde, um keinen „lebenden“ Eintrag zu löschen.
Sobald der neue Wert (Bild) heruntergeladen und im Cache abgelegt wurde, registrieren wir ihn in der Finalizer-Registrierung, um das WeakRef
Objekt zu verfolgen.
Diese Implementierung enthält nur tatsächliche oder „lebende“ Schlüssel/Wert-Paare. In diesem Fall wird jedes WeakRef
-Objekt in der FinalizationRegistry
registriert. Und nachdem die Objekte vom Garbage Collector bereinigt wurden, löscht der Cleanup-Callback alle undefined
Werte.
Hier ist eine visuelle Darstellung des aktualisierten Codes:
Ein wichtiger Aspekt der aktualisierten Implementierung besteht darin, dass Finalizer die Erstellung paralleler Prozesse zwischen dem „Hauptprogramm“ und Bereinigungsrückrufen ermöglichen. Im Kontext von JavaScript ist das „Hauptprogramm“ unser JavaScript-Code, der in unserer Anwendung oder Webseite ausgeführt wird.
Daher kann es zwischen dem Zeitpunkt, an dem ein Objekt vom Garbage Collector zum Löschen markiert wird, und der tatsächlichen Ausführung des Bereinigungsrückrufs eine gewisse Zeitspanne geben. Es ist wichtig zu verstehen, dass das Hauptprogramm während dieser Zeitspanne beliebige Änderungen am Objekt vornehmen oder es sogar wieder in den Speicher zurückholen kann.
Deshalb müssen wir im Cleanup-Callback prüfen, ob ein Eintrag vom Hauptprogramm wieder zum Cache hinzugefügt wurde, um zu vermeiden, dass „lebende“ Einträge gelöscht werden. Ebenso besteht bei der Suche nach einem Schlüssel im Cache die Möglichkeit, dass der Wert vom Garbage Collector gelöscht wurde, der Bereinigungsrückruf jedoch noch nicht ausgeführt wurde.
Solche Situationen erfordern besondere Aufmerksamkeit, wenn Sie mit FinalizationRegistry
arbeiten.
Stellen Sie sich beim Übergang von der Theorie zur Praxis ein reales Szenario vor, in dem ein Benutzer seine Fotos auf einem Mobilgerät mit einem Cloud-Dienst (z. B. iCloud oder Google Fotos) synchronisiert und sie auf anderen Geräten anzeigen möchte. Neben der Grundfunktionalität der Fotobetrachtung bieten solche Dienste viele Zusatzfunktionen, zum Beispiel:
Fotobearbeitung und Videoeffekte.
Erstellen von „Erinnerungen“ und Alben.
Videomontage aus einer Fotoserie.
…und noch viel mehr.
Als Beispiel verwenden wir hier eine recht primitive Implementierung eines solchen Dienstes. Der Hauptpunkt besteht darin, ein mögliches Szenario der gemeinsamen Verwendung von WeakRef
und FinalizationRegistry
im wirklichen Leben aufzuzeigen.
So sieht es aus:
Auf der linken Seite befindet sich eine Cloud-Bibliothek mit Fotos (sie werden als Miniaturansichten angezeigt). Wir können die benötigten Bilder auswählen und eine Collage erstellen, indem wir rechts auf der Seite auf die Schaltfläche „Collage erstellen“ klicken. Anschließend kann die resultierende Collage als Bild heruntergeladen werden.
Um die Ladegeschwindigkeit der Seite zu erhöhen, wäre es sinnvoll, Foto-Miniaturansichten in komprimierter Qualität herunterzuladen und anzuzeigen. Um jedoch eine Collage aus ausgewählten Fotos zu erstellen, laden Sie sie herunter und verwenden Sie sie in Vollbildqualität .
Unten sehen wir, dass die eigentliche Größe der Miniaturansichten 240 x 240 Pixel beträgt. Die Größe wurde bewusst gewählt, um die Ladegeschwindigkeit zu erhöhen. Darüber hinaus benötigen wir im Vorschaumodus keine Fotos in voller Größe.
Nehmen wir an, wir müssen eine Collage aus 4 Fotos erstellen: Wir wählen sie aus und klicken dann auf die Schaltfläche „Collage erstellen“. In diesem Stadium prüft die uns bereits bekannte Funktion weakRefCache
, ob sich das benötigte Bild im Cache befindet. Wenn nicht, lädt es es aus der Cloud herunter und legt es zur weiteren Verwendung im Cache ab. Dies geschieht für jedes ausgewählte Bild:
Wenn Sie auf die Ausgabe in der Konsole achten, können Sie sehen, welche der Fotos aus der Cloud heruntergeladen wurden – dies wird durch FETCHED_IMAGE angezeigt. Da dies der erste Versuch ist, eine Collage zu erstellen, bedeutet dies, dass zu diesem Zeitpunkt der „schwache Cache“ noch leer war und alle Fotos aus der Cloud heruntergeladen und dort abgelegt wurden.
Neben dem Herunterladen von Bildern findet jedoch auch eine Speicherbereinigung durch den Garbage Collector statt. Das bedeutet, dass das im Cache gespeicherte Objekt, auf das wir mit einer schwachen Referenz verweisen, vom Garbage Collector gelöscht wird. Und unser Finalizer wird erfolgreich ausgeführt, wodurch der Schlüssel gelöscht wird, mit dem das Bild im Cache gespeichert wurde. CLEANED_IMAGE benachrichtigt uns darüber:
Als nächstes stellen wir fest, dass uns die resultierende Collage nicht gefällt, und beschließen, eines der Bilder zu ändern und ein neues zu erstellen. Deaktivieren Sie dazu einfach das unnötige Bild, wählen Sie ein anderes aus und klicken Sie erneut auf die Schaltfläche „Collage erstellen“:
Diesmal wurden jedoch nicht alle Bilder aus dem Netzwerk heruntergeladen, und eines davon wurde aus dem schwachen Cache entnommen: Die Meldung CACHED_IMAGE informiert uns darüber. Das bedeutet, dass der Garbage Collector unser Bild zum Zeitpunkt der Collagenerstellung noch nicht gelöscht hatte und wir es mutig aus dem Cache genommen haben, wodurch die Anzahl der Netzwerkanfragen reduziert und die Gesamtzeit des Collagenerstellungsprozesses beschleunigt wurde:
Lassen Sie uns noch ein wenig „herumspielen“, indem wir eines der Bilder erneut ersetzen und eine neue Collage erstellen:
Dieses Mal ist das Ergebnis noch beeindruckender. Von den 4 ausgewählten Bildern wurden 3 aus dem schwachen Cache entnommen und nur eines musste aus dem Netzwerk heruntergeladen werden. Die Reduzierung der Netzwerklast betrug etwa 75 %. Beeindruckend, nicht wahr?
Natürlich ist es wichtig zu bedenken, dass ein solches Verhalten nicht garantiert ist und von der spezifischen Implementierung und dem Betrieb des Garbage Collectors abhängt.
Daraus ergibt sich sofort eine völlig logische Frage: Warum verwenden wir nicht einen gewöhnlichen Cache, in dem wir seine Entitäten selbst verwalten können, anstatt uns auf den Garbage Collector zu verlassen? Das ist richtig, in den allermeisten Fällen besteht keine Notwendigkeit WeakRef
und FinalizationRegistry
zu verwenden.
Hier haben wir lediglich eine alternative Implementierung ähnlicher Funktionalität demonstriert und dabei einen nicht trivialen Ansatz mit interessanten Sprachfunktionen verwendet. Dennoch können wir uns nicht auf dieses Beispiel verlassen, wenn wir ein konstantes und vorhersehbares Ergebnis benötigen.
Sie können dieses Beispiel in der Sandbox öffnen.
WeakRef
– Entwickelt, um schwache Referenzen auf Objekte zu erstellen, sodass diese vom Garbage Collector aus dem Speicher gelöscht werden können, wenn keine starken Referenzen mehr auf sie vorhanden sind. Dies ist vorteilhaft, um einer übermäßigen Speichernutzung entgegenzuwirken und die Nutzung von Systemressourcen in Anwendungen zu optimieren.
FinalizationRegistry
– ist ein Tool zum Registrieren von Rückrufen, die ausgeführt werden, wenn Objekte zerstört werden, auf die nicht mehr stark verwiesen wird. Dadurch können mit dem Objekt verknüpfte Ressourcen freigegeben oder andere notwendige Vorgänge ausgeführt werden, bevor das Objekt aus dem Speicher gelöscht wird.