ios - 从 TimerCallback 实例调用时,调用 Dropbox API Client.Files.DownloadAsync 不会返回元数据

标签 ios xamarin asynchronous dropbox-api dotnet-httpclient

我有一个移动跨平台 Xamarin.Forms 项目,在其中我尝试在启动时从 Dropbox 存储库下载文件。这是一个不到 50kB 的小 json 文件。操作 Dropbox API 调用的代码在我的 Android 和 iOS 项目之间共享,并且我的 Android 实现按预期工作。这是Task我将其称为 downloader 的方法为方便起见,请放在这里。

更新:使用iOS版本,我只有在调用我的downloader时才能成功下载文件。直接从 BackgroundSynchronizer.Launch() 启动器(这也是一个任务)我唯一的方法AppDelegate ,但在使用计时器委托(delegate)此调用来调用我的 downloader 时则不然通过TimerCallback它调用 EventHandler重复出现。

我不明白为什么。

downloader :

public class DropboxStorage : IDistantStoreService
{
    private string oAuthToken;
    private DropboxClientConfig clientConfig; 
    private Logger logger = new Logger
        (DependencyService.Get<ILoggingBackend>());

    public DropboxStorage()
    {
        var httpClient = new HttpClient(new NativeMessageHandler());
        clientConfig = new DropboxClientConfig
        {
            HttpClient = httpClient
        };
    }

    public async Task SetConnection()
    {
        await GetAccessToken();
    }

    public async Task<Stream> DownloadFile(string distantUri)
    {
        logger.Info("Dropbox downloader called.");
        try
        {
            await SetConnection();
            using var client = new DropboxClient(oAuthToken, clientConfig);
            var downloadArg = new DownloadArg(distantUri);
            var metadata = await client.Files.DownloadAsync(downloadArg);
            var stream = metadata?.GetContentAsStreamAsync();
            return await stream;
        }
        catch (Exception ex)
        {
            logger.Error(ex);
        }
        return null;
    }

更新:AppDelegate :

using Foundation;
using UIKit;

namespace Izibio.iOS
{
    // The UIApplicationDelegate for the application. This class is responsible for launching the 
    // User Interface of the application, as well as listening (and optionally responding) to 
    // application events from iOS.
    [Register("AppDelegate")]
    public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
    {

        private BackgroundSynchronizer synchronizer = new BackgroundSynchronizer();
        //
        // This method is invoked when the application has loaded and is ready to run. In this 
        // method you should instantiate the window, load the UI into it and then make the window
        // visible.
        //
        // You have 17 seconds to return from this method, or iOS will terminate your application.
        //
        public override bool FinishedLaunching(UIApplication app, NSDictionary options)
        {
            global::Xamarin.Forms.Forms.Init();
            LoadApplication(new App());

            return base.FinishedLaunching(app, options);
        }

        public override void OnActivated(UIApplication uiApplication)
        {
            synchronizer.Launch();
            base.OnActivated(uiApplication);
        }

    }
}

编辑:中间类(嵌入 DownloadProducts 函数):

public static class DropboxNetworkRequests
    {
        public static async Task DownloadProducts(IDistantStoreService distantStorage,
            IStoreService localStorage)
        {
            try
            {
                var productsFileName = Path.GetFileName(Globals.ProductsFile);
                var storeDirectory = $"/{Globals.StoreId}_products";
                var productsFileUri = Path.Combine(storeDirectory, productsFileName);
                var stream = await distantStorage.DownloadFile(productsFileUri);
                if (stream != null)
                {
                    await localStorage.Save(stream, productsFileUri);
                }
                else
                {
                    var logger = GetLogger();
                    logger.Info($"No file with the uri ’{productsFileUri}’ could " +
                        $"have been downloaded.");
                }
            }
            catch (Exception ex)
            {
                var logger = GetLogger();
                logger.Error(ex);
            }
        }

        private static Logger GetLogger()
        {
            var loggingBackend = DependencyService.Get<ILoggingBackend>();
            return new Logger(loggingBackend);
        }

    }

更新: 失败的启动器类(Launch 方法中注释的 TriggerNetworkOperations(this, EventArgs.Empty); 成功下载文件):

public class BackgroundSynchronizer
{
    private bool isDownloadRunning;
    private IDistantStoreService distantStorage;
    private IStoreService localStorage;
    private Timer timer;
    public event EventHandler SynchronizationRequested;

    public BackgroundSynchronizer()
    {
        Forms.Init();
        isDownloadRunning = false;
        distantStorage = DependencyService.Get<IDistantStoreService>();
        localStorage = DependencyService.Get<IStoreService>();
        Connectivity.ConnectivityChanged += TriggerNetworkOperations;
        SynchronizationRequested += TriggerNetworkOperations;
    }

