Jak odkryć wykorzystanie pamięci przez moją aplikację w systemie Android?

799

Jak mogę programowo znaleźć pamięć używaną w mojej aplikacji na Androida?

Mam nadzieję, że jest na to sposób. Dodatkowo, w jaki sposób mogę uzyskać bezpłatną pamięć telefonu?

Andrea Baccega
źródło
2
Lub jeśli ktoś chce dowiedzieć się więcej o tym wszystkim, zapoznaj się z http://elinux.org/Android_Memory_Usage
1
Zarządzaj pamięcią aplikacji developer.android.com/topic/performance/memory
Shomu

Odpowiedzi:

1007

Pamiętaj, że użycie pamięci w nowoczesnych systemach operacyjnych, takich jak Linux, jest niezwykle skomplikowanym i trudnym do zrozumienia obszarem. W rzeczywistości szanse na to, że właściwie poprawnie interpretujesz otrzymane liczby, są bardzo niskie. (Prawie za każdym razem, gdy patrzę na numery wykorzystania pamięci z innymi inżynierami, zawsze trwa długa dyskusja na temat tego, co one właściwie oznaczają, co tylko prowadzi do niejasnych wniosków.)

Uwaga: mamy teraz znacznie obszerniejszą dokumentację dotyczącą zarządzania pamięcią aplikacji, która obejmuje większą część materiału tutaj i jest bardziej aktualna w stanie Androida.

Pierwszą rzeczą jest prawdopodobnie przeczytanie ostatniej części tego artykułu, w której omówiono sposób zarządzania pamięcią na Androidzie:

Zmiany interfejsu API usług począwszy od Androida 2.0

Teraz ActivityManager.getMemoryInfo()jest nasz interfejs API najwyższego poziomu do sprawdzania ogólnego zużycia pamięci. Ma to na celu przede wszystkim pomóc aplikacji ocenić, jak blisko system nie ma już pamięci dla procesów w tle, a tym samym musi zacząć zabijać potrzebne procesy, takie jak usługi. W przypadku czystych aplikacji Java powinno to być mało przydatne, ponieważ limit sterty Java jest częściowo stosowany, aby jedna aplikacja nie była w stanie obciążać systemu do tego momentu.

Przechodząc na niższy poziom, możesz użyć interfejsu API debugowania, aby uzyskać nieprzetworzone informacje o zużyciu pamięci na poziomie jądra: android.os.Debug.MemoryInfo

Uwaga: począwszy od wersji 2.0 istnieje również interfejs API, ActivityManager.getProcessMemoryInfoaby uzyskać informacje o innym procesie: ActivityManager.getProcessMemoryInfo (int [])

Zwraca niskopoziomową strukturę MemoryInfo ze wszystkimi tymi danymi:

    /** The proportional set size for dalvik. */
    public int dalvikPss;
    /** The private dirty pages used by dalvik. */
    public int dalvikPrivateDirty;
    /** The shared dirty pages used by dalvik. */
    public int dalvikSharedDirty;

    /** The proportional set size for the native heap. */
    public int nativePss;
    /** The private dirty pages used by the native heap. */
    public int nativePrivateDirty;
    /** The shared dirty pages used by the native heap. */
    public int nativeSharedDirty;

    /** The proportional set size for everything else. */
    public int otherPss;
    /** The private dirty pages used by everything else. */
    public int otherPrivateDirty;
    /** The shared dirty pages used by everything else. */
    public int otherSharedDirty;

Ale, co jest różnica między Pss, PrivateDirtyi SharedDirty... no teraz zaczyna się zabawa.

Dużo pamięci w systemie Android (i ogólnie w systemach Linux) jest faktycznie współdzielonych przez wiele procesów. Tak więc, ile pamięci zajmuje proces, nie jest do końca jasne. Dodanie tego stronicowania na dysk (nie mówiąc już o zamianie, której nie używamy na Androidzie), a jest jeszcze mniej jasne.

Zatem jeśli weźmiesz całą fizyczną pamięć RAM faktycznie zmapowaną do każdego procesu i zsumujesz wszystkie procesy, prawdopodobnie skończyłbyś z liczbą znacznie większą niż rzeczywista całkowita pamięć RAM.

