Gdy zrozumiesz, jak Android zarządza pamięcią, i skonfigurujesz narzędzia do pomiaru wykorzystania pamięci przez grę, następnym krokiem będzie aktywne zmniejszanie i optymalizowanie jej. Przestrzeganie ścisłych limitów Androida pomaga zapobiegać zamykaniu gry przez system, skracać długi czas uruchamiania i zapewniać prawidłowe działanie gry na wszystkich urządzeniach.
W tym przewodniku znajdziesz praktyczne metody zmniejszania wykorzystania pamięci przez grę, ze szczególnym uwzględnieniem optymalizacji na poziomie zasobów, konfiguracji specyficznych dla silnika i sprawdzonych metod zarządzania pamięcią.
Zmniejszanie zużycia pamięci w Unity
Ze względu na architekturę Unity, gdy alokatory bloków natywnych i zarządzany stóg się rozszerzą, silnik zwykle zachowuje te strony pamięci do ponownego użycia, zamiast natychmiast zwracać je do systemu operacyjnego (OS), nawet po zwolnieniu zasobów. W szczególności przestrzeń adresów wirtualnych (pamięć zarezerwowana) pozostaje zarezerwowana przez cały czas trwania procesu, a pamięć fizyczna (RSS) nie jest odzyskiwana od razu, dopóki nie nastąpi kilka cykli odśmiecania pamięci (GC) i przycinania. W rezultacie tymczasowy szczyt wykorzystania pamięci może spowodować, że pamięć rezydentna pozostanie powiększona przez długi czas, nawet po spadku rzeczywistego wykorzystania. Takie zachowanie zwiększa ryzyko awarii z powodu braku pamięci (OOM) na urządzeniach z niższej półki i pogarsza ogólną stabilność środowiska wykonawczego.
Dlatego optymalizację pamięci w Unity należy przeprowadzać w oparciu o 3 główne filary dostosowane do tych zachowań silnika:
- Kontroluj aktywne wykorzystanie pamięci, aby zapobiegać szczytowym wzrostom wykorzystania pamięci.
- Zarządzaj formatami tekstur i wariantami shaderów, aby zapewnić, że nie będą tworzone niepotrzebne zasoby i zasoby natywne.
- Refaktoryzuj struktury kodu środowiska wykonawczego, aby wyeliminować niepotrzebne alokacje w zarządzanej stercie, co pozwoli zminimalizować częstotliwość czyszczenia pamięci i rozszerzanie sterty.
Więcej informacji znajdziesz w artykule Optymalizacja pamięci w Unity.
Zmniejszanie zużycia pamięci w Unreal Engine
W Unreal Engine potoki renderowania o wysokiej jakości i złożone wykresy zależności obiektów mogą znacznie zwiększyć zarówno anonimowe RSS, jak i wykorzystanie pamięci wspieranej przez pliki. W szczególności poleganie na twardych odwołaniach i głębokich hierarchiach dziedziczenia Blueprint powoduje, że nieużywane połączone zasoby są ładowane do pamięci. Ponadto nadmierne permutacje shaderów, niezoptymalizowane pule przesyłania strumieniowego tekstur i nieskompresowane tabele relokacji ELF przyczyniają się do wysokiego podstawowego wykorzystania pamięci, co zwiększa ryzyko zakończenia procesu przez Low Memory Killer (LMK).
Dlatego optymalizację pamięci w Unreal Engine należy przeprowadzać w oparciu o 3 główne filary dostosowane do tych zachowań silnika:
- Oddziel dane i logikę, zastępując twarde lub silne odwołania odwołaniami miękkimi lub słabymi.
- Usuń nieużywane funkcje oświetlenia mobilnego i opcje permutacji, aby zminimalizować obiekty stanu potoku (PSO) i nadmiarowe obiekty docelowe renderowania, a jednocześnie zastosuj kompresję ASTC i dostosuj pule przesyłania strumieniowego tekstur za pomocą profili urządzeń.
- Włącz kompresję tabeli relokacji RELR i APS, aby zmniejszyć rozmiar pliku binarnego ELF i zmniejszyć wykorzystanie pamięci fizycznej w środowisku wykonawczym.
Więcej informacji znajdziesz w artykule Optymalizacja pamięci w Unreal.
Optymalizacja wieloprocesowa
Wykorzystanie pamięci przez procesy w pamięci podręcznej jest wyłączone z limitów pamięci, ponieważ nie ma wpływu na aktywne aplikacje. Uruchomienie usługi w osobnym izolowanym procesie pomaga procesowi głównemu jak najszybciej przejść do stanu w pamięci podręcznej, co poprawia stan gry.
Więcej informacji znajdziesz w artykułach Jak śledzić stan procesu i pamięć, Jak izolować proces usługi za pomocą Unity i Jak izolować proces usługi za pomocą Unreal.
Zmniejszanie wykorzystania pamięci w usługach widocznych dla użytkownika
Twoja gra może wymagać uruchamiania logiki w usłudze widocznej dla użytkownika w przypadkach takich jak pobieranie dużych plików lub systemy czatu głosowego w tle. Te strategie mogą pomóc w zarządzaniu wykorzystaniem pamięci i jego zmniejszaniu w takich sytuacjach.
Strategie dotyczące pobierania dużych plików
Te strategie mogą mieć zastosowanie do pobierania dużych plików, które chcesz kontynuować nawet po zminimalizowaniu gry przez użytkownika.
1. Izoluj proces pobierania
Co: upewnij się, że system operacyjny może natychmiast odzyskać pamięć, której aplikacja nie używa, wykonując pobieranie w osobnym procesie. Może się bowiem zdarzyć, że alokator pamięci będzie przechowywać strony puli pamięci i utrzymywać sztucznie wysokie wykorzystanie anonimowej pamięci RSS nawet po zwolnieniu tablic. Gdy wyraźnie zakończysz lub zamkniesz usługę i proces, pamięć zostanie zwrócona do puli systemu operacyjnego, a proces główny pozostanie nienaruszony.
W Unity: odciąż pobieranie do natywnej usługi Androida
Servicezadeklarowanej w procesie takim jakandroid:process=":downloader"w niestandardowym pliku manifestu i wywołaj ją za pomocą JNIAndroidJavaClassUnity. Po zakończeniu pobierania zakończ proces. Więcej szczegółowych wskazówek znajdziesz w artykule Uruchamianie widocznej usługi w osobnym procesie za pomocą Unity.W Unreal: zadeklaruj niestandardową usługę Androida
Servicez procesem takim jakandroid:process=":downloader"za pomocą języka wtyczek Unreal i wywołaj ją za pomocą JNI C++. Po zakończeniu pobierania zakończ proces. Więcej szczegółowych wskazówek znajdziesz w artykule Uruchamianie widocznej usługi w osobnym procesie za pomocą Unreal.W przypadku natywnego Androida: zadeklaruj usługę
Servicew plikuAndroidManifestz procesem takim jakandroid:process=":downloader". Uruchom pobieranie w tym izolowanym procesie i po jego zakończeniu wywołajProcess.killProcess(Process.myPid()).
Jak to pomaga: skraca czas przechowywania pamięci i umożliwia kontynuowanie pobierania przy jednoczesnym zwalnianiu pamięci używanej przez większy proces główny.
2. Przesyłaj strumieniowo pobierane pliki bezpośrednio na dysk
Co: przesyłaj strumieniowo dane bezpośrednio z gniazda sieciowego na dysk, używając małego bufora wielokrotnego użytku o stałym rozmiarze, zamiast gromadzić odpowiedzi sieciowe w dużej tablicy przed zapisaniem.
W Unity: unikaj używania
DownloadHandlerBufferw przypadku pakietów zasobów lub dużych plików, ponieważ przydziela on natywny bufor pamięci równy rozmiarowi pliku (anonimowa pamięć RSS). Zamiast tego użyjDownloadHandlerFile, aby przesyłać strumieniowo bajty natywnie na dysk w wątku w tle.W Unreal: przesyłaj przychodzące fragmenty danych bezpośrednio do
FArchive(archiwum wspieranego przez pliki za pomocą Menedżera plików Unreal) za pomocąSetResponseBodyReceiveStream(), zamiast dołączać ładunki zIHttpRequestdoTArray<uint8>.W przypadku natywnego Androida: przesyłaj
InputStreamdoFileOutputStreamza pomocą bufora z puli, zamiast wywoływać.readBytes()lub.string()w odpowiedzi HTTP.
Jak to pomaga: zmniejsza szczytowe wykorzystanie pamięci.
3. Przesyłaj strumieniowo dekompresję pobranego pliku
Co: jeśli pobierany plik jest skompresowany, opakuj strumień wejściowy sieci w
dekompresor strumieniowy, taki jak ZipInputStream, zamiast pobierać
plik, ładować go do pamięci RAM, a następnie rozpakowywać.
Jak to pomaga: zmniejsza szczytowe wykorzystanie pamięci.
4. Deleguj do systemu operacyjnego
Co: aby całkowicie uniknąć zarządzania pamięcią w tle, deleguj pracę do natywnych interfejsów API Androida.
WorkManagerto nowoczesna, zalecana otoczka wokółJobSchedulerna poziomie systemu operacyjnego. W przypadku Androida 14 lub nowszegoWorkManagerautomatycznie obsługuje pobieranie wyzwalane przez użytkownika jako zadanie przenoszenia danych inicjowane przez użytkownika (UIDT). Działa to w procesie aplikacji, więc aby zminimalizować wykorzystanie pamięci, nadal musisz przesyłać strumieniowo dane bezpośrednio na dysk. UIDT chroni aplikację przed awariami z powodu braku pamięci, umożliwiając systemowi operacyjnemu wstrzymanie i wznowienie pobierania, jeśli zasoby systemowe staną się ograniczone.DownloadManagerdziała w osobnym procesie systemowym i nie przypisuje wykorzystania pamięci na potrzeby pobierania do Twojej aplikacji. Gdy plik zostanie pobrany i będzie gotowy, aplikacja otrzyma powiadomienie.
Jak to pomaga: WorkManager pomaga radzić sobie w sytuacjach, gdy brakuje pamięci, a DownloadManager zmniejsza wykorzystanie pamięci przez aplikację.
Strategie dotyczące usług pomocniczych
Te strategie mogą mieć zastosowanie do usług pomocniczych, które gra uruchamia równolegle z głównym procesem gry, np. czatu głosowego w tle.
1. Izoluj proces
Co: oddziel funkcję, np. rozwiązanie do czatu głosowego, od głównego silnika gry. Możesz na przykład uruchomić przechwytywanie mikrofonu i
przesyłanie strumieniowe w sieci w usłudze pierwszego planu Androida przypisanej do osobnego
procesu (zadeklarowanego w pliku manifestu, np. za pomocą android:process=":voice").
Jak to pomaga: gdy aplikacja jest zminimalizowana, obciążający proces główny gry może przejść do stanu w pamięci podręcznej o niższym priorytecie, a lżejsza usługa pomocnicza będzie nadal działać w stanie usługi widocznej dla użytkownika.
2. Przycinaj nieużywaną pamięć w procesie
Co: jeśli usługa pomocnicza jest zbyt głęboko zintegrowana z silnikiem gry, aby ją oddzielić, spróbuj jak najszybciej zmniejszyć obciążenie procesu, gdy gra zostanie przeniesiona do tła lub zminimalizowana. Rozważ wyczyszczenie pamięci podręcznych tekstur, zwolnienie nieistotnych scen, zmniejszenie częstotliwości odświeżania silnika i renderowania do 0 oraz wyraźne wywołanie odśmiecania pamięci.
W Unity: wykonaj to przycinanie, gdy zostanie wywołana funkcja
OnApplicationPause().Resources.UnloadUnusedAssets()może się przydać.W Unreal: powiąż logikę przycinania z delegatem
ApplicationWillEnterBackgroundDelegate.W przypadku natywnego Androida: w razie potrzeby wykonaj przycinanie w funkcji
onPause()lubonStop(). Zanim system operacyjny zdecyduje się na zakończenie procesów zużywających dużo pamięci, może spróbować wywołać Twoją implementację funkcjionTrimMemory().
Jak to pomaga: zmniejsza wykorzystanie pamięci, które nie jest potrzebne, gdy gra nie jest na pierwszym planie.