    public void Launch()
    {
        try
        {
            var millisecondsInterval = Globals.AutoDownloadMillisecondsInterval;
            var callback = new TimerCallback(SynchronizationCallback);
            timer = new Timer(callback, this, 0, 0);
            timer.Change(0, millisecondsInterval);
            //TriggerNetworkOperations(this, EventArgs.Empty);
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }

    protected virtual void OnSynchronizationRequested(object sender, EventArgs e)
    {
        SynchronizationRequested?.Invoke(sender, e);
    }

    private async void TriggerNetworkOperations(object sender, ConnectivityChangedEventArgs e)
    {
        if ((e.NetworkAccess == NetworkAccess.Internet) && !isDownloadRunning)
        {
            await DownloadProducts(sender);
        }
    }

    private async void TriggerNetworkOperations(object sender, EventArgs e)
    {
        if (!isDownloadRunning)
        {
            await DownloadProducts(sender);
        }
    }

    private void SynchronizationCallback(object state)
    {
        SynchronizationRequested(state, EventArgs.Empty);
    }

    private async Task DownloadProducts(object sender)
    {
        var instance = (BackgroundSynchronizer)sender;
        //Anti-reentrance assignments commented for debugging purposes
        //isDownloadRunning = true;
        await DropboxNetworkRequests.DownloadProducts(instance.distantStorage, instance.localStorage);
        //isDownloadRunning = false;
    }
}

我设置了一个日志文件来记录尝试下载时的应用程序行为。

编辑:以下是从 Launch 方法直接调用 TriggerNetworkOperations 时收到的消息:

2019-11-12 19:31:57.1758|INFO|xamarinLogger|iZiBio Mobile Launched
2019-11-12 19:31:57.4875|INFO|persistenceLogger|Dropbox downloader called.
2019-11-12 19:31:58.4810|INFO|persistenceLogger|Writing /MAZEDI_products/assortiment.json at /Users/dev3/Library/Developer/CoreSimulator/Devices/5BABB56B-9B42-4653-9D3E-3C60CFFD50A8/data/Containers/Data/Application/D6C517E9-3446-4916-AD8D-565F4C206AF2/Library/assortiment.json

编辑:这是我通过计时器及其回调启动时得到的结果(出于调试目的,间隔 10 秒):

2019-11-12 19:34:05.5166|INFO|xamarinLogger|iZiBio Mobile Launched
2019-11-12 19:34:05.8149|INFO|persistenceLogger|Dropbox downloader called.
2019-11-12 19:34:15.8083|INFO|persistenceLogger|Dropbox downloader called.
2019-11-12 19:34:25.8087|INFO|persistenceLogger|Dropbox downloader called.
2019-11-12 19:34:35.8089|INFO|persistenceLogger|Dropbox downloader called.

编辑:在第二种情况下,启动的任务事件最终被操作系统取消:

2019-11-13 09:36:29.7359|ERROR|persistenceLogger|System.Threading.Tasks.TaskCanceledException: A task was canceled.
  at ModernHttpClient.NativeMessageHandler.SendAsync (System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) [0x002a5] in /Users/paul/code/paulcbetts/modernhttpclient/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs:139 
  at System.Net.Http.HttpClient.SendAsyncWorker (System.Net.Http.HttpRequestMessage request, System.Net.Http.HttpCompletionOption completionOption, System.Threading.CancellationToken cancellationToken) [0x0009e] in /Users/builder/jenkins/workspace/xamarin-macios/xamarin-macios/external/mono/mcs/class/System.Net.Http/System.Net.Http/HttpClient.cs:281 
  at Dropbox.Api.DropboxRequestHandler.RequestJsonString (System.String host, System.String routeName, System.String auth, Dropbox.Api.DropboxRequestHandler+RouteStyle routeStyle, System.String requestArg, System.IO.Stream body) [0x0030f] in <8d8475f2111a4ae5850a1c1349c08d28>:0 
  at Dropbox.Api.DropboxRequestHandler.RequestJsonStringWithRetry (System.String host, System.String routeName, System.String auth, Dropbox.Api.DropboxRequestHandler+RouteStyle routeStyle, System.String requestArg, System.IO.Stream body) [0x000f6] in <8d8475f2111a4ae5850a1c1349c08d28>:0 
  at Dropbox.Api.DropboxRequestHandler.Dropbox.Api.Stone.ITransport.SendDownloadRequestAsync[TRequest,TResponse,TError] (TRequest request, System.String host, System.String route, System.String auth, Dropbox.Api.Stone.IEncoder`1[T] requestEncoder, Dropbox.Api.Stone.IDecoder`1[T] resposneDecoder, Dropbox.Api.Stone.IDecoder`1[T] errorDecoder) [0x000a5] in <8d8475f2111a4ae5850a1c1349c08d28>:0 
  at Izibio.Persistence.DropboxStorage.DownloadFile (System.String distantUri) [0x00105] in /Users/dev3/Virtual Machines.localized/shared/TRACAVRAC/izibio-mobile/Izibio/Izibio.Persistence/Services/DropboxStorage.cs:44 
2019-11-13 09:36:29.7399|INFO|persistenceLogger|No file with the uri ’/******_products/assortiment.json’ could have been downloaded.

我将简单地添加最后一个观察结果:调试 DownloadFile 时任务来自BackgroundSynchronizer ,我可以调用client.Files.DowloadAsync :var metadata = await client.Files.DownloadAsync(downloadArg); ,但我不会从此等待语句中检索任何返回。

最佳答案

好吧,我终于找到了解决这个问题的方法,用 iOS 实现 (NSTimer) 替换 .NET 计时器。

我的BackgroundSynchronizer类的新代码:

    public class BackgroundSynchronizer
    {
        private bool isDownloadRunning;
        private IDistantStoreService distantStorage;
        private IStoreService localStorage;
        private NSTimer timer;
        public event EventHandler SynchronizationRequested;

        public BackgroundSynchronizer()
        {
            Forms.Init();
            isDownloadRunning = false;
            distantStorage = DependencyService.Get<IDistantStoreService>();
            localStorage = DependencyService.Get<IStoreService>();
            Connectivity.ConnectivityChanged += TriggerNetworkOperations;
            SynchronizationRequested += TriggerNetworkOperations;
        }

        public void Launch()
        {
            try
            {
                var seconds = Globals.AutoDownloadMillisecondsInterval / 1000;
                var interval = new TimeSpan(0, 0, seconds);
                var callback = new Action<NSTimer>(SynchronizationCallback);
                StartTimer(interval, callback);
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        protected virtual void OnSynchronizationRequested(object sender, EventArgs e)
        {
            SynchronizationRequested?.Invoke(sender, e);
        }

        private async void TriggerNetworkOperations(object sender, ConnectivityChangedEventArgs e)
        {
            if ((e.NetworkAccess == NetworkAccess.Internet) && !isDownloadRunning)
            {
                await DownloadProducts();
            }
        }

        private async void TriggerNetworkOperations(object sender, EventArgs e)
        {
            if (!isDownloadRunning)
            {
                await DownloadProducts();
            }
        }

        private void SynchronizationCallback(object state)
        {
            SynchronizationRequested(state, EventArgs.Empty);
        }

        private async Task DownloadProducts()
        {
            isDownloadRunning = true;
            await DropboxNetworkRequests.DownloadProducts(distantStorage, localStorage);
            isDownloadRunning = false;
        }

        private void StartTimer(TimeSpan interval, Action<NSTimer> callback)
        {
            timer = NSTimer.CreateRepeatingTimer(interval, callback);
            NSRunLoop.Main.AddTimer(timer, NSRunLoopMode.Common);   
        }
    }

这会产生以下日志行:

2019-11-13 14:00:58.2086|INFO|xamarinLogger|iZiBio Mobile Launched
2019-11-13 14:01:08.5378|INFO|persistenceLogger|Dropbox downloader called.
2019-11-13 14:01:09.5656|INFO|persistenceLogger|Writing /****_products/assortiment.json at /Users/dev3/Library/Developer/CoreSimulator/Devices/****/data/Containers/Data/Application/****/Library/assortiment.json
2019-11-13 14:01:18.5303|INFO|persistenceLogger|Dropbox downloader called.
2019-11-13 14:01:19.2375|INFO|persistenceLogger|Writing /****_products/assortiment.json at /Users/dev3/Library/Developer/CoreSimulator/Devices/****/data/Containers/Data/Application/****/Library/assortiment.json

但我仍然愿意对两个计时器导致如此不同行为的原因进行明智的解释。

关于ios - 从 TimerCallback 实例调用时,调用 Dropbox API Client.Files.DownloadAsync 不会返回元数据,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58824805/

相关文章:

c# - C#中异步调用方法

javascript - 递归中的异步方法如何正确返回结果?

iphone - 在 Xcode 中存档不产生存档

c# 在父级返回时在后台运行异步任务

ios - NSXMLParser 的错误处理

ios - 我如何从 DatePicker 和 Timepicker 获取通知(仅用户在 Xamarin 表单 IOS 中按下“完成”按钮)

c# - 对 REST API 的未授权访问

listview - 如何将列表项绑定(bind)到它是否包含在另一个集合中

ios - 适用于 iOS 的 Google Analytics(分析)SDK 是否具有 SSL 固定?

ios - 使用 UISearchController 的 segue 的不同行为