为什么Xamarin阻塞中的异步HttpClient调用循环直到完成?

时间:2017-09-10 15:31:25

标签: c# xamarin xamarin.android

我在Xamarin for Android中编写了一个循环,我使用System.Net.Http.HttpClient异步加载了5个图像。启动5个请求会立即执行,但在完成所有5个响应之前我没有任何延续 - 大约4秒钟之后。为什么不能单独地,异步地回应?

我不太了解Xamarin中如何处理线程,所以我可能做错了什么。我不应该从UI线程调用它吗?有什么办法可以为HttpClient指定调度程序或线程策略吗?

加载函数的代码:

// This method is called 5 times in a foreach loop, initated from the UI thread, not awaited
private async void LoadAsync(String uri)
{
    Bitmap bitmap = await imgSvc.loadAndDecodeBitmap(uri);
    image.SetImageBitmap(bitmap);
}

public async Task<Bitmap> loadAndDecodeBitmap(String uri) { 
    var client = new HttpClient();
    byte[] data = await client.GetByteArrayAsync(uri);
    Bitmap img = BitmapFactory.DecodeByteArray(data, 0, data.Length);
    return img;
}

修改this repo

中的最小,可重复的示例

尝试在该示例中将ImagesToLoad在5和1之间切换,并查看第一个图像的加载时间如何发生显着变化(在我的计算机上差异大约为2秒)。

Load log 10 images
Load log if I only load 1

3 个答案:

答案 0 :(得分:1)

您应该考虑使用Task.WhenAll同时加载所有内容。您还创建了HttpClient的多个实例,这可能会对性能产生负面影响。

首先更新通话以避免使用async void

ArticleTeaserView.cs

public async Task SetModel(ArticleTeaser model) {
    title.SetText(model.promotionContent.title.value, TextView.BufferType.Normal);
    description.SetText(model.promotionContent.description.value, TextView.BufferType.Normal);
    try {
        var uri = Android.Net.Uri.Parse(model.promotionContent.imageAsset.urls[0].url);
        Log.Debug(TAG, "Image " + (++ctr) + " load starting...");
        await LoadAsync(model.promotionContent.imageAsset.GetUrlWithMinHeight(240), image);
        Log.Debug(TAG, "Image " + ctr + " load completed");
    } catch (Exception ex) {
        Log.Debug(TAG, ex.Message);
    }
}

static ImageSvc imgSvc = new ImageSvc(); //should consider injecting service

private async Task<Image> LoadAsync(String uri, ImageView image) {
    Bitmap bitmap = await imgSvc.loadAndDecodeBitmap(uri);
    image.SetImageBitmap(bitmap);
}

//Use only one shared instance of `HttpClient` for the life of the application
private static HttpClient client = new HttpClient();

public async Task<Bitmap> loadAndDecodeBitmap(String uri) {        
    byte[] data = await client.GetByteArrayAsync(uri);
    Bitmap img = BitmapFactory.DecodeByteArray(data, 0, data.Length);
    return img;
}

最后假设你有一个网址集合。您可以创建所有任务并同时调用所有任务。

MainActivity.cs

private event EventHandler LoadData = delegate { };

protected override void OnCreate(Bundle bundle) {
    base.OnCreate(bundle);

    // Set our view from the "main" layout resource
    SetContentView (Resource.Layout.Main);

    InitViews();
    LoadData += onDataLoading; // subscribe to event
    LoadData(this, EventArgs.Empty); // raise event
}

//async void allowed on event handlers (actual event handler)
//*OnCreate* is not an event handler. Just a based method.
private async void onDataLoading(object sender, EventArgs e) {
    LoadData -= onDataLoading;
    await LoadDataAsync();
}

private void InitViews() {
    //...
}

private async Task LoadDataAsync() {
    var svc = new NewsService();
    var promContent = svc.syncLoadStrong();
    await BindView(promContent);
}

private async Task BindView(ArticleList list) {
    Log.Debug(TAG, "Binding MainActivity");
    ViewGroup scroller = FindViewById<ViewGroup>(Resource.Id.scroller);        
    if (list != null) {
        var tasks = new List<Task>();
        foreach (ArticleTeaser teaser in list) {
            var atv = new ArticleTeaserView(this, null);
            tasks.Add(atv.SetModel(teaser));
            scroller.AddView(atv);
        }
        await Task.WhenAll(tasks);
    }
}

确保您没有将.Result.Wait()等阻止调用与async / await调用混合使用,并且除非是针对事件处理程序,否则请避免使用async void

答案 1 :(得分:0)

我的建议是使用HttpClient对象的单个实例。如上所述resizeColumnsToContents,您可以重复使用它来处理多重实例。

您遇到的问题是因为每次创建HttpClient对象时,都会打开一个连接并且此操作需要一些时间。同样,处理每个实例都需要关闭连接,这可能会对其他请求产生影响。

答案 2 :(得分:0)

HttpClient在并行运行时严重缩放。它似乎只在场景后面使用了几个(1?)线程,这些线程是相互依赖的,并且可以相互阻塞,无论我们将它包装在Task.Run中。在Client.GetByteArrayAsync()Task.Delay(1000)交换后,我得出了这个结论。他们的行为截然不同。

并行运行1或10 Task.Delay(1000)两者都能在刚好超过1200毫秒时给出第一个结果。请参阅the repo中的提交ff2a8e9 - 与LoadAndDecodeBitmapFake交换LoadAndDecodeBitmapFake

运行1 Client.GetByteArrayAsync()给我第一个结果〜1300ms后运行其中10个给我第一个结果~5000ms后。提交561691f

事实证明,最佳解决方案是以串行方式运行它们 - 等待每次执行而不是将它们混为一谈并在所有10上运行await Task.WhenAll()。请参阅提交eaa0f18。然后我得到~1400ms后的第一个结果和~2000ms后的最后一个结果!

听起来像HttpClient的设计缺陷,或者在重写/包裹到monodroid中时出现了一些损坏。