PssLiczba jest metryka to Oblicza jądra, która bierze pod dzielenia pamięci konto - w zasadzie każdej strony pamięci RAM w procesie jest skalowany przez stosunek liczby innych procesów również przy użyciu tej strony. W ten sposób możesz (teoretycznie) dodać pss we wszystkich procesach, aby zobaczyć całkowitą pamięć RAM, której używają, i porównać pss między procesami, aby uzyskać przybliżone wyobrażenie o ich względnej wadze.

Inną interesującą miarą jest tutaj PrivateDirtyilość pamięci RAM wewnątrz procesu, której nie można przywołać na dysk (nie jest ona wspierana tymi samymi danymi na dysku) i nie jest współdzielona z żadnymi innymi procesami. Innym sposobem, aby na to spojrzeć, jest pamięć RAM, która stanie się dostępna dla systemu, gdy proces ten zniknie (i prawdopodobnie szybko przejdzie do pamięci podręcznej i innych zastosowań).

To właściwie interfejsy API SDK do tego. Jest jednak więcej rzeczy, które możesz zrobić jako programista na swoim urządzeniu.

Używając adb, możesz uzyskać wiele informacji na temat wykorzystania pamięci przez uruchomiony system. Często stosowanym jest polecenie, adb shell dumpsys meminfoktóre wypluwa mnóstwo informacji na temat wykorzystania pamięci przez każdy proces Java, zawierające powyższe informacje, a także wiele innych rzeczy. Możesz także podać nazwę lub numer pid pojedynczego procesu, aby zobaczyć, na przykład adb shell dumpsys meminfo systempodaj mi proces systemowy:

** MEMINFO w pid 890 [system] **
                    natywny dalvik inne ogółem
            rozmiar: 10940 7047 N / A 17987
       przydzielono: 8943 5516 nie dotyczy 14459
            bezpłatnie: 336 1531 nie dotyczy 1867
           (Pss): 4585 9282 11916 25783
  (wspólne brudne): 2184 3596 916 6696
    (priv dirty): 4504 5956 7456 17916

 Obiekty
           Wyświetlenia: 149 Wyświetlenia: 4
     AppContexts: 13 Działania: 0
          Aktywa: 4 AssetManagers: 4
   Lokalne segregatory: 141 Segregatory proxy: 158
Odbiorcy śmierci: 49
 Gniazda OpenSSL: 0

 SQL
            sterta: 205 db Pliki: 0
       numPagers: 0 nieaktywne Strona KB: 0
    activePageKB: 0

Górna sekcja jest główna, gdzie sizejest całkowity rozmiar w przestrzeni adresowej konkretnej sterty, allocatedjest to KB rzeczywistych przydziałów, które stos uważa, że ​​ma, freeto pozostała wolna KB, którą sterty ma dla dodatkowych przydziałów, pssi priv dirtysą takie same jak omówiono wcześniej dla stron powiązanych z każdym ze stosów.

Jeśli chcesz tylko sprawdzić wykorzystanie pamięci we wszystkich procesach, możesz użyć polecenia adb shell procrank. Wynik tego w tym samym systemie wygląda następująco:

  PID Vss Rss Pss Uss cmdline
  890 84456K 48668K 25850K 21284K serwer_systemowy
 1231 50748K 39088K 17587K 13792K com.android.launcher2
  947 34488K 28528K 10834K 9308K com.android.wallpaper
  987 26964K 26956K 8751K 7308K com.google.process.gapps
  954 24300K ​​24296K 6249K 4824K com.android.phone
  948 23020K 23016K 5864K 4748K com.android.inputmethod.latin
  888 25728K 25724K 5774K 3668K zygote
  977 24100K 24096K 5667K 4340K android.process.acore
...
   59 336 K 332 K 99 K 92 K / system / bin / installd
   60 396K 392K 93K 84K / system / bin / magazyn kluczy
   51 280 K 276 K 74 K 68 K / system / bin / serwisant
   54 256 K 252 K 69 K 64 K / system / bin / debuggerd

W tym przypadku kolumny Vssi Rsssą w zasadzie szumem (są to prosta przestrzeń adresowa i użycie pamięci RAM przez proces, gdzie jeśli zsumujesz użycie pamięci RAM w procesach, otrzymasz absurdalnie dużą liczbę).

