Il y a 2 ans -
Temps de lecture 9 minutes
Glide for Android : comprendre et gérer le cache des images
Glide: Kezako
Glide est une bibliothèque de chargement d’images pour Android. Il s’agit de l’un des outils les plus répandus sur le marché avec Picasso, Fresco et Coil qui commence à gagner en visibilité. L’intérêt de Glide est de pouvoir afficher assez facilement des images et GIFs, tout en utilisant si besoin une couche réseau personnalisée.
Vous connaissez certainement les différentes manières de charger des images, que ce soit via des URLs ou des images déjà encodées en base64. L’utilisation basique de l’API se résume en effet à :
Glide.with(fragment) //view ou context .load(url) .into(imageView) //id de la Target
Il serait pourtant trop simple de résumer Glide à ces trois lignes.
Les bases
Imaginons une application contenant plusieurs écrans dans lesquels vous affichez des images. Imaginons également une contrainte qui nécessite l’utilisation d’entêtes HTTP personnalisés et qu’il faille appliquer un découpage circulaire sur chaque image. On pourrait faire une duplication de code et utiliser quelque chose de similaire à ceci partout dans l’application :
val glideUrl = GlideUrl( "https:my-url.com/photo1", Headers { mutableMapOf("My Custom Header Authorization" to "Bearer my-secret-token") } ) Glide.with(itemView) .load(glideUrl) .apply(RequestOptions().circleCrop()) .into(target)
Pour éviter cette méthode fastidieuse, l’idéal est de mutualiser ce code dans un GlideModule. Le GlideModule
est une class
que l’on doit créer à la racine de l’application et qui implémente AppGlideModule
.
La suite de cet article suppose que la bibliothèque est installée dans le projet, y compris la partie annotations (si besoin, se référer à la documentation d’installation).
En utilisant les annotations, les paramètres indiqués dans cette CustomGlideApplication
seront réutilisés à chaque appel pour afficher une image et permettra donc de mutualiser toutes les options :
@GlideModule class CustomGlideApplication : AppGlideModule() { @Inject lateinit var okHttpClient: OkHttpClient override fun registerComponents(context: Context, glide: Glide, registry: Registry) { registry.replace( GlideUrl::class.java, InputStream::class.java, OkHttpUrlLoader.Factory(okHttpClient) ) } override fun applyOptions(context: Context, builder: GlideBuilder) { val requestOptions = RequestOptions().circleCrop() builder.setDefaultRequestOptions(requestOptions) } }
Dans le détail, le GlideModule
est préparé de manière à ce que lorsque l’on appelle des GlideUrl
(wrapper pour requêtes HTTP/HTTPS), la couche réseau soit régentée par l’instance okHttpClient :
registry.replace( GlideUrl::class.java, InputStream::class.java, OkHttpUrlLoader.Factory(okHttpClient) )
De la même manière, dans cet exemple, toutes les images téléchargées subiront une transformation de type circleCrop
:
val requestOptions = RequestOptions().circleCrop() builder.setDefaultRequestOptions(requestOptions)
Le cache
Nous savons à présent comment effectuer des requêtes relativement simples pour afficher des images, mais nous gâchons une partie de nos performances réseau et mémoire. En effet, si l’on appelle plusieurs fois la même image sur des écrans différents de l’application, il est dommage de refaire un appel réseau à chaque fois. C’est là où le cache de Glide intervient.
Glide peut utiliser différentes stratégies de stockage des images/GIFs: AUTOMATIC
(par défaut), ALL
, DATA
, NONE
et RESOURCE
. Vous trouverez le détail de chaque stratégie dans la documentation. On peut également, si on le souhaite, charger uniquement les images depuis le cache ou bien pas du tout grâce aux méthodes onlyRetrieveFromCache()
et skipMemoryCache()
.
Le cache (enregistrement, stockage et réutilisation des ressources) est géré par défaut par la bibliothèque, qui génère elle-même les clés nécessaires au stockage et à la récupération des images (cache keys). Ces clés contiennent au moins deux éléments, parmi lesquels le modèle d’objet chargé (File
, Uri
, Url
), une éventuelle Signature
, ainsi qu’un savant mélange selon les stratégies entre les dimensions des images, les éventuelles transformations et/ou options appliquées, ainsi que le type de données récupérées (bitmap, GIF, autre). Pour générer le nom de la clé, tous ces éléments sont hachés pour n’obtenir plus qu’un String
qui servira de nom de fichier.
Et nous avons besoin d’aller plus loin ?
C’est la partie la plus intéressante de cet article puisque jusqu’à maintenant, nous sommes pour l’instant restés uniquement à la surface de l’API.
Imaginons un cas où l’on a une image à afficher sur plusieurs écrans différents. Selon les cas, elle est communiquée par l’API à l’application soit par sa valeur en base64, soit par son URL. On se souvient que Glide gère lui-même le nommage de ses fichiers en local. La bibliothèque est donc dans l’incacapacité de faire le lien entre l’URL et l’image stockée en base64. Or, l’intérêt d’avoir un outil spécifiquement pour gérer un cache d’images et optimiser les données est bien évidement d’être capable de faire ce lien.
ModelLoader
et DataFetcher
Deux interfaces, mais encore ?
Ce sont les deux interfaces qui vont permettre de résoudre cette problématique. Comme l’indique la Javadoc, le ModelLoader
et le DataFetcher
vont de pair. Le DataFetcher
va être utilisé pour indiquer comment récupérer la donnée, et le ModelLoader
comment la stocker.
Nous allons réaliser un petit projet dans lequel nous allons exploiter la gestion du cache et faire ce lien entre plusieurs mêmes images. L’objectif va être de précharger tous les logos des différentes versions d’Android au cours des années et de les afficher ensuite, tout en ne les téléchargeant qu’une seule fois.
Nous allons avoir besoin de 2 objets qui vont représenter la même version Android et son logo:
data class AndroidLogo(val apiVersion: String, val url: String)
et
data class JsonAndroidLogo(val apiVersion: String, val image64: String)
Nous allons devoir indiquer à Glide que pour vérifier dans le cache si l’un de nos objets a déjà été téléchargé, il doit se référer à une signature que nous avons nous-même choisie.
Charger les données
Le ModelLoader
contient deux méthodes à surcharger :
buildLoadData
: renvoie unModelLoader.LoadData
contenant unDataFetcher
requis pour décoder la donnée fournie en entréehandles
: renvoietrue
si le type de donnée fourni en entrée est reconnu et enregistrable.
Dans le cas de l’objet JsonAndroidLogo
, il faut enregistrer une image en base64 et la lier à une version d’API Android. La cache key va donc être forcée de manière à savoir si le logo lié à cet id est déjà présent dans le cache ou pas, et s’il y a besoin de re-télécharger l’image, ou pas.
Pour forcer les cache keys, un objet de type Key
doit être utilisé. Dans l’exemple, il s’agit de ClientIdSignature
. Il est nécessaire de surcharger la méthode updateDiskCacheKey
car c’est ce qui va permettre de savoir si l’image est déjà enregistrée dans le cache ou non.
data class AndroidApiSignature(private val api: String) : Key { override fun updateDiskCacheKey(messageDigest: MessageDigest) { messageDigest.update(api.toByteArray()) } }
Il reste donc à créer le DataFetcher
correspondant :
class JsonAndroidLogoDataFetcher(private val model: JsonAndroidLogo) : DataFetcher<ByteBuffer> { override fun loadData( priority: Priority, callback: DataFetcher.DataCallback<in ByteBuffer> ) { val data: ByteArray = Base64.decode(model.image64, Base64.DEFAULT) val byteBuffer = ByteBuffer.wrap(data) callback.onDataReady(byteBuffer) } override fun cleanup() {} override fun cancel() {} override fun getDataClass(): Class<ByteBuffer> { return ByteBuffer::class.java } override fun getDataSource(): DataSource { return DataSource.REMOTE } }
Puis à le lier au ModelLoader
:
class JsonAndroidLogoModelLoader : ModelLoader<JsonAndroidLogo, ByteBuffer> { override fun buildLoadData( model: JsonAndroidLogo, width: Int, height: Int, options: Options ): ModelLoader.LoadData<ByteBuffer> { return ModelLoader.LoadData(AndroidApiSignature(model.apiVersion), JsonAndroidLogoDataFetcher(model)) } override fun handles(model: JsonAndroidLogo): Boolean { return !model.image64.isBlank() } }
Maintenant que le ModelLoader
est fonctionnel, il faut néanmoins le relier au GlideModule
de l’application. Une ModelLoaderFactory
va pour cela être utilisée, récapitulant le type de donnée d’entrée, de sortie et le ModelLoader
utilisé :
class JsonAndroidVersionModelLoaderFactory : ModelLoaderFactory<JsonAndroidLogo, ByteBuffer> { override fun build(unused: MultiModelLoaderFactory): ModelLoader<JsonAndroidLogo, ByteBuffer> { return JsonAndroidLogoModelLoader() } override fun teardown() { // Do nothing. } }
Ce ModelLoaderFactory
peut à présent être ajouté au Registry
du GlideModule
pour être totalement intégré à l’application:
override fun registerComponents(context: Context, glide: Glide, registry: Registry) { registry.prepend( JsonAndroidLogo::class.java, ByteBuffer::class.java, JsonAndroidVersionModelLoaderFactory() )
Une fois toutes ces étapes terminées, nous pouvons vérifier avec notre préchargement que lorsque je télécharge un objet JsonAndroidLogo
, il est bien ajouté à mon cache :
//androidLogos étant de type List<JsonAndroidLogo> androidLogos.forEach { GlideApp.with(this).load(it).preload() }
Attention cependant, à partir de maintenant nous n’allons plus demander à l’application de charger via la bibliothèque une URL ou une base64, mais bien un objet AndroidLogo
ou un objet JsonAndroidLogo
.
Il ne reste donc plus qu’à faire la même opération avec le AndroidLogo
, qui utilise lui une URL basique. Glide fournit dans son API plusieurs types de DataFetcher
s que l’on peut utiliser à notre convenance. Si une intégration à d’autres API est nécessaire (OkHttp ou autres), des bibliothèques en plus peuvent être disponibles (https://bumptech.github.io/glide/int/about.html)
Le ModelLoader
de notre AndroidLogo
ressemblera donc à ceci :
class AndroidLogoModelLoader : ModelLoader<AndroidLogo, InputStream> { override fun buildLoadData( model: AndroidLogo, width: Int, height: Int, options: Options ): ModelLoader.LoadData<InputStream> { return ModelLoader.LoadData( AndroidApiSignature(model.apiVersion), HttpUrlFetcher(GlideUrl(model.url), 500) ) } override fun handles(model: AndroidLogo): Boolean { return true } } class AndroidLogoModelLoaderFactory : ModelLoaderFactory<AndroidLogo, InputStream> { override fun build(unused: MultiModelLoaderFactory): ModelLoader<AndroidLogo, InputStream> { return AndroidLogoModelLoader() } override fun teardown() { // Do nothing. } }
L’AndroidLogoModelLoaderFactory
devra être ajoutée de la même manière au Registry
du GlideModule
et l’objectif sera rempli : si l’objet AndroidLogo
est déjà chargé dans le cache de Glide via l’URL, l’objet JsonAndroidLogo
ne sera pas rechargé en base64 et vice versa. Ceci peut être vérifié, puisque lorsque nous voulons afficher la liste d’images après le préchargement, nous forçons le fait de ne pas utilier d’autres resources que le cache et nous avons bien notre résultat graphique:
Glide.with(itemView) .load(item) .onlyRetrieveFromCache(true) .into(itemView.findViewById(R.id.image))
Vous pourrez retrouver le code ayant servi à cet article sur Github: https://github.com/cdreyfus/GitCacheSample .
Glide ne se limite pas à simplement afficher des images : l’utilisation des ressources (cache, appels réseaux) est anticipé et fait partie des nombreuses améliorations à faire au niveau de la plupart des applications Android
Sources:
Commentaire