Bitmaps im Cache speichern

Hinweis : In den meisten Fällen empfehlen wir, die Glide-Bibliothek zu verwenden, um Bitmaps in Ihrer App abzurufen, zu decodieren und anzuzeigen. Glide entlastet den Großteil der Komplexität bei der Verarbeitung dieser und anderer Aufgaben im Zusammenhang mit der Arbeit mit Bitmaps und anderen Bildern unter Android. Informationen zum Verwenden und Herunterladen von Glide finden Sie im Glide-Repository auf GitHub.

Das Laden einer einzelnen Bitmap in Ihre Benutzeroberfläche ist unkompliziert. Es wird jedoch komplizierter, wenn Sie eine größere Gruppe von Bildern gleichzeitig laden müssen. In vielen Fällen (z. B. bei Komponenten wie ListView, GridView oder ViewPager) ist die Gesamtzahl der auf dem Bildschirm angezeigten Bilder in Kombination mit Bildern, die bald darauf scrollen könnten, praktisch unbegrenzt.

Bei Komponenten wie dieser wird die Arbeitsspeichernutzung gering gehalten, indem die untergeordneten Ansichten für eine spätere Ansicht aus dem Bildschirm wiederverwertet werden. Die automatische Speicherbereinigung gibt auch die geladenen Bitmaps frei, vorausgesetzt, Sie verwenden keine langlebigen Referenzen. Das alles funktioniert gut, aber um die Benutzeroberfläche fließend und schnell laden zu können, sollten die Bilder nicht jedes Mal verarbeitet werden, wenn sie wieder auf dem Bildschirm angezeigt werden. Dabei kann häufig ein Speicher- und Festplatten-Cache hilfreich sein, der es den Komponenten ermöglicht, verarbeitete Bilder schnell neu zu laden.

In dieser Lektion erfahren Sie, wie Sie einen Speicher- und Festplatten-Bitmap-Cache verwenden, um beim Laden mehrerer Bitmaps die Reaktionsschnelligkeit und die Flüssigkeit Ihrer UI zu verbessern.

Arbeitsspeicher-Cache verwenden

Ein Speicher-Cache bietet schnellen Zugriff auf Bitmaps, allerdings auf Kosten, die wertvollen Anwendungsspeicher in Anspruch nehmen. Die Klasse LruCache (auch in der Supportbibliothek für die Nutzung ab API-Level 4 verfügbar) eignet sich besonders gut für das Caching von Bitmaps. Dabei werden kürzlich referenzierte Objekte in einer stark referenzierten LinkedHashMap belassen und das zuletzt verwendete Mitglied entfernt, bevor der Cache die vorgegebene Größe überschreitet.

Hinweis:In der Vergangenheit war eine beliebte Arbeitsspeicher-Cache-Implementierung der Bitmap-Cache SoftReference oder WeakReference. Dies wird jedoch nicht empfohlen. Ab Android 2.3 (API-Level 9) ist die automatische Speicherbereinigung strenger beim Sammeln von Soft-/Schwach-Referenzen, wodurch sie ziemlich ineffektiver sind. Außerdem wurden vor Android 3.0 (API-Level 11) die Sicherungsdaten einer Bitmap im nativen Arbeitsspeicher gespeichert, der nicht vorhersehbar freigegeben wird. Dadurch konnte eine App kurzzeitig ihre Speicherlimits überschreiten und abstürzen.

Bei der Auswahl einer geeigneten Größe für LruCache sind eine Reihe von Faktoren zu berücksichtigen, z. B.:

  • Wie viel Arbeitsspeicher ist die restliche Aktivität und/oder Anwendung?
  • Wie viele Bilder werden gleichzeitig auf dem Bildschirm angezeigt? Wie viele müssen verfügbar sein, damit sie auf dem Bildschirm zu sehen sind?
  • Welche Displaygröße und -dichte hat das Gerät? Ein Gerät mit besonders hoher Dichte (xhdpi) wie das Galaxy Nexus benötigt einen größeren Cache, um die gleiche Anzahl von Bildern im Arbeitsspeicher zu speichern wie ein Gerät wie Nexus S (hdpi).
  • Welche Abmessungen und Konfigurationen haben die Bitmaps und wie viel Speicher nehmen sie somit jeweils in Anspruch?
  • Wie oft wird auf die Bilder zugegriffen? Werden einige häufiger aufgerufen als andere? In diesem Fall können Sie bestimmte Elemente immer im Arbeitsspeicher behalten oder sogar mehrere LruCache-Objekte für verschiedene Gruppen von Bitmaps haben.
  • Können Sie ein Gleichgewicht zwischen Qualität und Quantität schaffen? Manchmal kann es sinnvoller sein, eine größere Anzahl von Bitmaps von geringerer Qualität zu speichern und dann möglicherweise eine Version mit höherer Qualität in einer anderen Hintergrundaufgabe zu laden.

Es gibt keine bestimmte Größe oder Formel, die für alle Anwendungen geeignet ist. Es liegt an Ihnen, Ihre Nutzung zu analysieren und eine geeignete Lösung zu finden. Ein zu kleiner Cache verursacht ohnehin zusätzlichen Aufwand. Ein zu großer Cache kann wieder java.lang.OutOfMemory-Ausnahmen verursachen und dem Rest der Anwendung nur wenig Arbeitsspeicher geben.

Hier ein Beispiel für die Einrichtung einer LruCache für Bitmaps:

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