Pssjest jak widzieliśmy wcześniej i Ussjest Priv Dirty.

Interesująca rzecz do odnotowania tutaj: Pssi Usssą nieco (lub więcej niż nieco) inne niż to, co widzieliśmy meminfo. Dlaczego? Cóż, procrank używa innego mechanizmu jądra do gromadzenia danych niż meminforobi i dają one nieco inne wyniki. Dlaczego? Szczerze mówiąc nie mam pojęcia. Wydaje mi się, że procrankmoże być dokładniejszy ... ale tak naprawdę, po prostu zostaw punkt: „weź wszelkie informacje o pamięci, które otrzymujesz z dodatkiem ziarenka soli; często bardzo duże ziarno”.

Wreszcie istnieje polecenie, adb shell cat /proc/meminfoktóre daje podsumowanie ogólnego wykorzystania pamięci przez system. Jest tu wiele danych, tylko kilka pierwszych liczb, które warto omówić (i pozostałe są zrozumiałe dla kilku osób, a moje pytania o te kilka osób na ich temat często powodują sprzeczne wyjaśnienia):

MemTotal: 395144 kB
MemFree: 184936 kB
Bufory: 880 kB
W pamięci podręcznej: 84104 kB
SwapCached: 0 kB

MemTotal jest całkowitą ilością pamięci dostępnej dla jądra i przestrzeni użytkownika (często mniejszą niż faktyczna fizyczna pamięć RAM urządzenia, ponieważ część tej pamięci RAM jest potrzebna na radio, bufory DMA itp.).

MemFreeto ilość pamięci RAM, która w ogóle nie jest używana. Liczba, którą tu widzisz, jest bardzo wysoka; zwykle w systemie Android byłoby to zaledwie kilka MB, ponieważ staramy się wykorzystywać dostępną pamięć, aby procesy działały

Cachedjest używana pamięć RAM dla pamięci podręcznych systemu plików i innych podobnych rzeczy. Typowe systemy będą musiały mieć około 20 MB, aby uniknąć wpadania w zły stan stronicowania; zabójca pamięci poza systemem Android jest dostosowany do konkretnego systemu, aby upewnić się, że procesy w tle zostaną zabite, zanim pamięć RAM w pamięci podręcznej zostanie przez nich zbytnio zużyta, aby spowodować takie stronicowanie.

hackbod
źródło
1
Rzuć okiem na pixelbeat.org/scripts/ps_mem.py, który używa technik wymienionych powyżej, aby pokazać używaną pamięć RAM dla programów
pixelbeat
17
Bardzo dobrze napisane! Napisałem post na temat zarządzania pamięcią i korzystania z różnych narzędzi do sprawdzania zużycia sterty tutaj macgyverdev.blogspot.com/2011/11/…, jeśli ktoś uzna to za przydatne.
Johan Norén,
3
Czym dokładnie dwie kolumny „dalvik” są „rodzime”?
dacongy
1
Czym dokładnie są „rodzime” „dalvik” „inne”? W mojej aplikacji „inne” jest bardzo duże? jak mogę to zmniejszyć?
lądowanie
Mogę użyć „adb shell dumpsys meminfo”, ale „adb shell procrank” powiedz mi „/ system / bin / sh: procrank: not found”. Nie mam pojęcia. Chciałbym pomóc.
Hugo
79

Tak, możesz uzyskać informacje o pamięci programowo i zdecydować, czy wykonać intensywną pracę.

Uzyskaj rozmiar stosu maszyn wirtualnych, dzwoniąc:

Runtime.getRuntime().totalMemory();

Uzyskaj przydzieloną pamięć VM, dzwoniąc:

Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();

Uzyskaj limit rozmiaru stosu maszyn wirtualnych, dzwoniąc:

Runtime.getRuntime().maxMemory()

Uzyskaj natywną pamięć przydzieloną, dzwoniąc:

Debug.getNativeHeapAllocatedSize();

Zrobiłem aplikację, aby dowiedzieć się zachowanie OutOfMemoryError i monitorować użycie pamięci.

https://play.google.com/store/apps/details?id=net.coocood.oomresearch

Możesz pobrać kod źródłowy na https://github.com/coocood/oom-research

