Image depuis HTTP avec C# XAML – Comparatifs et Performances – Partie 1

Introduction

Chargement Bitmap Image en Http

Voici un article en 2 parties pour décrire les différentes implémentations possibles de la lecture d’images provenant d’un serveur en Http.
En terme de performances, il existe des différences assez conséquentes suivant les implémentations choisies.

Nous allons voir que parfois les résultats sont assez surprenants.

Charger une image depuis XAML

Le moyen le plus simple pour charger une image reste l’implémentation XAML.

<Image Source="{Binding ImageUrl}" Width="400" Height="240"/>

ImageUrl est de type Url et peut être déclarée de cette façon dans le viewModel associé à la Page.

public  Uri ImageUrl
{
    get
    {
        return new Uri(@"http://www.bing.com/az/hprichbg/rb/SnowyCP_FR-FR11972876588_400x240.jpg");
    }
}

Cette implémentation est très efficace, le contrôle XAML gère quasiment tout pour vous :

  • le chargement en asynchrone : Ne fige pas l’affichage de l’interface lors du chargement
  • la mise en cache mémoire (un seul chargement)
  • L’optimisation http (évite un échange serveur si l’image est déjà chargée)

Cependant l’image chargée en XAML sera de nouveau rechargée à chaque lancement de l’application.
Afin de mettre en place un système de cache persisté, il est nécessaire de charger le contenu de l’image et de la sauvegarder sur le téléphone dans l’Isolated Storage.

Quelles sont les méthodes possibles pour charger et afficher une image en asynchrone ?

Voici 9 façons de faire en Universal Apps (Windows Phone XAML ou Windows 8.1). Leurs temps de réponse seront mesurés sur Windows Phone 8.1 et exposés dans la 2ème partie de cet article.

Implémentations

 

1 – DataWriter

La première implémentation, la plus classique, est d’utiliser un httpClient pour récupérer le flux sous forme de Stream puis de le transférer dans le BitmapImage en passant par un DataWritter.

Windows 8 et les Universal Apps nous obligent à passer par un nouveau type de Stream le InMemoryRandomAccessStream pour l’afficher comme source dans le BitmapImage.

public static async Task<object> GethttpImage01(string urlImage)
{
    HttpClient client = new HttpClient { MaxResponseContentBufferSize = 1000000 };
    byte[] imageData = await client.GetByteArrayAsync(urlImage);
    InMemoryRandomAccessStream rstream = new InMemoryRandomAccessStream();
    IOutputStream outputStream = rstream.GetOutputStreamAt(0);
    DataWriter writer = new DataWriter(outputStream);
    writer.WriteBytes(imageData);           
    await writer.StoreAsync();
    await writer.FlushAsync();           
    BitmapImage b = new BitmapImage();
    await b.SetSourceAsync(rstream);
    return b;
}

Rien d’exceptionnel, c’est l’exemple le plus courant que l’on trouve sur les implémentations Windows 8.0.

2 – AsRandomAccessStream

La version 8.1 de Windows et Windows Phone Xaml a introduit une conversion implicite du MemoryStream en IMemoryRandomAccessStream.
Le même code peut être écrite de cette façon maintenant :

public static async Task<object> GethttpImage02(string urlImage)
{
    var client = new HttpClient();
    Stream stream = await client.GetStreamAsync(urlImage);
    var memStream = new MemoryStream();
    await stream.CopyToAsync(memStream);
    memStream.Position = 0;
    BitmapImage b = new BitmapImage();
    await b.SetSourceAsync(memStream.AsRandomAccessStream());
    return b;
}

Le code est du coup plus léger et plus lisible. Est-il plus rapide ?

3 – AsRandomAccessStream and ConfigureAwait.

Afin d’améliorer le concept la troisème écriture va utiliser l’option ConfigureAwait(false) sur les méthodes async/await.
Le ConfigureAwait(false) empêche au thread qui exécute la méthode en asynchrone de retourner sur le thread initial (souvent le Thread UI).

