从 TimerCallback 实例调用时,调用 Dropbox API Client.Files.DownloadAsync 不会 return 元数据

Call to Dropbox API Client.Files.DownloadAsync does not return metadata when call from an instance of TimerCallback

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

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

我不明白为什么。

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);
        }

    }
}

编辑: 中介 class(嵌入了 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);
        }

    }

更新: 和失败的启动器 class(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.

编辑: 在第二种情况下,启动的任务事件最终被 OS:

取消
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.

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

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

我的 BackgroundSynchronizer 新代码 class:

    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

但对于两个计时器导致如此不同行为的原因,我仍然愿意接受开明的解释。