coocood
źródło
7
Czy ten środowisko wykonawcze zwróci użycie pamięci przez bieżący proces lub ogólną stertę systemu?
Mahendran
1
@mahemadhi z JavaDoc metody totalMemory () „Zwraca całkowitą ilość pamięci dostępnej dla uruchomionego programu”
Alex
To nie jest poprawna odpowiedź na pytanie. Odpowiedź nie dotyczy konkretnej aplikacji.
Amir Rezazadeh,
52

To jest praca w toku, ale tego nie rozumiem:

ActivityManager activityManager = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE);
MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memoryInfo);

Log.i(TAG, " memoryInfo.availMem " + memoryInfo.availMem + "\n" );
Log.i(TAG, " memoryInfo.lowMemory " + memoryInfo.lowMemory + "\n" );
Log.i(TAG, " memoryInfo.threshold " + memoryInfo.threshold + "\n" );

List<RunningAppProcessInfo> runningAppProcesses = activityManager.getRunningAppProcesses();

Map<Integer, String> pidMap = new TreeMap<Integer, String>();
for (RunningAppProcessInfo runningAppProcessInfo : runningAppProcesses)
{
    pidMap.put(runningAppProcessInfo.pid, runningAppProcessInfo.processName);
}

Collection<Integer> keys = pidMap.keySet();

for(int key : keys)
{
    int pids[] = new int[1];
    pids[0] = key;
    android.os.Debug.MemoryInfo[] memoryInfoArray = activityManager.getProcessMemoryInfo(pids);
    for(android.os.Debug.MemoryInfo pidMemoryInfo: memoryInfoArray)
    {
        Log.i(TAG, String.format("** MEMINFO in pid %d [%s] **\n",pids[0],pidMap.get(pids[0])));
        Log.i(TAG, " pidMemoryInfo.getTotalPrivateDirty(): " + pidMemoryInfo.getTotalPrivateDirty() + "\n");
        Log.i(TAG, " pidMemoryInfo.getTotalPss(): " + pidMemoryInfo.getTotalPss() + "\n");
        Log.i(TAG, " pidMemoryInfo.getTotalSharedDirty(): " + pidMemoryInfo.getTotalSharedDirty() + "\n");
    }
}

Dlaczego PID nie jest mapowany na wynik w activityManager.getProcessMemoryInfo ()? Najwyraźniej chcesz, aby uzyskane dane były znaczące, więc dlaczego Google tak trudno skorelowało wyniki? Obecny system nawet nie działa dobrze, jeśli chcę przetworzyć całe użycie pamięci, ponieważ zwrócony wynik to tablica obiektów android.os.Debug.MemoryInfo, ale żaden z tych obiektów nie mówi ci, z którymi pidami są skojarzone. Jeśli po prostu przekażesz tablicę wszystkich ofert, nie będziesz w stanie zrozumieć wyników. Rozumiem, że jest użyteczny, dlatego nie ma sensu przekazywać więcej niż jednego pid na raz, a jeśli tak, to po co to robić, aby activityManager.getProcessMemoryInfo () pobierał tylko tablicę int?

Ryan Beesley
źródło
2
Prawdopodobnie są w tej samej kolejności co tablica wejściowa.
taer
2
To wydaje się być bardzo nieintuicyjnym sposobem robienia rzeczy. Tak, prawdopodobnie tak jest, ale jak to się dzieje, że OOP?
Ryan Beesley,
6
Interfejs API został zaprojektowany pod kątem wydajności, a nie łatwości użytkowania lub prostoty. Nie jest to coś, z czym 99% aplikacji powinno się kiedykolwiek dotykać, więc wydajność jest najważniejszym celem projektowym.
hackbod
2
Słusznie. Próbuję napisać wewnętrzne narzędzie do śledzenia użycia pamięci przez jedną lub więcej aplikacji, które piszemy. W związku z tym szukam sposobu na wykonanie tego monitorowania przy jednoczesnym najmniejszym wpływie na inne procesy, ale wciąż tak szczegółowym z wynikami, jak to możliwe (przetwarzanie końcowe). Iterowanie po procesach, a następnie wykonywanie wywołań dla każdego procesu wydaje się nieefektywne, zakładając, że dla każdego wywołania .getProcessMemoryInfo występuje pewien narzut. Jeśli na pewno zwrócona tablica będzie w tej samej kolejności co wywołanie, przetworzę wyniki na ślepo i po prostu przyjmę parzystość.
Ryan Beesley
5
Jest to drobny problem, ale w przypadku dziennika nie jest konieczne dodawanie kanału, który jest obsługiwany za Ciebie.
ThomasW
24

