Buforowanie map bitowych

Uwaga: w większości przypadków zalecamy że używasz Glide, do pobierania, dekodowania i wyświetlania map bitowych w aplikacji. Wyciągnij abstrakcje z większości radzenia sobie z tymi i innymi innych zadań związanych z pracą z mapami bitowymi i innymi obrazami na Androidzie. Informacje o używaniu i pobieraniu Glide znajdziesz na stronie Repozytorium Glide w GitHubie.

Wczytywanie pojedynczej bitmapy do interfejsu użytkownika (UI) jest proste, ale staje się więcej skomplikowane w przypadku wczytywania większego zestawu obrazów naraz. W wielu przypadkach (np. komponentów takich jak ListView, GridView lub ViewPager), łączną liczbę obrazów na ekranie razem z obrazami, które które mogą wkrótce przewijać się na ekran, są w zasadzie nieograniczone.

Komponenty takie jak to ograniczają wykorzystanie pamięci przez recykling widoków dziecka, gdy się poruszają. Moduł czyszczenia pamięci uwalnia też załadowane mapy bitowe, zakładając, że żadne długotrwałe odniesienia. To wszystko jest w porządku, ale aby interfejs był płynny i szybko ładujący się, chcesz uniknąć ciągłego przetwarzania tych obrazów za każdym razem, gdy wracają na ekran. Wspomnienie i pamięć podręczna dysku często może pomóc, umożliwiając komponentom szybkie ponowne załadowanie przetworzonych obrazów.

Z tej lekcji dowiesz się, jak korzystać z pamięci podręcznej bitmapy dysku i pamięci podręcznej, aby poprawić szybkość reagowania. oraz płynność interfejsu podczas wczytywania wielu map bitowych.

Używaj pamięci podręcznej

Pamięć podręczna zapewnia szybki dostęp do map bitowych i wymaga zajmowania cennej aplikacji pamięci. Klasa LruCache (dostępna też w bibliotece pomocy do użytku publicznego do poziomu interfejsu API poziomu 4) jest szczególnie przydatny do buforowania map bitowych, obiekty z odwołanymi obiektami w silnym, przywoływanym elemencie LinkedHashMap, usuwając najmniejsze ostatnio używany element, zanim pamięć podręczna przekroczy wyznaczony rozmiar.

Uwaga: w przeszłości popularną implementacją pamięci podręcznej była SoftReference lub WeakReference pamięć podręczna map bitowych, nie jest to jednak zalecane. Od Androida 2.3 (poziom interfejsu API 9) funkcja czyszczenia pamięci jest agresywnie gromadząc „delikatne”/słabe odniesienia, przez co są one dość nieskuteczne. Ponadto przed Androidem 3.0 (poziom interfejsu API 11) dane kopii zapasowej bitmapy były przechowywane w pamięci natywnej, która nie jest wydawana w przewidywalny sposób, co może spowodować, że aplikacja limity pamięci i awarie.

Aby wybrać odpowiedni rozmiar dla urządzenia LruCache, weź pod uwagę kilka czynników należy wziąć pod uwagę, na przykład:

  • Jak intensywnie korzysta się z pamięci w pozostałej części aktywności i aplikacji?
  • Ile obrazów jest widocznych na ekranie jednocześnie? Ile filmów musi być dostępnych i gotowych na ekranie?
  • Jaki jest rozmiar ekranu i gęstość ekranu urządzenia? Bardzo duże urządzenie z ekranem o dużej gęstości (xhdpi) takie jak Galaxy Nexus, większą pamięć podręczną, by przechowywać w pamięci taką samą liczbę obrazów niż w przypadku urządzeń takich jak Nexus S (hdpi).
  • jakie wymiary i konfiguracja są mapami bitowymi, a tym samym ile pamięci zajmuje każda z nich; w górę?
  • Jak często będą używane obrazy? Czy niektóre z nich będą używane częściej niż inne? Jeśli tak, być może warto zachować określone elementy zawsze w pamięci, a nawet mieć kilka obiektów LruCache dla różnych grup map bitowych.
  • Czy potrafisz znaleźć równowagę między jakością a ilością? Czasami lepiej jest przechowywać większą liczba map bitowych o niższej jakości, co potencjalnie może spowodować wczytywanie wersji o wyższej jakości w innej. w tle.

Nie istnieje konkretna formuła, która sprawdzi się we wszystkich zastosowaniach, ale to od Ciebie zależy i znalezieniu odpowiedniego rozwiązania. Zbyt mała pamięć podręczna powoduje dodatkowe obciążenie brak korzyści, zbyt duża pamięć podręczna może ponownie powodować java.lang.OutOfMemory wyjątków a większą ilość pamięci zostawić w aplikacji.

