如何在iOS中运行后台服务以永久同步数据

12

你好,我正在开发一个应用程序,需要在Web服务器上执行同步操作(数据提交和检索)。

用户可以离线提交表单(即将数据存储到设备上的本地数据库)。每当有网络连接时,后台服务应该将这些数据提交到Web服务器。

后台服务的详细要求如下:

  • 后台服务将首先检查网络是否可用
  • 如果网络可用,它将收集存储在设备上的本地数据库(SQLite)中的数据
  • 它将提交数据到服务器
  • 请求服务器是否有任何新数据,如果有,则获取该数据并更新设备上的本地数据库。

我对iOS和xamarin/monotouch还比较陌生,想知道如何实现这个功能?

我知道iOS有各种后台模式,例如后台获取、nsurlsession、后台传输等。

我尝试实现后台获取,我认为它适合我的情况。但是它运行在自己的时间。

另外,如果用户关闭了我的应用程序,后台获取是否仍会被调用并继续运行我的应用程序?

代码在我的appdelegate中类似于 PerformFetch 方法:

if(networkService.IsNetworkAvailable())
{
   if(this.syncDataService.DownloadNewDataFromServer())
   {
       Console.WriteLine("Data downloaded successfully from server..");
   }
   if(this.syncDataService.UploadDataToServer())
   {
       Console.WriteLine("Data submitted successfully to server...");
   }
   completionHandler(UIBackgroundFetchResult.NewData);
}
else
{
   completionHandler(UIBackgroundFetchResult.NoData);
}

更新:最终我按照以下方式实现了它(希望对某些人有所帮助):
public class LocationUpdatedEventArgs : EventArgs
{
    private CLLocation location;

    public LocationUpdatedEventArgs(CLLocation location)
    {
        this.location = location;
    }

    public CLLocation Location
    {
        get { return this.location; }
    }
}

public class LocationManager
    {
        private static DateTime lastServiceRun;

        private CLLocationManager locMgr;

        public LocationManager()
        {
            this.locMgr = new CLLocationManager();
            this.LocationUpdated += this.PrintLocation;
            this.locMgr.Failed += (object sender, NSErrorEventArgs e) =>
            {
                Console.WriteLine("didFailWithError " + e.Error);
                Console.WriteLine("didFailWithError coe " + e.Error.Code);
            };
        }


        public event EventHandler<LocationUpdatedEventArgs> LocationUpdated = delegate { };


        public static TimeSpan TimeDiff { get; set; }


        public CLLocationManager LocMgr
        {
            get
            {
                return this.locMgr;
            }
        }


        public void StartLocationUpdates()
        {

            if (CLLocationManager.LocationServicesEnabled)
            {
                // sets the accuracy that we want in meters
                this.LocMgr.DesiredAccuracy = 1;

                //// Location updates are handled differently pre-iOS 6. If we want to support older versions of iOS,
                //// we want to do perform this check and let our LocationManager know how to handle location updates.

                if (UIDevice.CurrentDevice.CheckSystemVersion(6, 0))
                {
                    this.LocMgr.LocationsUpdated += (object sender, CLLocationsUpdatedEventArgs e) =>
                    {
                        //// fire our custom Location Updated event
                        this.LocationUpdated(this, new LocationUpdatedEventArgs(e.Locations[e.Locations.Length - 1]));
                    };
                }
                else
                {
                    //// this won't be called on iOS 6 (deprecated). We will get a warning here when we build.
                    this.LocMgr.UpdatedLocation += (object sender, CLLocationUpdatedEventArgs e) =>
                    {
                        this.LocationUpdated(this, new LocationUpdatedEventArgs(e.NewLocation));
                    };
                }

                //// Start our location updates
                this.LocMgr.StartUpdatingLocation();

                lastServiceRun = DateTime.Now;

                // Get some output from our manager in case of failure
                this.LocMgr.Failed += (object sender, NSErrorEventArgs e) =>
                {
                    Console.WriteLine(e.Error);
                };
            }
            else
            {
                //// Let the user know that they need to enable LocationServices
                Console.WriteLine("Location services not enabled, please enable this in your Settings");
            }
        }

        /// <summary>
        /// The stop updating location.
        /// </summary>
        public void StopUpdatingLocation()
        {
            this.locMgr.StopUpdatingLocation();
        }

        /// <summary>
        /// The print location. (This will keep going in the background)
        /// </summary>
        /// <param name="sender"> The sender. </param>
        /// <param name="e"> Location updated event argument </param>
        public void PrintLocation(object sender, LocationUpdatedEventArgs e)
        {
            CLLocation location = e.Location;

            Console.WriteLine("Longitude: " + location.Coordinate.Longitude);
            Console.WriteLine("Latitude: " + location.Coordinate.Latitude);

            var diff = DateTime.Now - lastServiceRun;
            TimeDiff = diff;
            if (TimeDiff.Minutes == 2)
            {
        // RunInBackground() is my method which call the service to upload/download data from server
                if (this.RunInBackground())
                {
                    lastServiceRun = DateTime.Now;
                }
            }
        }
}

