Buforowanie map bitowych

Uwaga: w większości przypadków zalecamy korzystanie z biblioteki Glide do pobierania, dekodowania i wyświetlania map bitowych w aplikacji. Glide eliminuje większość złożoności obsługi tych i innych zadań związanych z pracą z mapami bitowymi i innymi obrazami na Androidzie. Informacje o używaniu i pobieraniu Glide znajdziesz w repozytorium Glide na GitHubie.

Wczytywanie pojedynczej bitmapy do interfejsu użytkownika jest proste, ale komplikuje się, jeśli trzeba załadować większy zbiór obrazów naraz. W wielu przypadkach (np. w przypadku komponentów takich jak ListView, GridView czy ViewPager) łączna liczba obrazów na ekranie w połączeniu z obrazami, które mogą wkrótce przewinąć na ekran, jest zasadniczo nieograniczona.

Takie komponenty zmniejszają wykorzystanie pamięci dzięki recyklingowi widoków elementów podrzędnych, gdy są one przesuwane poza ekran. Moduł zbierający śmieci zwalnia też wczytane mapy bitowe, o ile nie zachowujesz długotrwałych odwołań. Wszystko jest w porządku, ale jeśli chcesz, by interfejs użytkownika był płynny i szybko się ładował, unikaj ciągłego przetwarzania obrazów za każdym razem, gdy wracają one do ekranu. Pamięć i pamięć podręczna dysku są tu często pomocne, umożliwiając komponentom szybkie ponowne ładowanie przetworzonych obrazów.

Z tej lekcji dowiesz się, jak korzystać z pamięci podręcznej bitmapowej pamięci podręcznej, aby poprawić czas reakcji i płynność interfejsu użytkownika podczas wczytywania wielu map bitowych.

Użyj pamięci podręcznej

Pamięć podręczna zapewnia szybki dostęp do map bitowych, co wiąże się z koniecznością zajmowania cennej pamięci aplikacji. Klasa LruCache (dostępna również w Bibliotece pomocy do użycia z powrotem do interfejsu API poziomu 4) jest szczególnie przydatna do buforowania bitmap, polegająca na umieszczaniu ostatnio odwołań w określonym miejscu LinkedHashMap i usuwaniu z niej najmniejszego rozmiaru pamięci podręcznej.

Uwaga: w przeszłości popularną implementacją pamięci podręcznej była pamięć podręczna SoftReference lub WeakReference, ale nie jest to zalecane. Począwszy od Androida 2.3 (poziom interfejsu API 9) moduł pobierania śmieci jest bardziej agresywny w zakresie zbierania pogodnych/słabych odwołań, co sprawia, że są one dość nieskuteczne. Dodatkowo przed Androidem 3.0 (poziom interfejsu API 11) dane zapasowe bitmapy były przechowywane w pamięci natywnej, która nie jest publikowana w przewidywalny sposób, co mogło powodować krótkotrwałe przekroczenie limitów pamięci i awarię aplikacji.

Aby wybrać odpowiedni rozmiar dla elementu LruCache, należy wziąć pod uwagę kilka czynników, np.:

  • W jakim stopniu pamięć wykonywana jest przez pozostałą część Twojej aktywności lub aplikacji?
  • Ile obrazów będzie jednocześnie widocznych na ekranie? Ile jednostek musi być dostępnych, aby pojawić się na ekranie?
  • Jaki jest rozmiar ekranu i gęstość ekranu urządzenia? Urządzenia o bardzo dużej gęstości (xhdpi), takie jak Galaxy Nexus, będą potrzebowały większej pamięci podręcznej, by zmieścić w pamięci taką samą liczbę obrazów, niż urządzenia takie jak Nexus S (hdpi).
  • Jakie wymiary i konfiguracja są bitmapami i ile pamięci zajmuje każda z nich?
  • Jak często będzie można uzyskiwać dostęp do obrazów? Czy niektóre z nich będą używane częściej niż inne? Jeśli tak, możesz chcieć przechowywać określone elementy zawsze w pamięci lub nawet mieć wiele obiektów LruCache dla różnych grup map bitowych.
  • Czy można znaleźć równowagę między jakością a ilością? Czasami lepiej zapisać większą liczbę map bitowych o niższej jakości i potencjalnie wczytać wersję o wyższej jakości w innym zadaniu w tle.

Nie ma konkretnego rozmiaru ani formuły, która sprawdzi się we wszystkich zastosowaniach. To Ty musisz przeanalizować wykorzystanie i opracować rozwiązanie. Zbyt mała pamięć podręczna powoduje dodatkowe obciążenia i nie ma żadnych korzyści. Zbyt duża pamięć podręczna znów może powodować wyjątki (java.lang.OutOfMemory) i pozostawić mało pamięci na potrzeby reszty aplikacji.

