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 Speichernutzung wird bei solchen Komponenten niedrig gehalten, indem die untergeordneten Ansichten wiederverwendet werden, sobald sie nicht mehr auf dem Bildschirm zu sehen sind. 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 mit einem Arbeitsspeicher- und Festplatten-Bitmap-Cache die Reaktionsfähigkeit und Laufruhe Ihrer Benutzeroberfläche beim Laden mehrerer Bitmaps verbessern.

Arbeitsspeicher-Cache verwenden

Ein Speicher-Cache bietet schnellen Zugriff auf Bitmaps, belegt aber wertvollen Arbeitsspeicher. Die Klasse LruCache (auch in der Supportbibliothek verfügbar) API Level 4) eignet sich besonders für das Caching von Bitmaps, wobei kürzlich 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 ein SoftReference- oder WeakReference-Bitmap-Cache eine beliebte Speichercache-Implementierung. Dies wird jedoch nicht empfohlen. Ab Android 2.3 (API-Ebene 9) ist der Garbage Collector aggressiver beim Erfassen von Soft-/Weak-Referenzen, 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 sind die restlichen Aktivitäten und/oder Anwendungen?
  • 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 Gerät mit einem Bildschirm mit besonders hoher Dichte (Extra High Density, XHDPI) wie das Galaxy Nexus benötigt einen größeren Cache, um dieselbe Anzahl von Bildern im Arbeitsspeicher zu speichern wie ein Gerät wie das 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.
  • Können Sie Qualität und Quantität in Einklang bringen? 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 etwa 1,5 MB (800 × 480 × 4 Byte). Dadurch würden mindestens 2,5 Seiten mit Bildern in zu speichern.

Beim Laden einer Bitmap in eine ImageView wird zuerst die LruCache 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 Laufwerkcache verwendet werden, um verarbeitete Bitmaps zu speichern und die Ladezeiten zu verkürzen, wenn Bilder nicht mehr im Arbeitsspeicher-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, zum Beispiel 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 die Initialisierung des Laufwerkcaches erfordert Laufwerkvorgänge und sollte daher nicht im Hauptthread erfolgen. Es besteht jedoch die Möglichkeit, auf den Cache vor der Initialisierung zugegriffen wird. 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.

Der Arbeitsspeicher-Cache wird im UI-Thread geprüft, der Laufwerk-Cache im Hintergrund-Thread. Laufwerksvorgänge sollten niemals im UI-Thread stattfinden. Wenn die Bildverarbeitung abgeschlossen ist, wird die endgültige Bitmap sowohl dem Arbeitsspeicher als auch dem Laufwerkcache zur späteren 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, alle Bilder noch einmal verarbeiten zu müssen, damit Nutzer bei einer Konfigurationsänderung reibungslos und schnell arbeiten können.

Zum Glück haben Sie einen schönen Arbeitsspeicher-Cache 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 mit und ohne Fragment zu drehen. Sie sollten kaum bis gar keine Verzögerung bemerken, da die Bilder fast überall in der Aktivität dargestellt werden. sofort aus dem Arbeitsspeicher, 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.