Oto przykład konfiguracji pola 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 jedna ósma pamięci aplikacji to przeznaczonych dla naszej pamięci podręcznej. W przypadku zwykłych urządzeń/urządzeń hdpi jest to minimum ok. 4MB (32/8). Pełny ekran GridView wypełniony obrazami w urządzeniu o rozdzielczości 800x480 wykorzystują około 1,5 MB (800 x 480 * 4 bajtów), co spowoduje zapisanie w pamięci podręcznej co najmniej 2,5 strony obrazów pamięci.

Podczas wczytywania bitmapy do komponentu ImageView tag LruCache jest sprawdzane najpierw. Po znalezieniu wpisu jest on natychmiast używany do aktualizacji elementu ImageView. W przeciwnym razie w celu przetworzenia obrazu generowany jest wątek tła:

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);
    }
}

BitmapWorkerTask również musi być: Zaktualizowano, 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 jest przydatna do przyspieszenia dostępu do ostatnio wyświetlanych map bitowych, ale nie można korzystać z obrazów dostępnych w tej pamięci podręcznej. Komponenty takie jak GridView z duże zbiory danych mogą łatwo zapełniać pamięć podręczną. Działanie aplikacji mogło zostać przerwane przez inny takie jak połączenie telefoniczne, ale w tle może zostać przerwane, a pamięć podręczna może zostać skończona. zniszczenia. Gdy użytkownik wznowi swoje działanie, aplikacja będzie musiała przetworzyć wszystkie obrazy ponownie.

W takich przypadkach można użyć pamięci podręcznej dysku, aby utrzymać przetworzone mapy bitowe i przyspieszyć wczytywanie przypadków, gdy obrazy nie są już dostępne w pamięci podręcznej. Oczywiście pobieram obrazy z dysku. jest wolniejsze niż ładowanie z pamięci i powinno zostać wykonane w wątku w tle, ponieważ odczyty z dysku mogą być nieprzewidywalne.

Uwaga: wartość ContentProvider może być większa odpowiednie miejsce do przechowywania obrazów w pamięci podręcznej, jeśli są one częściej używane, na przykład galerii obrazów.

Przykładowy kod tej klasy korzysta z implementacji DiskLruCache pobieranej z Źródło Androida. Oto zaktualizowany przykładowy kod, który dodaje pamięć podręczną dysku do istniejącej pamięci podręcznej:

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 zainicjowanie pamięci podręcznej dysku wymaga operacji na dysku i dlatego nie powinny się odbywać w wątku głównym. Oznacza to jednak, że istnieje dostęp do pamięci podręcznej przed jej zainicjowaniem. Aby rozwiązać ten problem, w powyższej implementacji pojawia się blokada zapewnia, że aplikacja nie będzie odczytywała pamięci podręcznej dysku, dopóki pamięć podręczna nie zostanie zainicjowano.

Gdy pamięć podręczna pamięci jest sprawdzona w wątku interfejsu użytkownika, pamięć podręczna dysku jest sprawdzana w tle w wątku. Operacje na dysku nigdy nie powinny być wykonywane w wątku UI. Gdy przetwarzanie obrazu jest zakończono, ostateczna mapa bitowa jest dodawana do pamięci podręcznej oraz podręcznej pamięci dyskowej w celu użycia w przyszłości.

Obsługa zmian konfiguracji

Zmiany w konfiguracji środowiska wykonawczego, takie jak zmiana orientacji ekranu, powodują zniszczenie przez Androida uruchom ponownie uruchomioną aktywność z nową konfiguracją (więcej informacji o tym działaniu zapoznaj się z sekcją Obsługa zmian w środowisku wykonawczym). Chcesz uniknąć konieczności ponownego przetwarzania wszystkich obrazów, aby zapewnić użytkownikom płynne i szybkie gdy nastąpi zmiana konfiguracji.

Na szczęście masz sporą pamięć podręczną z mapami bitowymi, która została wbudowana w sekcji Użyj pamięci podręcznej. Tę pamięć podręczną można przekazać do nowego wystąpienia aktywności przy użyciu funkcji Fragment, która jest zachowywana przez wywołanie metody setRetainInstance(true). Po zakończeniu aktywności ten zachowany zasób Fragment zostanie ponownie podłączony i uzyskasz dostęp do istniejący obiekt pamięci podręcznej, co umożliwia szybkie pobieranie i ponowne wypełnianie obrazów w obiektach ImageView.

Oto przykład zachowywania obiektu LruCache w różnych konfiguracjach zmienia się za pomocą parametru 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 zarówno z zachowaniem elementu Fragment, jak i bez niego. Opóźnienie powinno być niewielkie lub zerowe, ponieważ obrazy wypełniają ćwiczenie prawie natychmiast z pamięci, jeśli zachowujesz pamięć podręczną. Wszystkie obrazy, których nie ma w pamięci podręcznej, są które prawdopodobnie są dostępne w pamięci podręcznej dysku. Jeśli nie, zostaną przetworzone w zwykły sposób.