LMKs debuggen

Das Beheben von LMKs in Ihrem Unity-Spiel ist ein systematischer Prozess:

Abbildung 1: Schritte zur Behebung von LMKs (Low Memory Kills) in Unity-Spielen.

Speichersnapshot erstellen

Verwenden Sie den Unity Profiler, um einen von Unity verwalteten Memory-Snapshot zu erstellen. Abbildung 2 zeigt die Speichermanagementebenen, die Unity verwendet, um den Speicher in Ihrem Spiel zu verwalten.

Abbildung 2. Übersicht über die Arbeitsspeicherverwaltung von Unity.

Verwalteter Arbeitsspeicher

Das Speichermanagement von Unity implementiert eine kontrollierte Speicherebene, die einen verwalteten Heap und einen Garbage Collector verwendet, um Speicher automatisch zuzuweisen. Das verwaltete Speichersystem ist eine C#-Scripting-Umgebung, die auf Mono oder IL2CPP basiert. Der Vorteil des verwalteten Speichersystems besteht darin, dass es einen Garbage Collector verwendet, um Speicherzuweisungen automatisch freizugeben.

Nicht verwalteter Speicher in C#

Die nicht verwaltete C#-Arbeitsspeicherebene bietet Zugriff auf die native Arbeitsspeicherebene und ermöglicht eine präzise Steuerung der Arbeitsspeicherzuweisungen bei Verwendung von C#-Code. Auf diese Speichermanagementebene kann über den Namespace Unity.Collections und über Funktionen wie UnsafeUtility.Malloc und UnsafeUtility.Free zugegriffen werden.

Nativer Arbeitsspeicher

Der interne C/C++-Kern von Unity verwendet ein natives Speichersystem, um Szenen, Assets, Grafik-APIs, Treiber, Subsysteme und Plug-in-Puffer zu verwalten. Der direkte Zugriff ist zwar eingeschränkt, aber Sie können Daten mit der C#-API von Unity sicher bearbeiten und von effizientem nativen Code profitieren. Der native Speicher erfordert selten eine direkte Interaktion. Sie können jedoch die Auswirkungen des nativen Speichers auf die Leistung mit dem Profiler beobachten und die Einstellungen anpassen, um die Leistung zu optimieren.

Der Speicher wird nicht zwischen C# und nativem Code freigegeben, wie in Abbildung 3 dargestellt. Daten, die von C# benötigt werden, werden jedes Mal, wenn sie benötigt werden, im verwalteten Speicherbereich zugewiesen.

