Aby skutecznie zoptymalizować wykorzystanie pamięci przez grę, musisz najpierw zrozumieć, jak platforma Android mierzy pamięć oraz jak korzystać z telemetrii systemowej, interfejsów API diagnostyki i narzędzi do profilowania. Z tego przewodnika dowiesz się, jak monitorować, rejestrować i analizować alokacje pamięci w grze zgodnie z nowymi wytycznymi platformy.
Interpretowanie danych RSS i przestrzeni wymiany
Aby skutecznie analizować i debugować zachowanie pamięci w grze, musisz znać dokładne dane techniczne, których platforma Android używa do egzekwowania zasad dotyczących pamięci. Szczegółowe informacje o tym, jak ten parametr telemetrii jest przetwarzany i monitorowany w rzeczywistych warunkach, znajdziesz w dokumentacji Android Vitals – Wykorzystanie pamięci (anonimowy RSS + przestrzeń wymiany).
1. Anonimowy RSS (RssAnon)
Rozmiar RSS (Resident Set Size) mierzy część pamięci zajętą przez proces, która jest przechowywana w fizycznej pamięci RAM urządzenia. RSS jest podzielony na pamięć obsługiwaną przez pliki i pamięć anonimową. Dane egzekwowania zasad w Androidzie skupiają się wyłącznie na anonimowym RSS:
- Co obejmuje: strony pamięci przydzielone bezpośrednio przez proces gry które nie są powiązane z fizycznym plikiem w pamięci. Te strony obejmują sterty Java lub Kotlin, stosy wykonywania wątków i, co najważniejsze, alokacje pamięci natywnej (takie jak niestandardowe alokatory silnika C++ lub bloki pamięci żądane za pomocą natywnego malloc lub new i zmodyfikowane przez logikę gry). Więcej informacji o tym wskaźniku znajdziesz w słowniku Pamięć procesu (RSS).
- Dlaczego jest to ważne: silniki gier używają ogromnych pul pamięci natywnej do obsługi fizyki, renderowania i logiki. Ponieważ te pule nie są obsługiwane przez pliki, znajdują się w całości w anonimowym RSS i stanowią większość fizycznego rozmiaru gry.
2. Nieskompresowana przestrzeń wymiany (VmSwap)
Android nie obsługuje tradycyjnej przestrzeni wymiany opartej na dysku ze względu na zużycie pamięci flash i ograniczenia dotyczące opóźnienia. Zamiast tego używa zRAM (nieskompresowanej przestrzeni wymiany):
- Co obejmuje: gdy wzrasta obciążenie fizycznej pamięci RAM, demon zarządzania pamięcią jądra kompresuje nieaktywne strony anonimowe i przenosi je do dedykowanej, nieskompresowanej części fizycznej pamięci RAM (zRAM).
- Obliczanie danych: system śledzi to na podstawie nieskompresowanego rozmiaru (VmSwap), aby ocenić rzeczywiste zapotrzebowanie gry na pamięć fizyczną. Jeśli gra przydziela pamięć, a system przenosi ją do zRAM, nadal jest ona wliczana do całkowitego rozmiaru pamięci gry.
3. Stany procesu
Wykorzystanie pamięci jest podzielone według stanów procesu w Android Vitals. W przypadku deweloperów gier zewnętrzne pakiety SDK lub gry mogą też nieoczekiwanie uruchamiać usługi widoczne dla użytkownika lub usługi działające w tle.
- Co obejmuje: pierwszy plan, usługi widoczne, tło i pamięć podręczna.
- Dlaczego jest to ważne: różne stany procesu mają różny wpływ na zarządzanie pamięcią w systemie operacyjnym Android. Możesz nie wiedzieć, że gra działa w stanie procesu, który wymaga szczególnej uwagi, jeśli któryś z zewnętrznych pakietów SDK nieumyślnie uruchomi zadanie w tle. Sprawdź, czy gra działa w tle
za pomocą
RunningAppProcessInfo.
Interfejsy programowania aplikacji (API)
Android udostępnia interfejsy API systemu, które umożliwiają grze dynamiczne reagowanie na obciążenie pamięci i rejestrowanie szczegółowych informacji diagnostycznych dotyczących pamięci w czasie działania.
Reagowanie na zdarzenia przycinania pamięci
System używa onTrimMemory, aby powiadamiać aplikację o zdarzeniach cyklu życia, które
stanowią dobrą okazję do dobrowolnego zmniejszenia wykorzystania pamięci przez aplikację
i uniknięcia zamknięcia przez mechanizm LMK (low-memory killer), aby zwolnić pamięć na potrzeby
innych aplikacji.
Jeśli system zamknie aplikację w tle, użytkownik po wznowieniu zobaczy powolne uruchomienie „na zimno”. Zmniejszenie wykorzystania pamięci w tle pomaga zapobiegać takim zamknięciom.
Reagując na zdarzenia przycinania, zwalniaj duże, możliwe do odtworzenia alokacje pamięci, które nie są natychmiast potrzebne:
Przykład: przycinanie lub czyszczenie bitmap z pamięci podręcznej (dekodowanych z pamięci lokalnej) w odpowiedzi na
TRIM_MEMORY_UI_HIDDEN.
Kotlin
class MainActivity : AppCompatActivity(), ComponentCallbacks2 {
override fun onTrimMemory(level: Int) {
if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
// Release memory related to UI elements, such as bitmap caches.
}
if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
// Release memory related to background processing, such as by
// closing a database connection.
}
}
}
Java
public class MainActivity extends AppCompatActivity implements ComponentCallbacks2 {
public void onTrimMemory(int level) {
switch (level) {
if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
// Release memory related to UI elements, such as bitmap caches.
}
if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
// Release memory related to background processing, such as by
// closing a database connection.
}
}
}
}
ProfilingManager
Wprowadzony w Androidzie 15 (poziom API 35) interfejs API ProfilingManager umożliwia
aplikacjom rejestrowanie zdefiniowanych programowo migawek (takich jak profile
sterty, ślady systemowe i zrzuty sterty Java) bezpośrednio w czasie działania.
Deweloperzy mogą ręcznie wywoływać rejestrowanie w określonych scenach lub rejestrować automatyczne wyzwalacze, takie jak TRIGGER_TYPE_ANOMALY, aby automatycznie uruchamiać rejestrowanie, gdy proces gry naruszy progi ogranicznika pamięci. Deweloperzy gier muszą jednak uwzględnić krytyczne ograniczenia w nowoczesnych silnikach gier:
Uwaga: nowoczesne silniki gier (takie jak Unity czy Unreal) zarządzają wydajnością wykonywania, wstępnie przydzielając ogromne bloki pamięci wirtualnej z jądra za pomocą mmap z flagą MAP_ANONYMOUS. Silniki używają następnie niestandardowych alokatorów podrzędnych (np. natywnego menedżera pamięci Unity lub BinnedAllocators Unreal), aby wewnętrznie dzielić i przydzielać bloki pamięci.
ApplicationExitInfo
Jeśli gra zostanie zamknięta w tle lub zabita z powodu naruszenia limitów pamięci poszczególnych procesów, standardowe mechanizmy zrzutu awaryjnego Java lub natywnego (takie jak Firebase Crashlytics) nie zarejestrują tego zdarzenia. Aby programowo wysyłać zapytania o te
zamknięcia i je rejestrować, deweloperzy powinni na początku gry używać interfejsu API
ApplicationExitInfo.
- Implementacja: na początku wywołaj
ActivityManager.getHistoricalProcessExitReasons(), aby pobrać przyczyny zamknięcia ostatnich sesji. - Główne przyczyny zamknięcia związane z pamięcią:
REASON_LOW_MEMORY: wskazuje, że proces został zamknięty przez mechanizm LMK (low-memory killer) systemu. To zamknięcie następuje, gdy obciążenie pamięci w całym urządzeniu jest wysokie, a system musi odzyskać pamięć RAM. Ta przyczyna zamknięcia wskazuje, że rozmiar gry w tle jest zbyt duży, aby mogła ona współistnieć z innymi aplikacjami.REASON_MEMORY_LIMITER(Android 17 (poziom API 37) i nowsze): wskazuje, że proces został zamknięty, ponieważ przekroczył limit pamięci cgroup (RssAnon + VmSwap) przypisany przez ogranicznik pamięci platformy. To zamknięcie może nastąpić nawet wtedy, gdy w urządzeniu jest wystarczająco dużo fizycznej pamięci, co oznacza bezpośrednie naruszenie limitów poszczególnych procesów.
Korzystanie z dostępnych narzędzi
Podczas programowania i kontroli jakości używaj tych narzędzi platformy, aby dokładnie mierzyć wykorzystanie pamięci przez grę.
meminfo
To narzędzie zbiera statystyki dotyczące pamięci, aby pokazać, ile pamięci PSS zostało przydzielone i do jakich kategorii zostało użyte.
Statystyki meminfo możesz wydrukować na jeden z tych sposobów:
- Użyj polecenia
adb shell dumpsys meminfo package-name. - Użyj wywołania
MemoryInfoz Android Debug API.
Statystyka PrivateDirty pokazuje ilość pamięci RAM w procesie
, której nie można przenieść na dysk i która nie jest współdzielona z innymi procesami. Większość tej ilości staje się dostępna dla systemu, gdy proces zostanie zamknięty.
Punkty śledzenia pamięci
Punkty śledzenia pamięci śledzą ilość pamięci RSS używanej przez grę. Obliczanie wykorzystania pamięci RSS jest znacznie szybsze niż obliczanie wykorzystania pamięci PSS. Ponieważ obliczanie jest szybsze, RSS pokazuje większą szczegółowość zmian rozmiaru pamięci, co pozwala dokładniej mierzyć szczytowe wykorzystanie pamięci. Dzięki temu łatwiej zauważyć szczyty, które mogą spowodować brak pamięci (OOM) w grze.
Perfetto
Perfetto to pakiet narzędzi do zbierania informacji o wydajności i pamięci
na urządzeniu oraz wyświetlania ich w interfejsie internetowym. Obsługuje dowolnie długie ślady, dzięki czemu możesz zobaczyć, jak RSS zmienia się w czasie. Możesz też wykonywać zapytania SQL dotyczące danych, które generuje, na potrzeby przetwarzania offline. Włącz długie
ślady w aplikacji Śledzenie systemu. Upewnij się, że w przypadku śladu włączona jest kategoria memory:Memory. W przypadku niestandardowej instrumentacji pamięci podczas tworzenia i
testowania możesz też używać interfejsu API heapprofd (w wersji beta).
Sprawdzanie RssAnon i przestrzeni wymiany w Perfetto
Aby sprawdzić wpływ anonimowej pamięci gry i przestrzeni wymiany zRAM, wczytaj plik śladu w interfejsie internetowym na stronie ui.perfetto.dev i zastosuj te techniki analityczne, które zostały opracowane na potrzeby szczegółowych analiz pamięci (więcej informacji znajdziesz w artykule Perfetto Memory Analysis Case Studies):
1. Wyświetlanie liczników pamięci na osi czasu
- Znajdź swój proces: na liście nawigacyjnej wyszukaj nazwę pakietu lub procesu gry.
- Rozwiń grupę śladów: kliknij wiersz procesu, aby rozwinąć ślady wątków, i znajdź podgrupę o nazwie Pamięć.
- Analizuj ślady:
- mem.rss.anon (anonimowy RSS): ten wykres liniowy pokazuje fizyczną pamięć RAM zajętą w czasie rzeczywistym przez niezarządzane pule pamięci gry. Monitoruj tę oś czasu podczas wczytywania scen, wyskakujących okienek interfejsu lub przejść między rozgrywką, aby sprawdzić, czy nie występują wysokie szczyty alokacji.
- mem.swap (skompresowana przestrzeń wymiany lub VmSwap): ten wykres przedstawia rozmiar bloków pamięci przed skompresowaniem przeniesionych do zRAM. Wysoka aktywność przestrzeni wymiany zbiegająca się z rozgrywką wskazuje, że gra działa na urządzeniu z ograniczoną ilością pamięci, a system aktywnie kompresuje zasoby w tle.
2. Uruchamianie zapytań SQL (procesor śladów): aby przeprowadzić szczegółową analizę offline, możesz wykonywać zapytania SQL bezpośrednio w konsoli interfejsu Perfetto lub użyć samodzielnej biblioteki Trace Processor Python do obliczania szczytów statystycznych.
Znajdź szczytową alokację anonimowego RSS:
SELECT max(value) / 1024 / 1024 AS max_rss_anon_mb FROM counter JOIN counter_track ON counter.track_id = counter_track.id WHERE counter_track.name = 'mem.rss.anon' AND counter_track.upid IN ( SELECT upid FROM process WHERE name = 'your.game.package.name' );Koreluj RssAnon i VmSwap w dowolnej sygnaturze czasowej:
SELECT ts, track.name AS metric_type, value / 1024 / 1024 AS size_mb FROM counter JOIN counter_track track ON counter.track_id = track.id WHERE (track.name = 'mem.rss.anon' OR track.name = 'mem.swap') AND track.upid IN ( SELECT upid FROM process WHERE name = 'your.game.package.name' ) ORDER BY ts ASC;
Więcej informacji o sprawdzaniu plików śladów za pomocą Android Studio znajdziesz w artykule Sprawdzanie śladów systemowych: pamięć procesu (RSS). Więcej informacji o tworzeniu skryptów profili pamięci znajdziesz w artykule Rejestrowanie alokacji natywnych.
heapprofd
heapprofd to narzędzie do śledzenia pamięci, które jest częścią Perfetto. To narzędzie może pomóc w znalezieniu wycieków pamięci, pokazując, gdzie pamięć została przydzielona za pomocą malloc. heapprofd można uruchomić za pomocą skryptu Pythona. Ponieważ narzędzie ma niewielki narzut, nie wpływa na wydajność tak jak inne narzędzia, np. Malloc Debug.
bugreport
bugreport to narzędzie do rejestrowania, które pozwala sprawdzić, czy gra uległa awarii z powodu wyczerpania pamięci. Dane wyjściowe narzędzia są znacznie bardziej szczegółowe niż w przypadku użycia logcat. Jest to przydatne do debugowania pamięci, ponieważ pokazuje, czy gra uległa awarii z powodu wyczerpania pamięci, czy została zamknięta przez mechanizm LMK.
Więcej informacji znajdziesz w artykule Rejestrowanie i odczytywanie raportów o błędach.
Narzędzia silnika gry
O ile logi na poziomie platformy i telemetria systemowa są niezbędne do śledzenia progów i zgodności systemu operacyjnego, o tyle narzędzia specyficzne dla silnika gry pomagają przypisywać alokacje bezpośrednio do obiektów gry, zachowań skryptów i aktywnych hierarchii scen.
Unity
W środowisku Unity Engine możesz z dużą dokładnością oszacować wykorzystanie pamięci Android Anonymous RSS + Swap w czasie działania (zwykle z odchyleniem mniejszym niż 10% w porównaniu z rzeczywistymi wartościami na poziomie systemu operacyjnego) za pomocą natywnych narzędzi i klas profilowania Unity.
Pełny samouczek krok po kroku, w tym reguły konfiguracji i skrypty czasu działania, znajdziesz w artykule Jak sprawdzić pamięć za pomocą narzędzi Unity.
- Interfejs API profilera Unity: możesz programowo przybliżyć rozmiar niezarządzanej pamięci gry w czasie działania, wysyłając zapytania o podstawowe dane silnika:
- Używanie klasy Profiler: śledź łączną alokację pamięci, sumując
wartości
Profiler.GetTotalReservedMemoryLong()iProfiler.GetMonoHeapSizeLong(). - Używanie klasy
ProfilerRecorder: Monitoruj kategorie pamięci dynamicznie. Aby ustalić wiarygodną wartość bazową, pobierz łączną ilość zarezerwowanej pamięci (w przypadku kompilacji do publikacji) lub odejmij od niej ilość zarezerwowanej pamięci karty graficznej (w przypadku kompilacji deweloperskich), aby usunąć komponenty pamięci karty graficznej obsługiwane przez pliki.
- Używanie klasy Profiler: śledź łączną alokację pamięci, sumując
wartości
- Profiler pamięci Unity: aby zidentyfikować i debugować wycieki pamięci offline,
zarejestruj migawkę pamięci i sprawdź wykres Pamięć rezydentna na urządzeniu
w sekcji Cała pamięć. Aby obliczyć przybliżony rozmiar, zsumuj wartości w tych kategoriach: Nieśledzone, Środowisko wykonawcze Androida, Natywne i Zarządzane.
- Ograniczenie zRAM: w warunkach ograniczonej ilości pamięci jądro Androida może kompresować nieaktywne strony pamięci do przestrzeni wymiany (zRAM). Ponieważ profiler pamięci Unity nie może wykrywać parametrów przestrzeni wymiany na poziomie systemu operacyjnego, w scenach z dużym obciążeniem pamięci mogą występować niewielkie rozbieżności w rozmiarze. Aby potwierdzić dokładne wartości, porównaj szacunki z Perfetto.