Lorsqu’il y a plusieurs await à la suite qui ne nécessitent pas un affichage, cela peut accélérer considérablement les performances.
Par contre BitmapImage a besoin de s’exécuter dans le ThreadUI, il faudra donc indiquer le retour au Thread UI avec la méthode RunAsync du Dispatcher.
Pour exécuter une action depuis une classe il faudra utiliser la syntaxe suivante pour retrouver le Dispatcher associé à l’écran principal.

await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync
  (Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
  {
//        UI Thread
  });

Ce qui nous donne :

public static async Task<object> GethttpImage03(string urlImage)
{
    var client = new HttpClient();
    Stream stream = await client.GetStreamAsync(urlImage).ConfigureAwait(false);
    var memStream = new MemoryStream();
    await stream.CopyToAsync(memStream).ConfigureAwait(false);
    memStream.Position = 0;
    BitmapImage b = null;
    await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync
        (Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
        {
            b = new BitmapImage();
            await b.SetSourceAsync(memStream.AsRandomAccessStream());
        });
    return b;
}

 4 – Transformation du flux MemoryStream dans un Task à part

La solution suivante consiste à réécrire la transformation du MemoryStream vers IRandomAccessTream dans une tâche exécutée en asynchrone.
La procédure de conversion ressemble à ça.

public static async Task<IRandomAccessStream> ConvertToRandomAccessStream(MemoryStream memoryStream)
{
    var randomAccessStream = new InMemoryRandomAccessStream();

    var outputStream = randomAccessStream.GetOutputStreamAt(0);
    var dw = new DataWriter(outputStream);
    var task = new Task(async () => 
            {
                dw.WriteBytes(memoryStream.ToArray());
                await dw.StoreAsync();
                await outputStream.FlushAsync();
            });
    task.Start();
    await task.ConfigureAwait(false);
            
    return randomAccessStream;
}

Maintenant nous pouvons utiliser cette méthode dans le chargement de l’image :

public static async Task<object> GethttpImage04(string urlImage)
{
    var httpClient = new HttpClient();
    var memStream = new MemoryStream();
    using (var responseStream = await httpClient.GetStreamAsync(new Uri(urlImage)))         
    {
        await responseStream.CopyToAsync(memStream).ConfigureAwait(false); 
        memStream.Position = 0;
    }
    IRandomAccessStream rstream = await ConvertToRandomAccessStream(memStream).ConfigureAwait(false);
    BitmapImage b = null;
    await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync
        (Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
        {
            b = new BitmapImage();
            await b.SetSourceAsync(rstream);
        });
    return b;
}

5 – Remplacement du HttpClient par le HttpWebRequest

On remplace l’accès http qui se fait simplement par le HttpClient par un HttpWebRequest.
Le HttpWebRequest est moins évolué que le HttpClient et permet un paramétrage plus fin tout en étant plus performant.
Rien de compliqué, le code reste identique, seul le début change.

public static async Task<object> GethttpImage05(string urlImage)
{
    var request = (HttpWebRequest)WebRequest.Create(urlImage);
    var memStream = new MemoryStream();
    request.Proxy = null;
    request.ContinueTimeout = 0;            
    WebResponse httpwebResponse = await request.GetResponseAsync().ConfigureAwait(false);
    using (var responseStream = httpwebResponse.GetResponseStream())
    {
        await responseStream.CopyToAsync(memStream).ConfigureAwait(false);
        memStream.Position = 0;
    }
    IRandomAccessStream rstream = await ConvertToRandomAccessStream(memStream).ConfigureAwait(false);
    BitmapImage b = null;
    await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync
        (Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
        {
            b = new BitmapImage();
            b.SetSource(rstream);                   
        });           
    return b;
}

6 – Actions à la place du Async / Await

Cela reste toujours une énigme mais l’utilisation du mode standard sans le async/await reste la plus performante (cf. article sur Windows Phone 7 et 8).
Le code est réécrit dans un fonctionnement avec des types Action (Comme au bon vieux temps)

