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.