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.