Bitmaps im Cache speichern

Hinweis : In den meisten Fällen empfehlen wir, dass Sie mit Glide zum Abrufen, Decodieren und Anzeigen von Bitmaps in Ihrer App. Gleiten Sie abstrakt die Komplexität bei der Bewältigung dieser und weitere Aufgaben im Zusammenhang mit der Arbeit mit Bitmaps und anderen Bildern auf Android. Informationen zum Verwenden und Herunterladen von Glide finden Sie auf der Glide-Repository auf GitHub.

Das Laden einer einzelnen Bitmap in Ihre Benutzeroberfläche ist ganz einfach, kompliziert, wenn Sie mehrere Bilder gleichzeitig laden müssen. In vielen Fällen (z. B. bei Komponenten wie ListView, GridView oder ViewPager), die Gesamtzahl der Bilder auf dem Bildschirm in Kombination mit Bildern, auf dem Bildschirm scrollen könnten, praktisch unbegrenzt.

Die Arbeitsspeichernutzung wird durch Komponenten wie diese niedrig gehalten, indem die untergeordneten Ansichten bei der Bewegung recycelt werden zu entfernen. Die automatische Speicherbereinigung gibt ebenfalls die geladenen Bitmaps frei, vorausgesetzt, Sie speichern keine langlebige Referenzen. Das alles ist gut, aber um eine flüssige und schnell ladende Benutzeroberfläche sollten diese Bilder nicht jedes Mal neu verarbeitet werden. Eine Erinnerung und der Festplatten-Cache können dabei oft helfen, damit Komponenten verarbeitete Bilder schnell neu laden können.

In dieser Lektion erfahren Sie, wie Sie einen Bitmap-Cache für Arbeitsspeicher und Laufwerke verwenden, um die Reaktionsfähigkeit zu verbessern. und flüssige UI beim Laden mehrerer Bitmaps.

Arbeitsspeicher-Cache verwenden

Ein Arbeitsspeicher-Cache bietet schnellen Zugriff auf Bitmaps, allerdings erfordert dies die Nutzung einer wertvollen Anwendung. zu speichern. Die Klasse LruCache (auch in der Supportbibliothek verfügbar) API Level 4) eignet sich besonders gut für das Caching von Bitmaps. referenzierte Objekte in einem stark referenzierten LinkedHashMap und Entfernen der geringsten Anzahl von Objekten eines kürzlich verwendeten Mitglieds, bevor der Cache die festgelegte Größe überschreitet.

Hinweis: In der Vergangenheit war eine gängige Implementierung des Arbeitsspeicher-Cache ein Bitmap-Cache SoftReference oder WeakReference Dies wird jedoch nicht empfohlen. Ab Android 2.3 (API-Level 9) ist die automatische Speicherbereinigung bei der Erfassung weicher/schwacher Referenzen aggressiv, was sie ziemlich ineffektiv macht. Außerdem Vor Android 3.0 (API-Level 11) wurden die unterstützenden Daten einer Bitmap im nativen Arbeitsspeicher gespeichert, nicht auf vorhersehbare Weise freigegeben wird, was dazu führen kann, dass eine Anwendung kurzzeitig seine und Abstürze.

Bei der Auswahl einer geeigneten Größe für LruCache werden verschiedene Faktoren berücksichtigt werden sollten. Beispiele:

  • Wie speicherintensiv ist der Rest Ihrer Aktivität und/oder Anwendung?
  • Wie viele Bilder werden gleichzeitig auf dem Bildschirm zu sehen sein? Wie viele davon müssen bereit sein auf dem Bildschirm?
  • Wie groß ist das Display und die Dichte des Geräts? Ein Extra HD-Display (xhdpi) wie Galaxy Nexus größerer Cache für die gleiche Anzahl von Bildern im Speicher wie ein Gerät wie Nexus S (HDPI).
  • Welche Dimensionen und Konfiguration sind die Bitmaps und wie viel Speicher benötigen sie jeweils? oben?
  • Wie oft wird auf die Images zugegriffen? Werden einige häufiger aufgerufen als auf andere? Wenn ja, möchten Sie vielleicht bestimmte Elemente immer im Speicher behalten oder sogar mehrere LruCache-Objekte für verschiedene Gruppen von Bitmaps haben.
  • Schaffst du ein Gleichgewicht zwischen Qualität und Quantität? Manchmal ist es sinnvoller, eine größere qualitativ minderwertiger Bitmaps, sodass eine Version mit höherer Qualität Hintergrundaufgabe.

Es gibt keine bestimmte Größe oder Formel, die für alle Anwendungen geeignet ist. und eine geeignete Lösung zu finden. Ein zu kleiner Cache verursacht zusätzlichen Aufwand mit Kein Vorteil, ein zu großer Cache kann wieder java.lang.OutOfMemory-Ausnahmen verursachen. und lassen Sie den Rest Ihrer App wenig Arbeitsspeicher zum Arbeiten.