你是怎么实现的?我有一个完全相同的任务(使用Xamarin),需要每5分钟将手机与服务器同步一次,我也尝试了后台获取,但它并没有每5分钟调用同步。我看到你下面的评论说你使用了LocationService,你能否分享你的代码或一些关于如何完成这个任务的链接? - subha
@Subha 我已经更新了我的问题,并附上了我用来调用位置服务的代码。如果您有任何疑问,请告诉我。 - SoftSan
感谢您的回复。我在论坛上问过这个问题,并被建议使用远程推送通知。我们已决定使用推送通知,因为我们的应用程序与位置服务无关。我只是想知道,我能否仅使用位置服务在后台运行某些功能而不更新任何位置信息? - subha
@Subha - 是的,你可以使用位置服务。由于苹果没有提供任何后台服务的方式,因此使用位置服务是在后台运行事物的一种方式,并且它会自动调用。但是,如果您需要在后台执行更大的任务,则可能不允许您执行它。同时,位置服务可能对电池寿命造成伤害,因为它会持续轮询位置,因此电池可能会很快耗尽。因此,请考虑所有这些因素来确定方法。 - SoftSan
这仍然有效,并且是最好的方法吗?你能展示一下RunInBackground()方法吗? - Marcel
4个回答

4
计时器在后台不起作用,并且您无法在此方法中添加另一个线程。 DidEnterBackground 用于完成未完成的任务。这通常需要大约30秒至1分钟的时间。
您可以在此方法中添加循环或长时间运行的任务。

谢谢您的答复。我明白计时器永远不会在后台工作,但我有一个要求,需要运行后台服务来同步从服务器上传和下载的数据。我尝试了后台获取,但它并不可靠。那么,您能否提供一些关于如何在iOS中实现此功能的建议或重点? - SoftSan
我有类似的需求,我的解决方法是:添加一个定时器,在后台调用一个方法(但这仅在应用程序处于活动状态时有效,不会影响项目的正常流程)。然后在后台调用它一次。稍后当我的应用再次回到前台时,它会再次继续计时器。这是一个解决方法,因为我尝试了许多方法,我还尝试在进入后台和前台时添加通知,但无法重复调用后台方法。 - TechGirl
据我所了解,你是可以做到的。具有位置服务、音频、视频、新闻等功能的应用程序可以永久在后台运行。iOS不会杀死这个应用程序。但是如果你要提交到Appstore,最好使用这些服务。尝试并希望它能够正常工作,一旦完成,请不要忘记发布你的答案,同时我也会尝试。 - TechGirl
好的,我已经实现了相同的功能,它运行良好。我在位置服务中设置了一个条件,每隔X分钟同步数据。它运行良好,应用程序没有崩溃或闪退。但是我仍然需要检查苹果商店是否会批准它? - SoftSan
感谢您的帮助和支持。 - SoftSan
显示剩余3条评论

3
我认为解决您的问题的方法是将任务注册为长时间运行的任务。
考虑到您正在使用Xamarin,这里还有一个非常好的关于使用Xamarin进行后台任务的解释,可能更加相关。请点击此处查看。
另外,请参阅应用程序状态和多任务处理,了解应用程序进入后台时发生的情况:"从applicationDidEnterBackground:方法返回后,大多数应用程序不久后会进入暂停状态。请求特定后台任务(如播放音乐)或从系统请求一点额外执行时间的应用程序可能会继续运行更长时间。"
通常来说,浏览苹果开发者网站是个好事情,因为100%的iOS API都在Xamarin中。

2
一个关于iOS的想法。一种实现方式可能是创建一个后台任务调度器,可能基于LimitedConcurrencyLevelTaskScheduler (https://msdn.microsoft.com/en-us/library/ee789351(v=vs.110).aspx),用于执行您的IBackgroundTask作业。这些作业可以持久化到存储中,并且将按顺序执行。例如,如果你想下载很多大文件,你需要逐个执行它们并显示进度或在UI上渲染它们当它们加载到设备上时。 然后,您可以使用该调度程序将任何下载任务排队,并在任务即将结束时重新启动它:

void StartBackgroundTask()
{
 BackgroundTaskId = UIApplication.SharedApplication.BeginBackgroundTask(() => {
  // Handle expiration: will happen before reaching 600 secs
  TaskScheduler.Stop();
  UIApplication.SharedApplication.EndBackgroundTask(BackgroundTaskId);

  // Start again
  StartBackgroundTask();
 });

 Task.Run(() => {
  TaskScheduler = new BackgroundTaskScheduler().Start();
 });
}


2

在您的应用程序委托的FinishedLaunching方法中,您可以创建一个定时器,在您设置的时间间隔内调用您的代码。

    public override bool FinishedLaunching(UIApplication app, NSDictionary options)
    {
        ...
        Timer timer = new Timer(5000); //5000 milisecs - 5 secs
        timer.Elapsed += timer_Elapsed;
        timer.Start();
        ...
    }

    private void timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        //your code here
    }

如果用户关闭你的应用程序,那么就不会再调用任何代码,包括后台获取。


这意味着我们的应用程序必须在后台任务中运行才能工作。有没有办法处理这种情况?同时,设置定时器也会耗电。对吧?还有,有被应用商店拒绝的可能性吗? - SoftSan
不,没有办法处理这种情况,这不是 Android,你无法创建某些服务,如果应用程序被杀死,那么没有任何方式可以运行任何代码。只有在调用代码时才会耗电,而不是一直。 - choper
谢谢你的回答。但如果我使用上述代码,我的应用程序应该在前台运行。而我想让它在后台运行。那么该怎么做? - SoftSan
抱歉,我不太明白你所说的“在后台运行”的确切含义是什么? - choper
我写了以下代码:'public override void DidEnterBackground(UIApplication application){} - SoftSan
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接