Hinweis:In diesem Beispiel ist ein Achtel des Anwendungsarbeitsspeichers für den Cache zugewiesen. Auf einem normalen/hdpi-Gerät sind dies mindestens 4 MB (32/8). Eine Vollbild-GridView mit Bildern auf einem Gerät mit einer Auflösung von 800 × 480 würde etwa 1,5 MB (800 × 480 × 4 Byte) beanspruchen, sodass dadurch mindestens 2,5 Seiten an Bildern im Arbeitsspeicher zwischengespeichert werden.

Beim Laden einer Bitmap in ein ImageView wird zuerst LruCache geprüft. Wenn ein Eintrag gefunden wird, wird er sofort verwendet, um ImageView zu aktualisieren. Andernfalls wird ein Hintergrundthread erstellt, um das Image zu verarbeiten:

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

Der BitmapWorkerTask muss auch aktualisiert werden, um dem Arbeitsspeicher-Cache Einträge hinzuzufügen:

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

Festplatten-Cache verwenden

Ein Arbeitsspeicher-Cache ist nützlich, um den Zugriff auf kürzlich aufgerufene Bitmaps zu beschleunigen. Sie können sich jedoch nicht darauf verlassen, dass Bilder in diesem Cache verfügbar sind. Komponenten wie GridView mit größeren Datasets können den Arbeitsspeicher-Cache problemlos füllen. Ihre Anwendung könnte durch eine andere Aufgabe wie einen Telefonanruf unterbrochen werden. Im Hintergrund wird sie möglicherweise beendet und der Arbeitsspeicher-Cache gelöscht. Sobald der Nutzer fortgesetzt wird, muss Ihre Anwendung jedes Bild erneut verarbeiten.

In diesen Fällen kann ein Festplatten-Cache verwendet werden, um verarbeitete Bitmaps beizubehalten und die Ladezeiten zu verkürzen, wenn Bilder nicht mehr im Speicher-Cache verfügbar sind. Natürlich ist das Abrufen von Bildern vom Laufwerk langsamer als das Laden aus dem Speicher und sollte in einem Hintergrundthread erfolgen, da die Lesezeiten des Laufwerks unvorhersehbar sein können.

Hinweis:Ein ContentProvider ist möglicherweise besser zum Speichern von Bildern im Cache geeignet, wenn häufiger darauf zugegriffen wird, z. B. in einer Bildergalerie-Anwendung.

Im Beispielcode dieser Klasse wird eine DiskLruCache-Implementierung verwendet, die aus der Android-Quelle abgerufen wird. Hier ist der aktualisierte Beispielcode, mit dem zusätzlich zum vorhandenen Arbeitsspeicher-Cache ein Festplatten-Cache hinzugefügt wird:

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

Hinweis:Auch das Initialisieren des Festplatten-Cache erfordert Laufwerksvorgänge und sollte daher nicht im Hauptthread erfolgen. Allerdings kann vor der Initialisierung auf den Cache zugegriffen werden. In der obigen Implementierung sorgt ein Sperrobjekt dafür, dass die Anwendung erst dann aus dem Festplatten-Cache liest, wenn der Cache initialisiert wurde.

Während der Arbeitsspeicher-Cache im UI-Thread geprüft wird, wird der Festplatten-Cache im Hintergrund-Thread geprüft. Laufwerksvorgänge sollten nie im UI-Thread ausgeführt werden. Wenn die Bildverarbeitung abgeschlossen ist, wird die endgültige Bitmap zur späteren Verwendung sowohl dem Arbeitsspeicher- als auch dem Festplatten-Cache hinzugefügt.

Konfigurationsänderungen verarbeiten

Änderungen der Laufzeitkonfiguration, z. B. eine Änderung der Bildschirmausrichtung, führen dazu, dass Android die laufende Aktivität mit der neuen Konfiguration löscht und neu startet. Weitere Informationen zu diesem Verhalten finden Sie unter Umgang mit Laufzeitänderungen. Sie sollten vermeiden, dass Sie alle Ihre Images noch einmal verarbeiten müssen, damit Konfigurationsänderungen für den Nutzer reibungslos und schnell verarbeitet werden können.

Glücklicherweise haben Sie einen Speicher-Cache mit Bitmaps, den Sie im Abschnitt Arbeitsspeicher-Cache verwenden erstellt haben. Dieser Cache kann mit einem Fragment, der durch Aufrufen von setRetainInstance(true) beibehalten wird, an die neue Aktivitätsinstanz übergeben werden. Nachdem die Aktivität neu erstellt wurde, wird diese beibehaltene Fragment wieder angehängt und Sie erhalten Zugriff auf das vorhandene Cache-Objekt. Bilder können schnell abgerufen und wieder in die ImageView-Objekte eingefügt werden.

Hier ein Beispiel für die Beibehaltung eines LruCache-Objekts über Konfigurationsänderungen mit einem 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);
    }
}

Um dies zu testen, drehe ein Gerät sowohl mit als auch ohne Beibehaltung der Fragment. Sie sollten eine geringe oder gar keine Verzögerung bemerken, da die Bilder die Aktivität nahezu sofort aus dem Arbeitsspeicher übernehmen, wenn Sie den Cache behalten. Alle Bilder, die nicht im Arbeitsspeicher-Cache gefunden werden, sind hoffentlich im Festplatten-Cache verfügbar. Andernfalls werden sie wie gewohnt verarbeitet.