Je vous propose un petit tour d’horizon sur le stockage d’images dans l’Isolated Storage.
Pour rappel, l’Isolated storage est l’emplacement réservé à votre application Windows Phone pour le stockage de fichiers.
Quelles sont les implémentations qui permettent de récupérer une image déjà stockée dans cet espace ? Quelles sont les méthodes les plus efficaces ?
L’implémentation classique (Windows Phone 7)
C’est l’implémentation standard « old school » : j’utilise GetUserStoreForApplication() et OpenFile pour lire un Stream. Le Stream est ensuite lu par le BitmapImage et renvoyé dans une Action.
public static void LoadImageFromStorage(string name, Action<BitmapImage> actionBitmap) { using (IsolatedStorageFile myIsolatedStorage = IsolatedStorageFile.GetUserStoreForApplication()) { if (myIsolatedStorage != null && myIsolatedStorage.FileExists(name)) { IsolatedStorageFileStream fileStream = myIsolatedStorage.OpenFile(name, FileMode.Open, FileAccess.Read); { Deployment.Current.Dispatcher.BeginInvoke(() => { BitmapImage bitmapSource = new BitmapImage(); bitmapSource.SetSource(fileStream); if (actionBitmap != null) actionBitmap(bitmapSource); }); } } } }
Remarque 1
BitmapImage nécessite de s’exécuter dans le ThreadUI principal, pour cela, on utilise
Deployment.Current.Dispatcher.BeginInvoke(() =>
Remarque 2
Le Stream de l’isolated Storage peut s’exécuter dans un Thread en arrière plan, on ne peut pas ici utiliser une syntaxe comme celle ci dessous (qui permettrait de garantir la libération mémoire du FileStream).
using (IsolatedStorageFileStream fileStream = myIsolatedStorage.OpenFile(name, FileMode.Open, FileAccess.Read)) { ... }
Attention donc, car l’utilisation du using fermerait le FileStream avant que le ThreadUI ne puisse l’utiliser.
Utilisation de la méthode
L’utilisation de cette première méthode pourrait être celle ci :
IsolatedImageService.LoadImageFromStorage("TestImage.png", b => { Image = (b as BitmapImage); });
(où image représente une propriété de type BitmapImage)
L’implémentation Windows Phone 8 (async / await)
Windows Phone 8 permet l’ancienne syntaxe Windows Phone 7, mais ajoute également tout une gestion de l’Isolated Storage avec la nouvelle classe StorageFile.
La classe StorageFile va proposer des actions asynchrones assez efficaces.
Ici, j’utilise GetFileFromApplicationUriAsync pour récupérer en une seule instruction le fichier image de l’Isolated Storage.
Le Stream est ensuite accessible avec OpenStreamForReadAsync();
public static async Task<BitmapImage> LoadImageFromStorage2(string name) { Windows.Storage.StorageFile storageFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appdata:///local/" + name)); BitmapImage image = null; using (Stream stream = await storageFile.OpenStreamForReadAsync()) { using (MemoryStream ms = new MemoryStream()) { await stream.CopyToAsync(ms); await Task.Run(() => { synchroUI.Send(_ => { image = new BitmapImage(); image.SetSource(ms); }, null); }); } } return image; }
Remarque 1
Le code suivant :
DispatcherSynchronizationContext synchroUI = new DispatcherSynchronizationContext; ... await Task.Run(() => { synchroUI.Send(_ => {
permet d’utiliser l’UIThread principal, obligatoire pour l’utilisation de BitmapImage. Il peut être remplacé par
Deployment.Current.Dispatcher.BeginInvoke(() =>
Remarque 2
L’utilisation du MemoryStream permet d’éviter une fuite mémoire qui a lieu lorsque le Stream de l’IsolatedStorage reste bloqué dans le Thread de tâche de fond.
C’est le flux de données copié dans le MemoryStream qui va ensuite être utilisé par le BitmapImage.
using (MemoryStream ms = new MemoryStream()) { await stream.CopyToAsync(ms);
Remarque 3
Il est possible d’utiliser la syntaxe Using sur les Stream sans générer de fuites mémoire:
using (Stream stream = await storageFile.OpenStreamForReadAsync())
Appel de la méthode
La méthode peut alors être appelée de la façon suivante :
Image = await IsolatedImageService.LoadImageFromStorage2("TestImage.png");
(où Image est une propriété de type BitmapImage)
Une autre implementation async await Windows Phone 8
Il existe une variante du code précédent avec l’utilisation d’un IRandomAccessStream et du ReadAsync.
public static async Task<BitmapImage> LoadImageFromStorage3(string name) { Windows.Storage.StorageFolder localFolder = Windows.Storage.ApplicationData.Current.LocalFolder; Windows.Storage.StorageFile storageFile = await localFolder.GetFileAsync(name); BitmapImage image = null; IRandomAccessStream accessStream = await storageFile.OpenReadAsync(); using (Stream stream = accessStream.AsStreamForRead((int)accessStream.Size)) { byte[] content = new byte[stream.Length]; await stream.ReadAsync(content, 0, (int)stream.Length); await Task.Run(() => { synchroUI.Send(_ => { image = new BitmapImage(); using (var ms = new MemoryStream(content)) { image.SetSource(ms); } }, null); }); stream.Close(); } return image; }
Le ReadAsync lit les données dans un buffer et utilise un MemoryStream pour son injection dans BitmapImage.
Quelle méthode est la plus rapide ?
J’ai repris le programme de Benchmark et testé nos 3 méthodes en chargeant une image importante (400ko, 1000 pixels x 1000 pixels) 100 fois.
Voici les résultats (moyennes après une dizaine de tests consécutifs pour 100 appels):
- Méthode 1 (Action, syntaxe wp7 ) : 8,5 secondes
- Méthode 2 (WP8 avec GetFileFromApplicationUriAsync : 11 secondes
- Méthode 3 (WP8 avec IRandomAccessStream et ReadAsync : 9,5 secondes
Avec ce test, on voit que l’utilisation de IRandomAccessStream avec Async / Await n’est pas très éloignée d’une utilisation avec le simple Action.
Les 3 méthodes restent cependant incroyablement longues par rapport à l’utilisation directe d’une ressource de l’application.(0,3 s).
Conclusion : Privilégiez toujours l’utilisation des ressources lorsque c’est possible.
Le code source est ici :