Oto przykład konfigurowania LruCache na potrzeby map bitowych:

Kotlin

private lateinit var memoryCache: LruCache<String, Bitmap>

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    // Get max available VM memory, exceeding this amount will throw an
    // OutOfMemory exception. Stored in kilobytes as LruCache takes an
    // int in its constructor.
    val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()

    // Use 1/8th of the available memory for this memory cache.
    val cacheSize = maxMemory / 8

    memoryCache = object : LruCache<String, Bitmap>(cacheSize) {

        override fun sizeOf(key: String, bitmap: Bitmap): Int {
            // The cache size will be measured in kilobytes rather than
            // number of items.
            return bitmap.byteCount / 1024
        }
    }
    ...
}

Java

private LruCache<String, Bitmap> memoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Get max available VM memory, exceeding this amount will throw an
    // OutOfMemory exception. Stored in kilobytes as LruCache takes an
    // int in its constructor.
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

    // Use 1/8th of the available memory for this memory cache.
    final int cacheSize = maxMemory / 8;

    memoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // The cache size will be measured in kilobytes rather than
            // number of items.
            return bitmap.getByteCount() / 1024;
        }
    };
    ...
}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        memoryCache.put(key, bitmap);
    }
}

public Bitmap getBitmapFromMemCache(String key) {
    return memoryCache.get(key);
}

Uwaga: w tym przykładzie 1/8 pamięci aplikacji jest przeznaczona na pamięć podręczną. W przypadku zwykłych urządzeń i urządzeń HD jest to minimum około 4 MB (32/8). Pełnoekranowy GridView z obrazami na urządzeniu o rozdzielczości 800 x 480 zajmuje około 1,5 MB (800*480*4 bajtów), więc w pamięci podręcznej znajdzie się co najmniej 2,5 strony obrazów.

Podczas wczytywania bitmapy do elementu ImageView najpierw sprawdzany jest parametr LruCache. W przypadku znalezienia wpisu natychmiast jest on używany do zaktualizowania obrazu ImageView. W przeciwnym razie generowany jest wątek tła w celu przetworzenia obrazu:

Kotlin

fun loadBitmap(resId: Int, imageView: ImageView) {
    val imageKey: String = resId.toString()

    val bitmap: Bitmap? = getBitmapFromMemCache(imageKey)?.also {
        mImageView.setImageBitmap(it)
    } ?: run {
        mImageView.setImageResource(R.drawable.image_placeholder)
        val task = BitmapWorkerTask()
        task.execute(resId)
        null
    }
}

Java

public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);

    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
        mImageView.setImageBitmap(bitmap);
    } else {
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}

Musisz też zaktualizować BitmapWorkerTask, aby dodać wpisy do pamięci podręcznej:

Kotlin

private inner class BitmapWorkerTask : AsyncTask<Int, Unit, Bitmap>() {
    ...
    // Decode image in background.
    override fun doInBackground(vararg params: Int?): Bitmap? {
        return params[0]?.let { imageId ->
            decodeSampledBitmapFromResource(resources, imageId, 100, 100)?.also { bitmap ->
                addBitmapToMemoryCache(imageId.toString(), bitmap)
            }
        }
    }
    ...
}

Java

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100));
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}

Używaj pamięci podręcznej dysku

Pamięć podręczna przydaje się do przyspieszania dostępu do ostatnio wyświetlanych map bitowych. Nie można jednak polegać na dostępności obrazów. Komponenty takie jak GridView z większymi zbiorami danych mogą łatwo zapełnić pamięć podręczną. Działanie aplikacji może zostać zakłócone przez inne zadanie, na przykład połączenie telefoniczne, a podczas działania aplikacji w tle może ona zostać zamknięta, a pamięć podręczna zostanie zniszczona. Gdy użytkownik wznowi wyświetlanie, aplikacja musi ponownie przetworzyć wszystkie zdjęcia.

W takich przypadkach można użyć pamięci podręcznej dysku, aby zachować przetworzone mapy bitowe i skrócić czasy wczytywania, gdy obrazy nie są już dostępne w pamięci podręcznej. Pobieranie obrazów z dysku trwa wolniej niż pobieranie z pamięci i należy je przeprowadzać w wątku w tle, ponieważ czas odczytu dysku może być nieprzewidywalny.

Uwaga: ContentProvider może być lepszym miejscem do przechowywania obrazów w pamięci podręcznej, jeśli częściej otwierają się one na przykład w aplikacji galerii obrazów.