Hackbod's jest jedną z najlepszych odpowiedzi na temat przepełnienia stosu. Rzuca światło na bardzo niejasny obiekt. Bardzo mi pomogło.

Innym naprawdę przydatnym źródłem jest ten film, który musisz zobaczyć: Google I / O 2011: Zarządzanie pamięcią dla aplikacji na Androida


AKTUALIZACJA:

Process Stats, usługa pozwalająca odkryć, w jaki sposób aplikacja zarządza pamięcią, wyjaśniona na blogu Statystyki procesu: Zrozumienie, w jaki sposób Twoja aplikacja korzysta z pamięci RAM przez Dianne Hackborn:

Xavi Gil
źródło
19

Android Studio 0.8.10+ wprowadziło niezwykle przydatne narzędzie o nazwie Memory Monitor .

wprowadź opis zdjęcia tutaj

Do czego służy:

  • Wyświetlanie dostępnej i używanej pamięci na wykresie oraz zdarzeń usuwania śmieci w czasie.
  • Szybkie testowanie, czy spowolnienie aplikacji może być związane z nadmiernymi zdarzeniami polegającymi na usuwaniu elementów bezużytecznych.
  • Szybkie testowanie, czy awarie aplikacji mogą być związane z brakiem pamięci.

wprowadź opis zdjęcia tutaj

Rysunek 1. Wymuszanie zdarzenia GC (Garbage Collection) w Monitorze pamięci systemu Android

Korzystając z niej, możesz uzyskać wiele dobrych informacji na temat zużycia pamięci RAM w czasie rzeczywistym przez aplikację.

Machado
źródło
16

1) Chyba nie, przynajmniej nie z Javy.
2)

ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
MemoryInfo mi = new MemoryInfo();
activityManager.getMemoryInfo(mi);
Log.i("memory free", "" + mi.availMem);
Janchenko
źródło
1
zmień na (ActivityManager activityManager = (ActivityManager) getSystemService (ACTIVITY_SERVICE);) zamiast ((ActivityManager activityManager = = (ActivityManager) getSystemService (ACTIVITY_SERVICE);)
Rajkamal
7

Okazało się, że wszystkie standardowe sposoby uzyskania całkowitej pamięci bieżącego procesu mają pewne problemy.

  • Runtime.getRuntime().totalMemory(): zwraca tylko pamięć JVM
  • ActivityManager.getMemoryInfo(), Process.getFreeMemory()I cokolwiek innego na podstawie /proc/meminfo- zwraca informacje o pamięci wszystkich procesów połączonych (np android_util_Process.cpp )
  • Debug.getNativeHeapAllocatedSize()- używa, mallinfo()które zwracają tylko informacje o przydziałach pamięci wykonywanych przez malloc()i powiązanych funkcjach (patrz android_os_Debug.cpp )
  • Debug.getMemoryInfo()- wykonuje swoją pracę, ale jest zbyt wolna. Pojedyncza rozmowa trwa około 200 ms na Nexusie 6 . Narzut związany z wydajnością sprawia, że ​​ta funkcja jest dla nas bezużyteczna, ponieważ nazywamy ją regularnie, a każde połączenie jest dość zauważalne (patrz android_os_Debug.cpp )
  • ActivityManager.getProcessMemoryInfo(int[])- wywołuje Debug.getMemoryInfo()wewnętrznie (patrz ActivityManagerService.java )

W końcu użyliśmy następującego kodu:

const long pageSize = 4 * 1024; //`sysconf(_SC_PAGESIZE)`
string stats = File.ReadAllText("/proc/self/statm");
var statsArr = stats.Split(new [] {' ', '\t', '\n'}, 3);

if( statsArr.Length < 2 )
    throw new Exception("Parsing error of /proc/self/statm: " + stats);

return long.Parse(statsArr[1]) * pageSize;

Zwraca metrykę VmRSS . Możesz znaleźć więcej szczegółów na ten temat tutaj: jeden , dwa i trzy .


PS Zauważyłem, że temat nadal ma brak rzeczywistej i prosty fragment kodu, w jaki sposób oszacować prywatnego wykorzystania pamięci procesu, jeśli wydajność nie jest krytyczny wymóg:

Debug.MemoryInfo memInfo = new Debug.MemoryInfo();
Debug.getMemoryInfo(memInfo);
long res = memInfo.getTotalPrivateDirty();

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) 
    res += memInfo.getTotalPrivateClean(); 