Damit der Code des verwalteten Spiels (C#) auf die nativen Speicherdaten der Engine zugreifen kann, wird beispielsweise bei einem Aufruf von GameObject.transform ein nativer Aufruf ausgeführt, um auf Speicherdaten im nativen Bereich zuzugreifen. Anschließend werden Werte mithilfe von Bindings an C# zurückgegeben. Bindungen sorgen für die richtigen Aufrufkonventionen für jede Plattform und übernehmen das automatische Marshalling verwalteter Typen in ihre nativen Entsprechungen.

Dies geschieht nur beim ersten Mal, da die verwaltete Shell für den Zugriff auf die transform-Eigenschaft im nativen Code beibehalten wird. Durch das Caching der transform-Eigenschaft kann die Anzahl der Hin- und Her-Aufrufe zwischen verwaltetem und nativem Code reduziert werden. Die Nützlichkeit des Caching hängt jedoch davon ab, wie oft die Eigenschaft verwendet wird. Außerdem werden bei Zugriff auf diese APIs keine Teile des nativen Speichers in den verwalteten Speicher kopiert.

Abbildung 3: Zugriff auf nativen Speicher über verwalteten C#-Code

Weitere Informationen finden Sie unter Arbeitsspeicher in Unity – Einführung.

Außerdem ist es wichtig, ein Speicherbudget festzulegen, damit das Spiel reibungslos läuft. Durch die Implementierung eines Analyse- oder Berichtssystems für den Speicherverbrauch wird sichergestellt, dass jede neue Version das Speicherbudget nicht überschreitet. Eine weitere Strategie, um bessere Einblicke zu erhalten, ist die Integration von Play-Modus-Tests in Ihre Continuous Integration (CI), um den Speicherverbrauch in bestimmten Bereichen des Spiels zu überprüfen.

Assets verwalten

Dies ist der wichtigste und am besten zu beeinflussende Teil des Arbeitsspeicherverbrauchs. Profil so früh wie möglich erstellen.

Die Speichernutzung in Android-Spielen kann je nach Art des Spiels, Anzahl und Art der Assets sowie Strategien zur Speicheroptimierung erheblich variieren. Zu den häufigsten Faktoren, die die Speichernutzung beeinflussen, gehören Texturen, Meshes, Audiodateien, Shader, Animationen und Skripts.

Doppelte Assets erkennen

Im ersten Schritt müssen Sie mit dem Memory Profiler, einem Build-Berichtstool oder dem Project Auditor schlecht konfigurierte Assets und doppelte Assets erkennen.

Strukturen

Analysieren Sie die Geräteunterstützung Ihres Spiels und legen Sie das richtige Texturformat fest. Mit Play Asset Delivery, Addressable oder einem manuelleren Prozess mit einem AssetBundle können Sie die Textur-Bundles für High-End- und Low-End-Geräte aufteilen.

Halte dich an die bekanntesten Empfehlungen, die im Beitrag Optimize Your Mobile Game Performance und im Beitrag Optimising Unity Texture Import Settings verfügbar sind. Probieren Sie dann Folgendes:

  • Komprimieren Sie Texturen mit ASTC-Formaten, um den Speicherbedarf zu reduzieren, und testen Sie eine höhere Blockrate, z. B. 8 × 8.

    Wenn ETC2 erforderlich ist, packen Sie Ihre Texturen in Atlas. Wenn Sie mehrere Texturen in einer einzigen Textur platzieren, wird sichergestellt, dass sie eine Zweierpotenz ist. Außerdem können so Draw-Aufrufe reduziert und das Rendern beschleunigt werden.

  • Optimieren Sie das Texturformat und die Größe von RenderTarget. Vermeiden Sie unnötig hochauflösende Texturen. Wenn Sie auf Mobilgeräten kleinere Texturen verwenden, sparen Sie Arbeitsspeicher.

  • Verwenden Sie Texture Channel Packing, um Arbeitsspeicher für Texturen zu sparen.

Meshes und Modelle

Prüfen Sie zuerst die Grundeinstellungen (Seite 27) und dann die folgenden Einstellungen für den Mesh-Import:

  • Redundante und kleinere Meshes zusammenführen
  • Reduzieren Sie die Anzahl der Eckpunkte für Objekte in Szenen (z. B. statische oder entfernte Objekte).
  • Erstellen Sie LOD-Gruppen (Level of Detail) für Assets mit vielen geometrischen Details.

Materialien und Shader

  • Nicht verwendete Shader-Varianten während des Build-Prozesses programmatisch entfernen.
  • Fassen Sie häufig verwendete Shader-Varianten in Uber-Shadern zusammen, um Shader-Duplikate zu vermeiden.
  • Aktivieren Sie das dynamische Laden von Shadern, um den großen Speicherbedarf von vorab geladenen Shadern im VRAM/RAM zu reduzieren. Achten Sie jedoch darauf, ob die Shader-Kompilierung zu Rucklern führt.
  • Verwenden Sie das dynamische Laden von Shadern, um zu verhindern, dass alle Varianten geladen werden. Weitere Informationen finden Sie im Blogpost Improvements to shader build times and memory usage.
  • Verwenden Sie Material-Instancing richtig, indem Sie MaterialPropertyBlocks nutzen.

Audio

Prüfen Sie zuerst die Grundeinstellungen (Seite 41) und dann die folgenden Einstellungen für den Import von Meshs:

  • Entfernen Sie nicht verwendete oder redundante AudioClip-Referenzen, wenn Sie Audio-Engines von Drittanbietern wie FMOD oder Wwise verwenden.
  • Audiodaten vorab laden. Deaktivieren Sie das Vorladen für Clips, die während der Laufzeit oder beim Start einer Szene nicht sofort benötigt werden. Dadurch wird der Speicheraufwand bei der Initialisierung von Szenen reduziert.

Animationen

  • Passen Sie die Einstellungen für die Animationskomprimierung von Unity an, um die Anzahl der Keyframes zu minimieren und redundante Daten zu entfernen.
    • Keyframe-Reduzierung: Unnötige Keyframes werden automatisch entfernt.
    • Quaternion-Komprimierung: Komprimiert Rotationsdaten, um den Arbeitsspeicherverbrauch zu reduzieren

Sie können die Komprimierungseinstellungen auf dem Tab Rig oder Animation in den Animation Import Settings (Einstellungen für den Import von Animationen) anpassen.

  • Verwenden Sie Animationsclips wieder, anstatt sie für verschiedene Objekte zu duplizieren.

    Mit Animator Override Controllers können Sie einen AnimatorController wiederverwenden und bestimmte Clips für verschiedene Charaktere ersetzen.

  • Physikbasierte Animationen rendern: Wenn Ihre Animationen physikbasiert oder prozedural sind, rendern Sie sie in Animationsclips, um Berechnungen zur Laufzeit zu vermeiden.

  • Skelett-Rig optimieren: Verwenden Sie weniger Bones in Ihrem Rig, um die Komplexität und den Speicherverbrauch zu reduzieren.

    • Vermeiden Sie zu viele Bones für kleine oder statische Objekte.
    • Wenn bestimmte Bones nicht animiert werden oder nicht benötigt werden, entfernen Sie sie aus dem Rig.
  • Die Länge von Animationsclips reduzieren.

    • Schneiden Sie Animationsclips so zu, dass nur die erforderlichen Frames enthalten sind. Vermeiden Sie es, ungenutzte oder übermäßig lange Animationen zu speichern.
    • Verwende Animationen in Schleife, anstatt lange Clips für wiederholte Bewegungen zu erstellen.
  • Es darf nur eine Animationskomponente angehängt oder aktiviert sein. Deaktivieren oder entfernen Sie beispielsweise Legacy Animation-Komponenten, wenn Sie Animator verwenden.

  • Vermeiden Sie die Verwendung des Animator, wenn er nicht erforderlich ist. Für einfache visuelle Effekte können Sie Tweening-Bibliotheken verwenden oder den visuellen Effekt in einem Script implementieren. Das Animator-System kann ressourcenintensiv sein, insbesondere auf Low-End-Mobilgeräten.

  • Verwenden Sie das Job-System für Animationen, wenn Sie eine große Anzahl von Animationen verarbeiten, da dieses System vollständig überarbeitet wurde, um speichereffizienter zu sein.

Ambiente-Optionen

Wenn neue Szenen geladen werden, werden Assets als Abhängigkeiten hinzugefügt. Ohne eine ordnungsgemäße Verwaltung des Asset-Lebenszyklus werden diese Abhängigkeiten jedoch nicht von Referenzzählern überwacht. Daher können Assets im Arbeitsspeicher verbleiben, auch nachdem die nicht verwendeten Szenen entladen wurden, was zu einer Speicherfragmentierung führt.

  • Verwenden Sie Unitys Object Pooling, um GameObject-Instanzen für wiederkehrende Gameplay-Elemente wiederzuverwenden, da beim Object Pooling ein Stack verwendet wird, um eine Sammlung von Objektinstanzen zur Wiederverwendung zu speichern. Object Pooling ist nicht threadsicher. Durch die Minimierung von Instantiate und Destroy werden sowohl die CPU-Leistung als auch die Speicherstabilität verbessert.
  • Assets werden entladen:
    • Laden Sie Assets strategisch in weniger kritischen Momenten, z. B. auf Splashscreens oder Ladebildschirmen, aus dem Speicher.
    • Die häufige Verwendung von Resources.UnloadUnusedAssets führt zu Spitzen bei der CPU-Verarbeitung, da umfangreiche interne Vorgänge zur Überwachung von Abhängigkeiten ausgeführt werden.
    • Suchen Sie im Profilmarker GC.MarkDependencies nach großen CPU-Spitzen. Entfernen Sie die Ausführungshäufigkeit oder reduzieren Sie sie und entladen Sie bestimmte Ressourcen manuell mit Resources.UnloadAsset, anstatt sich auf die umfassende Resources.UnloadUnusedAssets() zu verlassen.
  • Strukturieren Sie Szenen neu, anstatt ständig „Resources.UnloadUnusedAssets“ zu verwenden.
  • Wenn Resources.UnloadUnusedAssets() für Addressables aufgerufen wird, können dynamisch geladene Bundles unbeabsichtigt entladen werden. Verwalten Sie den Lebenszyklus dynamisch geladener Assets sorgfältig.

Sonstiges

  • Fragmentierung durch Szenenübergänge: Wenn die Methode Resources.UnloadUnusedAssets() aufgerufen wird, führt Unity Folgendes aus:

    • Gibt Arbeitsspeicher für nicht mehr verwendete Assets frei
    • Führt einen Vorgang wie die Garbage Collection aus, um den Heap für verwaltete und native Objekte nach ungenutzten Assets zu durchsuchen und sie zu entladen.
    • Gibt Arbeitsspeicher für Texturen, Meshes und Assets frei, sofern keine aktive Referenz vorhanden ist.
  • AssetBundle oder Addressable: Änderungen in diesem Bereich sind komplex und erfordern eine gemeinsame Anstrengung des Teams, um die Strategien umzusetzen. Sobald diese Strategien jedoch beherrscht werden, verbessern sie die Speichernutzung erheblich, reduzieren die Downloadgröße und senken die Cloud-Kosten. Weitere Informationen zur Asset-Verwaltung in Unity finden Sie unter Addressables.

  • Zentrale freigegebene Abhängigkeiten: Gruppieren Sie freigegebene Abhängigkeiten wie Shader, Texturen und Schriftarten systematisch in dedizierten Bundles oder Addressable-Gruppen. Dadurch werden Duplikate reduziert und unnötige Assets effizient entladen.

  • Addressables für die Abhängigkeitsverfolgung verwenden: Addressables vereinfachen das Laden und Entladen und können automatisch Abhängigkeiten entladen, auf die nicht mehr verwiesen wird. Die Umstellung auf Addressables für die Inhaltsverwaltung und die Auflösung von Abhängigkeiten kann je nach Spiel eine praktikable Lösung sein. Analysieren Sie Abhängigkeitsketten mit dem Tool „Analyze“, um unnötige Duplikate oder Abhängigkeiten zu ermitteln. Wenn Sie AssetBundles verwenden, können Sie auch die Unity Data Tools verwenden.

  • TypeTrees: Wenn die Addressables und AssetBundles Ihres Spiels mit derselben Unity-Version wie der Player erstellt und bereitgestellt werden und keine Abwärtskompatibilität mit anderen Player-Builds erforderlich ist, sollten Sie in Erwägung ziehen, das Schreiben von TypeTree zu deaktivieren. Dadurch sollte die Bundle-Größe und der Speicherbedarf für serialisierte Dateiobjekte reduziert werden. Ändern Sie den Build-Prozess in der lokalen Addressables-Paketeinstellung ContentBuildFlags zu DisableWriteTypeTree.

Code schreiben, der für die automatische Speicherbereinigung geeignet ist

Unity verwendet die automatische Speicherbereinigung (Garbage Collection, GC), um den Arbeitsspeicher zu verwalten. Dabei wird nicht verwendeter Arbeitsspeicher automatisch identifiziert und freigegeben. Die Garbage Collection ist zwar unerlässlich, kann aber zu Leistungsproblemen (z. B. Frame-Rate-Spitzen) führen, wenn sie nicht richtig gehandhabt wird. Der Prozess kann das Spiel kurzzeitig pausieren, was zu Leistungseinbußen und einer suboptimalen Nutzererfahrung führt.

Im Unity-Handbuch finden Sie nützliche Techniken zum Reduzieren der Häufigkeit von Zuweisungen im verwalteten Heap. Beispiele finden Sie auf Seite 271 der UnityPerformanceTuningBible.

  • Zuweisungen für die Garbage Collection reduzieren:

    • Vermeiden Sie LINQ, Lambdas und Closures, da diese Heapspeicher zuweisen.
    • Verwenden Sie StringBuilder für veränderliche Strings anstelle der Stringverkettung.
    • Verwenden Sie Sammlungen wieder, indem Sie COLLECTIONS.Clear() aufrufen, anstatt sie neu zu instanziieren.

    Weitere Informationen finden Sie im E-Book Ultimate Guide to Profiling Unity Games.

  • UI-Canvas-Updates verwalten:

    • Dynamische Änderungen an UI-Elementen: Wenn UI-Elemente wie Text, Bild oder RectTransform-Eigenschaften aktualisiert werden (z. B. durch Ändern von Textinhalten, Anpassen der Größe von Elementen oder Animieren von Positionen), kann die Engine Speicher für temporäre Objekte zuweisen.
    • String-Zuweisungen: Für UI-Elemente wie Text sind häufig String-Aktualisierungen erforderlich, da Strings in den meisten Programmiersprachen unveränderlich sind.
    • „Dirty Canvas“: Wenn sich etwas auf einem Arbeitsbereich ändert (z. B. durch Ändern der Größe, Aktivieren und Deaktivieren von Elementen oder Ändern von Layouteigenschaften), wird möglicherweise der gesamte Arbeitsbereich oder ein Teil davon als dirty markiert und neu erstellt. Dadurch kann die Erstellung temporärer Datenstrukturen (z. B. Mesh-Daten, Vertex-Puffer oder Layoutberechnungen) ausgelöst werden, was zu mehr Garbage führt.
    • Komplexe oder häufige Aktualisierungen: Wenn das Canvas eine große Anzahl von Elementen enthält oder häufig aktualisiert wird (z. B. in jedem Frame), können diese Neuerstellungen zu einem erheblichen Speicherverbrauch führen.
  • Inkrementelle Garbage Collection aktivieren, um große Spitzen bei der Garbage Collection zu reduzieren, indem die Bereinigung der Zuweisung auf mehrere Frames verteilt wird. Profil erstellen, um zu prüfen, ob diese Option die Leistung und den Speicherbedarf Ihres Spiels verbessert.

  • Wenn für Ihr Spiel ein kontrollierter Ansatz erforderlich ist, stellen Sie den Garbage Collection-Modus auf „Manuell“ ein. Rufen Sie die Garbage Collection dann bei einem Levelwechsel oder zu einem anderen Zeitpunkt ohne aktives Gameplay auf.

  • Rufen Sie die manuelle automatische Speicherbereinigung mit GC.Collect() für Übergänge des Spielstatus auf, z. B. beim Wechsel des Levels.

  • Optimieren Sie Arrays, indem Sie mit einfachen Codepraktiken beginnen und bei Bedarf native Arrays oder andere native Container für große Arrays verwenden.

  • Überwachen Sie verwaltete Objekte mit Tools wie dem Unity Memory Profiler, um nicht verwaltete Objektreferenzen zu verfolgen, die nach der Zerstörung bestehen bleiben.

    Verwenden Sie eine Profiler-Markierung, um sie an das Tool für Leistungsberichte zu senden und so einen automatisierten Ansatz zu nutzen.

Speicherlecks und ‑fragmentierung vermeiden

Speicherlecks

Wenn im C#-Code nach dem Löschen eines Unity-Objekts eine Referenz auf das Objekt vorhanden ist, verbleibt das verwaltete Wrapper-Objekt, die sogenannte Managed Shell, im Arbeitsspeicher. Der mit der Referenz verknüpfte native Speicher wird freigegeben, wenn die Szene entladen wird oder wenn das GameObject, an das der Speicher angehängt ist, oder eines seiner übergeordneten Objekte mit der Methode Destroy() zerstört wird. Wenn andere Verweise auf die Szene oder das GameObject jedoch nicht gelöscht wurden, kann der verwaltete Speicher als Leaked Shell-Objekt bestehen bleiben. Weitere Informationen zu Managed Shell-Objekten finden Sie im Handbuch Managed Shell Objects.

Außerdem können Speicherlecks durch Ereignisabos, Lambdas und Closures, Stringverkettungen und eine falsche Verwaltung von gepoolten Objekten verursacht werden:

  • Weitere Informationen finden Sie unter Speicherlecks finden.
  • Prüfen Sie, ob Ereignisabos und Speicherlecks vorhanden sind. Wenn Objekte Ereignisse abonnieren (z. B. über Delegaten oder UnityEvents), sich aber nicht ordnungsgemäß abmelden, bevor sie zerstört werden, behält der Event-Manager oder Publisher möglicherweise Verweise auf diese Objekte bei. Dadurch wird verhindert, dass diese Objekte automatisch bereinigt werden, was zu Speicherlecks führt.
  • Globale oder Singleton-Klassenereignisse beobachten, die beim Löschen von Objekten nicht abgemeldet werden. Beispiel: Abmelden oder Aufheben der Verknüpfung von Delegaten in Objektdestruktoren.
  • Achten Sie darauf, dass durch das Löschen von gepoolten Objekten Verweise auf Text-Mesh-Komponenten, Texturen und übergeordnete GameObjects vollständig aufgehoben werden.
  • Wenn Sie Unity Memory Profiler-Snapshots vergleichen und einen Unterschied beim Arbeitsspeicherverbrauch ohne ersichtlichen Grund feststellen, kann der Unterschied durch den Grafiktreiber oder das Betriebssystem selbst verursacht werden.

Speicherfragmentierung

Eine Speicherfragmentierung tritt auf, wenn viele kleine Zuweisungen in zufälliger Reihenfolge freigegeben werden. Heap-Zuweisungen erfolgen sequenziell. Das bedeutet, dass neue Speicherblöcke erstellt werden, wenn der vorherige Block keinen Speicherplatz mehr hat. Daher werden neue Objekte nicht in die leeren Bereiche alter Chunks eingefügt, was zu einer Fragmentierung führt. Außerdem können große temporäre Zuweisungen während der gesamten Spielsitzung zu einer permanenten Fragmentierung führen.

Dieses Problem ist besonders problematisch, wenn kurzlebige große Zuweisungen in der Nähe von langlebigen Zuweisungen erfolgen.

Gruppieren Sie Zuweisungen nach ihrer Lebensdauer. Idealerweise sollten langlebige Zuweisungen gemeinsam und früh im Lebenszyklus der Anwendung erfolgen.

Beobachter und Eventmanager

  • Zusätzlich zu dem im Abschnitt 77 (Speicherlecks) erwähnten Problem können Speicherlecks im Laufe der Zeit zur Fragmentierung beitragen, indem sie ungenutzten Speicher für Objekte reservieren, die nicht mehr verwendet werden.
  • Achten Sie darauf, dass durch das Löschen von gepoolten Objekten alle Verweise auf Text-Mesh-Komponenten, Texturen und übergeordnete GameObjects vollständig aufgehoben werden.
  • Eventmanager erstellen und speichern häufig Listen oder Wörterbücher, um Eventabos zu verwalten. Wenn diese während der Laufzeit dynamisch vergrößert und verkleinert werden, kann dies aufgrund häufiger Zuweisungen und Freigaben zu einer Speicherfragmentierung führen.

Code

  • Coroutinen weisen manchmal Speicher zu. Das lässt sich leicht vermeiden, indem Sie die Rückgabeanweisung des IEnumerator zwischenspeichern, anstatt jedes Mal eine neue zu deklarieren.
  • Behalten Sie die Lebenszyklusstatus von zusammengefassten Objekten im Blick, um UnityEngine.Object-Geisterreferenzen zu vermeiden.

Assets

  • Verwenden Sie dynamische Fallback-Systeme für textbasierte Spiele, um zu vermeiden, dass alle Schriftarten für mehrsprachige Fälle vorab geladen werden.
  • Organisieren Sie Assets (z. B. Texturen und Partikel) nach Typ und erwartetem Lebenszyklus.
  • Reduzieren Sie Assets mit Attributen für den Leerlauf-Lebenszyklus, z. B. redundante UI-Bilder und statische Meshes.

Zuweisungen basierend auf der Lebensdauer

  • Weisen Sie langlebige Assets zu Beginn des Anwendungslebenszyklus zu, um kompakte Zuweisungen zu gewährleisten.
  • Verwenden Sie NativeCollections oder benutzerdefinierte Zuweisungen für speicherintensive oder temporäre Datenstrukturen (z. B. Physik-Cluster).

Auch die ausführbare Datei des Spiels und die Plug-ins wirken sich auf die Speichernutzung aus.

IL2CPP-Metadaten

IL2CPP generiert zur Build-Zeit Metadaten für jeden Typ (z. B. Klassen, Generics und Delegates), die dann zur Laufzeit für die Reflektion, die Typüberprüfung und andere laufzeitspezifische Vorgänge verwendet werden. Diese Metadaten werden im Arbeitsspeicher gespeichert und können erheblich zum gesamten Speicherbedarf der Anwendung beitragen. Der Metadatencache von IL2CPP trägt erheblich zu Initialisierungs- und Ladezeiten bei. Außerdem werden bestimmte Metadatenelemente (z. B. generische Typen oder serialisierte Informationen) in IL2CPP nicht dedupliziert, was zu einer aufgeblähten Speicherauslastung führen kann. Dies wird durch die wiederholte oder redundante Verwendung von Typen im Projekt noch verstärkt.

IL2CPP-Metadaten können reduziert werden, indem Sie:

  • Vermeiden Sie die Verwendung von Reflection-APIs, da sie einen erheblichen Beitrag zu IL2CPP-Metadatenzuweisungen leisten können.
  • Integrierte Pakete deaktivieren
  • Implementierung von Unity 2022 Full Generic Sharing, wodurch der durch Generics verursachte Overhead reduziert werden sollte. Um die Zuweisungen noch weiter zu reduzieren, sollten Sie jedoch die Verwendung von Generics einschränken.

Entfernen von Code

Durch das Entfernen von Code wird nicht nur die Größe des Builds reduziert, sondern auch die Speichernutzung. Beim Erstellen mit dem IL2CPP-Scripting-Backend wird durch das Entfernen von verwaltetem Bytecode (standardmäßig aktiviert) ungenutzter Code aus verwalteten Assemblys entfernt. Dabei werden Stamm-Assemblies definiert und dann mithilfe der statischen Codeanalyse ermittelt, welcher andere verwaltete Code von diesen Stamm-Assemblies verwendet wird. Nicht erreichbarer Code wird entfernt. Weitere Informationen zum Entfernen von verwaltetem Code finden Sie im Blogpost TTales from the optimization trenches: Better managed code stripping with Unity 2020 LTS und in der Dokumentation zum Entfernen von verwaltetem Code.

Native Zuweisungen

Experimentieren Sie mit nativen Speicherzuweisungen, um Speicherzuweisungen zu optimieren. Wenn das Spiel wenig Arbeitsspeicher hat, verwenden Sie kleinere Speicherblöcke, auch wenn dies langsamere Zuweisungen erfordert. Weitere Informationen finden Sie im Beispiel für die dynamische Heap-Zuweisung.

Native Plug-ins und SDKs verwalten

  • Problematisches Plug-in finden: Entfernen Sie jedes Plug-in und vergleichen Sie die Speicher-Snapshots des Spiels. Dazu gehört, viele Codefunktionen mit Scripting Define Symbols zu deaktivieren und stark gekoppelte Klassen mit Schnittstellen umzugestalten. Weitere Informationen finden Sie unter Code mit Mustern für die Spieleprogrammierung optimieren. So können Sie externe Abhängigkeiten deaktivieren, ohne dass Ihr Spiel unspielbar wird.

  • Plugin- oder SDK-Autor kontaktieren: Die meisten Plugins sind nicht Open Source.

  • Plug‑in-Speichernutzung reproduzieren: Sie können ein einfaches Plug‑in schreiben (verwenden Sie dieses Unity-Plug‑in als Referenz), das Speicherzuweisungen vornimmt. Sehen Sie sich die Memory-Snapshots mit Android Studio an, da Unity diese Zuweisungen nicht erfasst. Alternativ können Sie die Klasse MemoryInfo und die Methode Runtime.totalMemory() im selben Projekt aufrufen.

Ein Unity-Plug-in weist Java- und nativen Arbeitsspeicher zu. So gehts:

Java

byte[] largeObject = new byte[1024 * 1024 * megaBytes];
list.add(largeObject);

Nativ

char* buffer = new char[megabytes * 1024 * 1024];

// Random data to fill the buffer
for (int i = 1; i < megabytes * 1024 * 1024; ++i) {
   buffer[i] = 'A' + (i % 26); // Fill with letters A-Z
}