Najbardziej wydajny sposób na zmianę rozmiaru map bitowych w systemie Android?

115

Tworzę aplikację społecznościową, w której obrazy są przesyłane z serwera na urządzenie. Gdy urządzenie ma mniejsze rozdzielczości ekranu, muszę zmienić rozmiar map bitowych na urządzeniu, aby dopasować je do zamierzonych rozmiarów wyświetlania.

Problem polega na tym, że użycie createScaledBitmap powoduje, że napotykam wiele błędów braku pamięci po zmianie rozmiaru hordy miniatur.

Jaki jest najbardziej efektywny pod względem wykorzystania pamięci sposób zmiany rozmiaru map bitowych w systemie Android?

Colt McAnlis
źródło
7
Czy Twój serwer nie może wysłać odpowiedniego rozmiaru, aby zaoszczędzić pamięć RAM i przepustowość Twojego klienta !?
James
2
Jest to ważne tylko wtedy, gdybym był właścicielem zasobu serwera, miał dostępny komponent obliczeniowy i we wszystkich przypadkach mógł przewidzieć dokładne wymiary obrazów dla współczynników proporcji, których jeszcze nie widział. Więc jeśli ładujesz zawartość zasobów z zewnętrznego CDN (tak jak ja), to nie działa :(
Colt McAnlis

Odpowiedzi:

168

Ta odpowiedź jest podsumowana w sekcji Ładowanie dużych map bitowych Efficiently, która wyjaśnia, jak używać inSampleSize do ładowania skalowanej w dół wersji mapy bitowej.

W szczególności mapy bitowe ze skalowaniem wstępnym wyjaśniają szczegóły różnych metod, jak je łączyć i które są najbardziej wydajne w pamięci.

Istnieją trzy dominujące sposoby zmiany rozmiaru mapy bitowej w systemie Android, które mają różne właściwości pamięci:

createScaledBitmap API

Ten interfejs API weźmie istniejącą bitmapę i utworzy NOWĄ bitmapę z dokładnie wybranymi wymiarami.

Z drugiej strony możesz uzyskać dokładnie taki rozmiar obrazu, jakiego szukasz (niezależnie od tego, jak wygląda). Jednak wadą jest to, że ten interfejs API wymaga istniejącej mapy bitowej, aby działać . Oznacza to, że obraz musiałby zostać załadowany, zdekodowany i utworzona mapa bitowa, zanim będzie można utworzyć nową, mniejszą wersję. Jest to idealne rozwiązanie, jeśli chodzi o uzyskanie dokładnych wymiarów, ale okropne ze względu na dodatkowe obciążenie pamięci. W związku z tym jest to przełom w przypadku większości twórców aplikacji, którzy mają tendencję do dbania o pamięć

Flaga inSampleSize

BitmapFactory.Optionsma właściwość oznaczoną jako, inSampleSizektóra zmieni rozmiar obrazu podczas dekodowania, aby uniknąć potrzeby dekodowania do tymczasowej mapy bitowej. Użyta tutaj wartość całkowita załaduje obraz w rozmiarze zmniejszonym o 1 / x. Na przykład ustawienie inSampleSize2 zwraca obraz o połowę mniejszy, a ustawienie 4 zwraca obraz o 1/4 rozmiaru. Zasadniczo rozmiary obrazu zawsze będą o potęgę dwa mniejsze niż rozmiar źródła.

Z punktu widzenia pamięci używanie inSampleSizejest naprawdę szybką operacją. W efekcie dekoduje tylko każdy X piksel obrazu do wynikowej mapy bitowej. Są jednak dwa główne problemy inSampleSize:

  • Nie podaje dokładnych rozdzielczości . Zmniejsza tylko rozmiar mapy bitowej o potęgę 2.

  • Nie powoduje zmiany rozmiaru w najlepszej jakości . Większość filtrów zmieniających rozmiar tworzy dobrze wyglądające obrazy, odczytując bloki pikseli, a następnie ważąc je w celu uzyskania danego piksela o zmienionym rozmiarze. inSampleSizeunika tego wszystkiego, po prostu czytając co kilka pikseli. Rezultat jest dość wydajny i ma mało pamięci, ale cierpi na tym jakość.

Jeśli masz do czynienia tylko z pomniejszeniem obrazu o pewien rozmiar pow2, a filtrowanie nie stanowi problemu, nie możesz znaleźć metody bardziej wydajnej pamięci (lub wydajności) niż inSampleSize.

Flagi inScaled, inDensity, inTargetDensity

Jeśli chcesz przeskalować obraz do wymiaru, który nie jest równy potęgi dwóch, będziesz potrzebować flag inScaled, inDensityi . Gdy flaga została ustawiona, system wyprowadzi wartość skalowania, która ma być zastosowana do twojej mapy bitowej, dzieląc przez wartości.inTargetDensityBitmapOptionsinScaledinTargetDensityinDensity

mBitmapOptions.inScaled = true;
mBitmapOptions.inDensity = srcWidth;
mBitmapOptions.inTargetDensity =  dstWidth;

// will load & resize the image to be 1/inSampleSize dimensions
mCurrentBitmap = BitmapFactory.decodeResources(getResources(), 
      mImageIDs, mBitmapOptions);

Użycie tej metody spowoduje zmianę rozmiaru obrazu, a także zastosowanie do niego „filtra zmiany rozmiaru”, to znaczy wynik końcowy będzie wyglądał lepiej, ponieważ podczas zmiany rozmiaru uwzględniono dodatkowe obliczenia. Ale uwaga: ten dodatkowy krok filtra wymaga dodatkowego czasu przetwarzania i może szybko zsumować się w przypadku dużych obrazów, powodując powolne zmiany rozmiaru i dodatkowe alokacje pamięci dla samego filtra.

Generalnie nie jest dobrym pomysłem stosowanie tej techniki do obrazu, który jest znacznie większy niż pożądany rozmiar, z powodu dodatkowego narzutu filtrowania.

Magiczna kombinacja

Z punktu widzenia pamięci i wydajności można połączyć te opcje, aby uzyskać najlepsze wyniki. (ustawienie inSampleSize, inScaled, inDensitya inTargetDensityflagi)

inSampleSizezostanie najpierw zastosowany do obrazu, zwiększając go do następnej potęgi dwa WIĘKSZEJ niż docelowy rozmiar. Następnie inDensity& inTargetDensitysłużą do skalowania wyniku do dokładnych wymiarów, które chcesz, stosując operację filtrowania w celu oczyszczenia obrazu.

Połączenie tych dwóch jest znacznie szybszą operacją, ponieważ inSampleSizekrok ten zmniejszy liczbę pikseli, które wynikowy krok oparty na gęstości będzie musiał zastosować filtr zmiany rozmiaru.

mBitmapOptions.inScaled = true;
mBitmapOptions.inSampleSize = 4;
mBitmapOptions.inDensity = srcWidth;
mBitmapOptions.inTargetDensity =  dstWidth * mBitmapOptions.inSampleSize;

// will load & resize the image to be 1/inSampleSize dimensions
mCurrentBitmap = BitmapFactory.decodeFile(fileName, mBitmapOptions);

Jeśli potrzebujesz dopasować obraz do określonych wymiarów i trochę ładniejszego filtrowania, ta technika jest najlepszym pomostem do uzyskania odpowiedniego rozmiaru, ale jest wykonywana w szybkiej operacji o małej ilości pamięci.

Pobieranie wymiarów obrazu

Uzyskiwanie rozmiaru obrazu bez dekodowania całego obrazu Aby zmienić rozmiar mapy bitowej, musisz znać przychodzące wymiary. Możesz użyć inJustDecodeBoundsflagi, aby uzyskać wymiary obrazu, bez konieczności dekodowania danych pikseli.

// Decode just the boundaries
mBitmapOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(fileName, mBitmapOptions);
srcWidth = mBitmapOptions.outWidth;
srcHeight = mBitmapOptions.outHeight;


//now go resize the image to the size you want

Możesz użyć tej flagi, aby najpierw zdekodować rozmiar, a następnie obliczyć odpowiednie wartości skalowania do rozdzielczości docelowej.

Colt McAnlis
źródło
1
byłoby wspaniale, gdybyś mógł nam powiedzieć, czym jest dstWidth?
k0sh
@ k0sh dstWIdth jest szerokością ImageView, gdzie zmierza do ie destination widthlub dstWidth w skrócie
tyczj
@tyczj dzięki za odpowiedź, wiem co to jest, ale niektórzy mogą tego nie wiedzieć, a skoro Colt, który faktycznie odpowiedział na to pytanie, to może mógłby to wyjaśnić, żeby ludzie nie byli zdezorientowani.
k0sh
Fajny post ... wcześniej nie wiedziałem o flagach inScaled, inDensity, inTargetDensity ...
maveroid
Oglądałem serię wzorców wydajności Androida i wiele się nauczyłem!
Anis
13

Jakkolwiek ładna (i dokładna) jest ta odpowiedź, jest również bardzo skomplikowana. Zamiast wymyślać koło na nowo, rozważ biblioteki takie jak Glide , Picasso , UIL , Ion lub wiele innych, które wdrażają tę złożoną i podatną na błędy logikę.

Sam Colt zaleca nawet przyjrzenie się Glide i Picasso w filmie Pre-scaling Bitmaps Performance Patterns .

Korzystając z bibliotek, możesz uzyskać każdą wydajność wymienioną w odpowiedzi Colta, ale dzięki znacznie prostszym interfejsom API, które działają konsekwentnie w każdej wersji Androida.

Sam Judd
źródło