return res * 1024L;
Dmitry Shesterkin
źródło
1

Istnieje wiele odpowiedzi powyżej, które na pewno ci pomogą, ale (po 2 dniach zakupu i badaniach nad narzędziami pamięci adb) myślę, że mogę również pomóc z moją opinią .

Jak mówi Hackbod: Zatem jeśli weźmiesz całą fizyczną pamięć RAM faktycznie zamapowaną do każdego procesu i zsumujesz wszystkie procesy, prawdopodobnie skończyłbyś z liczbą znacznie większą niż rzeczywista całkowita pamięć RAM. więc nie ma możliwości uzyskania dokładnej ilości pamięci na proces.

Ale możesz się do tego zbliżyć za pomocą logiki ... a ja powiem jak ...

Istnieje kilka interfejsów API podobnych android.os.Debug.MemoryInfoi ActivityManager.getMemoryInfo()wymienionych powyżej, o których być może czytałeś i których używasz, ale powiem o innym sposobie

Po pierwsze, musisz być użytkownikiem root, aby działał. Wejdź do konsoli z uprawnieniami roota, wykonując suproces i zdobądź ją output and input stream. Następnie przekaż id\n (enter) w ouputstream i zapisz go do przetwarzania danych wyjściowych. Jeśli otrzymasz strumień wejściowy zawierający uid=0, jesteś użytkownikiem root.

Oto logika, której użyjesz w powyższym procesie

Kiedy pojawi się ouputstream procesu , wydaj polecenie (procrank, dumpsys meminfo itp.) \nZamiast zamiast id i pobierz go inputstreami przeczytaj, przechowuj strumień w bajtach [], char [] itd. Użyj surowych danych .. i są skończone!!!!!

pozwolenie :

<uses-permission android:name="android.permission.FACTORY_TEST"/>

Sprawdź, czy jesteś użytkownikiem root:

// su command to get root access
Process process = Runtime.getRuntime().exec("su");         
DataOutputStream dataOutputStream = 
                           new DataOutputStream(process.getOutputStream());
DataInputStream dataInputStream = 
                           new DataInputStream(process.getInputStream());
if (dataInputStream != null && dataOutputStream != null) {
   // write id to console with enter
   dataOutputStream.writeBytes("id\n");                   
   dataOutputStream.flush();
   String Uid = dataInputStream.readLine();
   // read output and check if uid is there
   if (Uid.contains("uid=0")) {                           
      // you are root user
   } 
}

Wykonaj polecenie za pomocą su

Process process = Runtime.getRuntime().exec("su");         
DataOutputStream dataOutputStream = 
                           new DataOutputStream(process.getOutputStream());
if (dataOutputStream != null) {
 // adb command
 dataOutputStream.writeBytes("procrank\n");             
 dataOutputStream.flush();
 BufferedInputStream bufferedInputStream = 
                     new BufferedInputStream(process.getInputStream());
 // this is important as it takes times to return to next line so wait
 // else you with get empty bytes in buffered stream 
 try {
       Thread.sleep(10000);
 } catch (InterruptedException e) {                     
       e.printStackTrace();
 }
 // read buffered stream into byte,char etc.
 byte[] bff = new byte[bufferedInputStream.available()];
 bufferedInputStream.read(bff);
 bufferedInputStream.close();
 }
}

logcat: wynik

Otrzymujesz nieprzetworzone dane w jednym ciągu z konsoli zamiast w jakimś przypadku z dowolnego interfejsu API, który jest skomplikowany do przechowywania, ponieważ będziesz musiał go oddzielić ręcznie .

To tylko próba, proszę zasugerować mi, jeśli coś przeoczyłem

Iamat8
źródło