Przykładowy kod tej klasy korzysta z implementacji DiskLruCache pobranej ze źródła Androida. Oto zaktualizowany przykładowy kod, który oprócz istniejącej pamięci podręcznej dodaje też pamięć podręczną dysku:

Kotlin

private const val DISK_CACHE_SIZE = 1024 * 1024 * 10 // 10MB
private const val DISK_CACHE_SUBDIR = "thumbnails"
...
private var diskLruCache: DiskLruCache? = null
private val diskCacheLock = ReentrantLock()
private val diskCacheLockCondition: Condition = diskCacheLock.newCondition()
private var diskCacheStarting = true

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    // Initialize memory cache
    ...
    // Initialize disk cache on background thread
    val cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR)
    InitDiskCacheTask().execute(cacheDir)
    ...
}

internal inner class InitDiskCacheTask : AsyncTask<File, Void, Void>() {
    override fun doInBackground(vararg params: File): Void? {
        diskCacheLock.withLock {
            val cacheDir = params[0]
            diskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE)
            diskCacheStarting = false // Finished initialization
            diskCacheLockCondition.signalAll() // Wake any waiting threads
        }
        return null
    }
}

internal inner class  BitmapWorkerTask : AsyncTask<Int, Unit, Bitmap>() {
    ...

    // Decode image in background.
    override fun doInBackground(vararg params: Int?): Bitmap? {
        val imageKey = params[0].toString()

        // Check disk cache in background thread
        return getBitmapFromDiskCache(imageKey) ?:
                // Not found in disk cache
                decodeSampledBitmapFromResource(resources, params[0], 100, 100)
                        ?.also {
                            // Add final bitmap to caches
                            addBitmapToCache(imageKey, it)
                        }
    }
}

fun addBitmapToCache(key: String, bitmap: Bitmap) {
    // Add to memory cache as before
    if (getBitmapFromMemCache(key) == null) {
        memoryCache.put(key, bitmap)
    }

    // Also add to disk cache
    synchronized(diskCacheLock) {
        diskLruCache?.apply {
            if (!containsKey(key)) {
                put(key, bitmap)
            }
        }
    }
}

fun getBitmapFromDiskCache(key: String): Bitmap? =
        diskCacheLock.withLock {
            // Wait while disk cache is started from background thread
            while (diskCacheStarting) {
                try {
                    diskCacheLockCondition.await()
                } catch (e: InterruptedException) {
                }

            }
            return diskLruCache?.get(key)
        }

// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
fun getDiskCacheDir(context: Context, uniqueName: String): File {
    // Check if media is mounted or storage is built-in, if so, try and use external cache dir
    // otherwise use internal cache dir
    val cachePath =
            if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()
                    || !isExternalStorageRemovable()) {
                context.externalCacheDir.path
            } else {
                context.cacheDir.path
            }

    return File(cachePath + File.separator + uniqueName)
}

Java

private DiskLruCache diskLruCache;
private final Object diskCacheLock = new Object();
private boolean diskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Initialize memory cache
    ...
    // Initialize disk cache on background thread
    File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
    new InitDiskCacheTask().execute(cacheDir);
    ...
}

class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
    @Override
    protected Void doInBackground(File... params) {
        synchronized (diskCacheLock) {
            File cacheDir = params[0];
            diskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
            diskCacheStarting = false; // Finished initialization
            diskCacheLock.notifyAll(); // Wake any waiting threads
        }
        return null;
    }
}

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final String imageKey = String.valueOf(params[0]);

        // Check disk cache in background thread
        Bitmap bitmap = getBitmapFromDiskCache(imageKey);

        if (bitmap == null) { // Not found in disk cache
            // Process as normal
            final Bitmap bitmap = decodeSampledBitmapFromResource(
                    getResources(), params[0], 100, 100));
        }

        // Add final bitmap to caches
        addBitmapToCache(imageKey, bitmap);

        return bitmap;
    }
    ...
}

public void addBitmapToCache(String key, Bitmap bitmap) {
    // Add to memory cache as before
    if (getBitmapFromMemCache(key) == null) {
        memoryCache.put(key, bitmap);
    }

    // Also add to disk cache
    synchronized (diskCacheLock) {
        if (diskLruCache != null && diskLruCache.get(key) == null) {
            diskLruCache.put(key, bitmap);
        }
    }
}

public Bitmap getBitmapFromDiskCache(String key) {
    synchronized (diskCacheLock) {
        // Wait while disk cache is started from background thread
        while (diskCacheStarting) {
            try {
                diskCacheLock.wait();
            } catch (InterruptedException e) {}
        }
        if (diskLruCache != null) {
            return diskLruCache.get(key);
        }
    }
    return null;
}

// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
public static File getDiskCacheDir(Context context, String uniqueName) {
    // Check if media is mounted or storage is built-in, if so, try and use external cache dir
    // otherwise use internal cache dir
    final String cachePath =
            Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
                    !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
                            context.getCacheDir().getPath();

    return new File(cachePath + File.separator + uniqueName);
}

Uwaga: nawet inicjowanie pamięci podręcznej dysku wymaga operacji na dysku i dlatego nie powinno się odbywać w wątku głównym. Oznacza to jednak, że przed zainicjowaniem można uzyskać dostęp do pamięci podręcznej. Aby rozwiązać ten problem, w ramach powyższej implementacji obiekt blokady uniemożliwia aplikacji odczyt z pamięci podręcznej na dysku do czasu jej zainicjowania.

Gdy pamięć podręczna jest sprawdzana w wątku UI, pamięć podręczna dysku jest sprawdzana w wątku w tle. Operacje na dysku nie powinny nigdy być wykonywane w wątku interfejsu użytkownika. Po zakończeniu przetwarzania obrazu końcowa mapa bitowa jest dodawana zarówno do pamięci podręcznej, jak i pamięci podręcznej na dysku. Będzie można z niej korzystać w przyszłości.

Obsługa zmian konfiguracji

Zmiany konfiguracji środowiska wykonawczego, takie jak zmiana orientacji ekranu, powodują zniszczenie i ponowne uruchomienie uruchomionej aktywności z nową konfiguracją przez Androida. Więcej informacji na ten temat znajdziesz w sekcji Obsługa zmian środowiska wykonawczego. Pozwala to uniknąć konieczności ponownego przetwarzania wszystkich obrazów, aby zapewnić płynność i szybkość działania stron po zmianie konfiguracji.

Na szczęście masz sporo pamięci podręcznej map bitowych zbudowaną w sekcji Użycie pamięci podręcznej. Tę pamięć podręczną można przekazać do nowej instancji aktywności za pomocą obiektu Fragment, który jest zachowywany przez wywołanie setRetainInstance(true). Po odtworzeniu aktywności ta zachowana Fragment jest ponownie dołączana i uzyskujesz dostęp do istniejącego obiektu pamięci podręcznej, dzięki czemu obrazy mogą być szybko pobierane i ponownie zapełniane w obiektach ImageView.

Oto przykład zachowywania obiektu LruCache po zmianie konfiguracji za pomocą Fragment:

Kotlin

private const val TAG = "RetainFragment"
...
private lateinit var mMemoryCache: LruCache<String, Bitmap>

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    val retainFragment = RetainFragment.findOrCreateRetainFragment(supportFragmentManager)
    mMemoryCache = retainFragment.retainedCache ?: run {
        LruCache<String, Bitmap>(cacheSize).also { memoryCache ->
            ... // Initialize cache here as usual
            retainFragment.retainedCache = memoryCache
        }
    }
    ...
}

class RetainFragment : Fragment() {
    var retainedCache: LruCache<String, Bitmap>? = null

    companion object {
        fun findOrCreateRetainFragment(fm: FragmentManager): RetainFragment {
            return (fm.findFragmentByTag(TAG) as? RetainFragment) ?: run {
                RetainFragment().also {
                    fm.beginTransaction().add(it, TAG).commit()
                }
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        retainInstance = true
    }
}

Java

private LruCache<String, Bitmap> memoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    RetainFragment retainFragment =
            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    memoryCache = retainFragment.retainedCache;
    if (memoryCache == null) {
        memoryCache = new LruCache<String, Bitmap>(cacheSize) {
            ... // Initialize cache here as usual
        }
        retainFragment.retainedCache = memoryCache;
    }
    ...
}

class RetainFragment extends Fragment {
    private static final String TAG = "RetainFragment";
    public LruCache<String, Bitmap> retainedCache;

    public RetainFragment() {}

    public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
        RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
        if (fragment == null) {
            fragment = new RetainFragment();
            fm.beginTransaction().add(fragment, TAG).commit();
        }
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }
}

Aby to przetestować, obróć urządzenie z zachowaniem Fragment i bez. Opóźnienie powinno być niewielkie lub żadne, ponieważ po zachowywaniu pamięci podręcznej obrazy wypełniają dane działanie niemal natychmiast z pamięci. Wszelkie obrazy, których nie ma w pamięci podręcznej, prawdopodobnie są dostępne w tej pamięci. Jeśli tak nie jest, są przetwarzane w zwykły sposób.