private static void BitmapImageAction(string urlImage, Action<object> action)
{
    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(urlImage);
    request.Proxy = null;
    request.ContinueTimeout = 0;   
    request.BeginGetResponse(async result =>
    {
        using (var sr = request.EndGetResponse(result))
        {
            MemoryStream memStream = new MemoryStream();
            sr.GetResponseStream().CopyTo(memStream);                    
            IRandomAccessStream rstream = await ConvertToRandomAccessStream(memStream).ConfigureAwait(false);
            await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync
                (Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
                {
                    BitmapImage image = new BitmapImage();
                    image.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
                    image.SetSource(rstream);                            
                    if (action != null) action(image);
                });
        }
    }, null);
}

La seule différence avec la méthode 5 est donc le résultat qui est retourné par une Action.
Afin de l’utiliser facilement, la fonction est appelée dans un mécanisme async/await de cette façon :

public static Task<object> GethttpImage06(string urlImage)
{
    return Task.Run(() =>
    {
        var t = new TaskCompletionSource<object>();
        BitmapImageAction(urlImage, s => t.TrySetResult(s));
        return t.Task;
    });
}

7 – HttpWebRequest et AsRandomAccessStream

Une variante de la 3ème méthode avec le HttpWebRequest

public static async Task<object> GethttpImage07(string urlImage)
{
    var request = (HttpWebRequest)WebRequest.Create(urlImage);
    var memStream = new MemoryStream();
    request.Proxy = null;
    request.ContinueTimeout = 0;
    WebResponse httpwebResponse = await request.GetResponseAsync().ConfigureAwait(false);
    using (var responseStream = httpwebResponse.GetResponseStream())
    {
        await responseStream.CopyToAsync(memStream).ConfigureAwait(false);
        memStream.Position = 0;
    }

    BitmapImage b = null;
    await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync
        (Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
        {
            b = new BitmapImage();
            await b.SetSourceAsync(memStream.AsRandomAccessStream());
        });
    return b;            
}

 8 – HttpWebRequest et AsRandomAccessStream avec Action

C’est une variante de la solution 6. On remplace la conversion maison par le AsRandomAccessStream. Le tout est retourné par une Action.

private static void GethttpImageAction2(string urlImage, Action<object> action)
{
    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(urlImage);
    request.Proxy = null;
    request.ContinueTimeout = 0;
    MemoryStream memStream = new MemoryStream();
    request.BeginGetResponse(async result =>
    {
        using (var sr = request.EndGetResponse(result))
        {                    
            sr.GetResponseStream().CopyTo(memStream);                
            memStream.Position = 0;
        }                
        await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync
                (Windows.UI.Core.CoreDispatcherPriority.Low, () =>
                {
                    BitmapImage image = new BitmapImage();
                    image.SetSource(memStream.AsRandomAccessStream());
                    if (action != null) action(image);
                });                
    }, null);
}

public static Task<object> GethttpImage08(string urlImage)
{
    return Task.Run(() =>
    {
        var t = new TaskCompletionSource<object>();
        GethttpImageAction2(urlImage, s => t.TrySetResult(s));
        return t.Task;
    });
}

9 – HttpWebRequest, AsRandom Access Stream sans ConfigureAwait

Le même scénario que le 2 mais en utilisant HttpWebRequest à la place de HttpClient.

public static async Task<object> GethttpImage09(string urlImage)
{
    var request = (HttpWebRequest)WebRequest.Create(urlImage);
    var memStream = new MemoryStream();
    request.Proxy = null;
    request.ContinueTimeout = 0;
    WebResponse httpwebResponse = await request.GetResponseAsync();
    using (var responseStream = httpwebResponse.GetResponseStream())
    {
        responseStream.CopyTo(memStream);
        memStream.Position = 0;
    }

    BitmapImage b = null;
    await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync
        (Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
        {
            b = new BitmapImage();
            await b.SetSourceAsync(memStream.AsRandomAccessStream());
        });
    return b;
}

 Conclusion de la Partie 1

Plusieurs implémentations et des différences dans les résultats de performances. Certes pas énormes : 15-20%% d’écart, mais tout de même 15% de performances sur des devices mobiles c’est toujours bon à prendre.

Quelle est la méthode la plus rapide selon vous ?

Réponse très prochainement dans la deuxième Partie.

Laissez un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

deux × cinq =

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.