Hier ist ein Beispiel für die Einrichtung eines 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 Anwendungsspeichers die unserem Cache zugewiesen sind. Auf einem normalen Gerät oder HD-Gerät sind dies mindestens 4 MB (32/8). Eine vollständige mit Bildern GridView auf einem Gerät mit einer Auflösung von 800 x 480 ca. 1,5 MB (800 x 480 x 4 Byte) benötigt werden, sodass mindestens 2,5 Seiten mit Bildern im Cache zu speichern.

Beim Laden einer Bitmap in ein ImageView-Objekt wird der LruCache wird zuerst geprüft. Wenn ein Eintrag gefunden wird, wird er sofort zum Aktualisieren von ImageView verwendet. 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 außerdem aktualisiert, 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 angesehene Bitmaps zu beschleunigen, Sie können jedoch dass Bilder in diesem Cache verfügbar sind. Komponenten wie GridView mit größere Datasets einen Arbeitsspeicher-Cache leicht füllen können. Ihre Anwendung wird möglicherweise von einem anderen unterbrochen wie ein Telefonanruf, kann im Hintergrund beendet werden und der Cache zerstört. Wenn der Nutzer den Vorgang fortsetzt, 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 verringern. wenn keine Bilder mehr im Cache verfügbar sind. Bilder vom Laufwerk abrufen, langsamer ist als das Laden aus dem Arbeitsspeicher und sollte in einem Hintergrund-Thread erfolgen, da die Lesedauer der Festplatte unvorhersehbar sein.

Hinweis: Ein ContentProvider kann ein geeignet zum Speichern von im Cache gespeicherten Bildern, wenn häufiger darauf zugegriffen wird, z. B. in einem Bildergalerie-Anwendung.

Im Beispielcode dieser Klasse wird eine DiskLruCache-Implementierung verwendet, die aus dem Android-Quelle. Mit dem folgenden aktualisierten Beispielcode wird zusätzlich zum vorhandenen Arbeitsspeicher-Cache ein Festplatten-Cache hinzugefügt:

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 für das Initialisieren des Festplatten-Cache sind Laufwerkvorgänge erforderlich. und sollte daher nicht im Hauptthread erfolgen. Es besteht jedoch die Möglichkeit, wird vor der Initialisierung auf den Cache zugegriffen. Um dies zu beheben, wird in der obigen Implementierung durch eine Sperre -Objekt stellt sicher, dass die App erst aus dem Festplatten-Cache liest, wenn der Cache aktualisiert wurde initialisiert.

Während der Arbeitsspeicher-Cache im UI-Thread geprüft wird, wird der Festplatten-Cache im Hintergrund überprüft Diskussions-Thread. Laufwerksvorgänge sollten niemals im UI-Thread stattfinden. Wenn die Bildverarbeitung abgeschlossen ist, wird die letzte Bitmap sowohl dem Arbeitsspeicher- als auch dem Festplatten-Cache für die zukünftige Verwendung hinzugefügt.

Konfigurationsänderungen verarbeiten

Änderungen der Laufzeitkonfiguration, z. B. eine Änderung der Bildschirmausrichtung, führen dazu, dass Android Starten Sie die laufende Aktivität mit der neuen Konfiguration neu. Weitere Informationen siehe Laufzeitänderungen verarbeiten). Sie möchten vermeiden, dass alle Ihre Bilder erneut verarbeitet werden müssen, damit der Nutzer eine reibungslose und schnelle wenn eine Konfigurationsänderung auftritt.

Zum Glück haben Sie einen schönen Speichercache mit Bitmaps, die Sie im Abschnitt Speichercache verwenden erstellt haben. Dieser Cache kann an das neue Aktivitätsinstanz mit einer Fragment, die durch Aufrufen von setRetainInstance(true) beibehalten wird. Nachdem die Aktivität neu erstellt, wird die beibehaltene Fragment wieder angehängt und Sie erhalten Zugriff auf Vorhandenes Cache-Objekt, wodurch Bilder schnell abgerufen und wieder in die ImageView-Objekte eingefügt werden können.

Hier ein Beispiel für die Beibehaltung eines LruCache-Objekts über die gesamte Konfiguration hinweg mit 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, kannst du versuchen, ein Gerät sowohl mit als auch ohne Fragment zu drehen. Sie sollten wenig bis gar keine Verzögerung bemerken, da die Bilder fast überall in der Aktivität dargestellt werden. sofort aus dem Speicher entfernt, wenn Sie den Cache beibehalten. Alle Bilder, die nicht im Arbeitsspeicher-Cache gefunden werden, hoffentlich im Festplatten-Cache verfügbar. Andernfalls werden sie wie gewohnt verarbeitet.