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

Posted

技术标签:

【中文标题】从 TimerCallback 实例调用时,对 Dropbox API Client.Files.DownloadAsync 的调用不返回元数据【英文标题】:Call to Dropbox API Client.Files.DownloadAsync does not return metadata when call from an instance of TimerCallback 【发布时间】:2019-11-12 18:42:14 【问题描述】:

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

更新: 使用 iOS 版本,我只有在直接从我唯一的BackgroundSynchronizer.Launch() 方法调用我的downloader 的启动器(这也是一个任务)时才能成功下载文件AppDelegate,但在使用计时器委派此呼叫以通过 TimerCallback 调用我的 downloader 时不会,该 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.

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

【问题讨论】:

为什么启动的任务事件最终会被操作系统取消?你可以在那里添加一个try-catch,看看那里是否有任何异常。 您调用 Forms.Init 两次,这可能会导致问题。您可以在 FinishedLaunching 中调用它,也可以在 BackgroundSynchronizer 的构造函数中调用它。此外,您没有等待 TestDownload,因此可能会吞下任何异常......始终“等待”对异步方法的调用是最佳实践。但是,我看不到您在哪里调用 DownloadFile,因此这是一个不完整的示例。始终提供 MCVE(最小、完整、可验证的示例:***.com/help/mcve @JackHua-MSFT :已经有一个 try-catch,但正如 jgoldberger 所说,sn-p 丢失了;我现在包括它,还有记录的错误。 @jgoldberger-MSFT :我包括了你提到的 sn-p,但我不知道我可以实际复制多少代码来使我的帖子成为一个完整的 MCVE 示例,因为有很多部分创建 Xamarin 解决方案时我什至没有编辑的代码。你觉得这样就够完整了吗? TestDownload 很脏(重复调用 Forms.Init 并且没有等待),因为它的唯一目的是评估底层代码在从 AppDelegate 以这种方式调用时是否正常工作:一旦我修复同步器,整个函数将被删除。 Launch() 方法。 【参考方案1】:

好的,我终于找到了解决方法,将 .NET 计时器替换为 iOS 实现 (NSTimer)。

我的 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

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

【讨论】:

以上是关于从 TimerCallback 实例调用时,对 Dropbox API Client.Files.DownloadAsync 的调用不返回元数据的主要内容,如果未能解决你的问题,请参考以下文章

从基类调用派生类中的函数

每日JAVA面试

从两个子类调用时仅实例化一次父类

未导出成员函数时,从 C# 调用 C++ 本机/非托管成员函数

题18

线程池配置对实例性能的影响