当应用程序快速终止时解析保存最终失败

pyr*_*cam 3 ios parse-platform

我有一个应用程序,它使用 parse 将数据存储在 parse 的本地数据存储中,并将所述数据备份到解析云。一般来说,这种方法效果很好。在本地和云端存储数据的关键代码如下:

- (void)store:(PFObject*) parseObject {
    if (parseObject) {
        [parseObject pinInBackground];
        [parseObject saveEventually];
    } else
        NSLog(@"Err :Store was passed a nil?");
}
Run Code Online (Sandbox Code Playgroud)

我有一个应用程序,一些用户表示,如果他们设置数据然后“不久”终止该应用程序,则会丢失数据。

当用户数据更新时,该函数会快速连续传递大约 10 个或更多项目来存储。

我通过执行以下操作测试了这种情况。我让所有项目都被存储,并设置一个断点,以便在完成后命中。然后,我让应用程序再次运行,并通过按主页键并滑开应用程序来终止它。它还有大约一秒左右的运行时间,但关键点是无论如何,每个对象的存储都已完成。

我确实发现数据可能会丢失。看来,仅仅因为这些方法已运行并不能保证数据将被存储。需要明确的是,我知道这些函数不存储数据,但我曾认为(假设)在完成后可以保证存储数据的意图。

我想添加以下内容:

  1. 后期数据更容易丢失。也就是说,解析似乎按顺序处理数据。
  2. 您可以发现数据已固定在本地数据存储中,但无法对其进行解析(几乎就像固定有效,但最终保存没有)。
  3. 较旧(较慢?)的设备比新设备更容易受到影响。事实上,我很难在新的 iPad mini 上实现这一点,但可以在 iPhone 4 上实现。
  4. 需要启用网络,但如果您模拟不良网络(使用设备上的 iOS 设置),则更容易实现这一点。
  5. 数据量很小(100 字节),我没有达到解析保存限制。
  6. 我使用的是 Parse 版本 1.8.4。

我的问题如下:

我预计一旦这些电话返回,任务就会被锁定并且始终会完成。我知道无法保证 saveeventually 可能需要多长时间,但即使在下次运行应用程序时,它也始终会“最终”完成。我做错了什么吗?我是否面临这种数据丢失的风险并需要采取进一步的预防措施?有人有什么经验或建议吗?即使只是您发现它对您有用?可能我在其他地方做了一些愚蠢的事情,但很难看出是怎么做的。

谢谢你的时间。

pyr*_*cam 5

我在这里发布我自己的结果,也许它们对某人有用。这是我在查看 Parse 源代码后对其工作原理的理解,我不能保证这是最终的结论或整个故事,但它符合事实。

Parse 现在是开源的,所以如果遇到问题,您至少可以去尝试确定发生了什么。

在github上解析

Parse 保留一个任务队列,它们严格按照请求的顺序进行处理。无论任务是在前台(阻塞)还是在后台(有或没有回调)请求,都是如此。另外,无论任务是查询、固定、保存等,它们都在任务列表中排队。任务列表保存在内存中,如果应用程序被挂起然后终止或在所有任务处理完毕之前刚刚终止,则任务将丢失。

因此,如果解析没有执行任何操作,并且您请求 pin 或保存,它将启动此操作,并且很可能一切都会好起来。

如果解析很忙(根据我上面的描述),您的 saveEventually 位于等待处理的队列末尾。在处理此任务之前,您将面临 Parse 不记录 saveEventually 的风险。请注意,重要的是要理解,过程是指查看任务 - 不是完成任务 - 只是简单地查看任务是什么并将其记录在 saveEventually 的情况下。

我不一定认为这是一个错误。我的问题是我将 saveEventually 视为数据库提交。我知道这并不意味着它还在云中,我只是假设当 saveEventually 返回时,将其推送到云的请求以非易失性方式存储。一旦处理完毕,它就像提交一样,最终会将其发送到云端,但您无法确定它是否已以编程方式处理(saveEventually 回调是在完成时而不是记录 saveEventually)。

如果应用程序被核武器攻击,那就这样吧,用户已经采取了一些行动,他们应该知道自己在做什么。但是,如果您声明您的应用程序有后台任务,则可以确保在用户按下主页键或移动到另一个应用程序时仍然有一个线程可以执行 - 我认为他们有权这样做。我已经这样做了,并且似乎可以防止我看到的数据丢失。我基本上确保任务列表得到处理,并且 Parse 已记录我所有的 saveEventually,即使应用程序转到后台也是如此。

iOS 后台执行

很难确定何时终止该线程。在我的应用程序中,我进行了 pin 操作并最终成对保存。Pin 确实有回调,所以我保留了未完成的 pin 计数。当它达到零时,我再等待一分钟并关闭线程。这给了 1 分钟的时间来执行最终的 saveEventually,这在测试中似乎绰绰有余。如果我当时可以访问互联网,则稍后或下次运行应用程序时会注意到 saveEventually 并不重要。

我认为您必须是 Parse 的重度用户才能看到这个问题。我有很多小数据对象分布在不相关的不同表中。这会创建大量需要备份的查询和固定,如果您有一个表或一个可以一次性固定的数据块,您将不会看到我描述的问题。

示例代码(2016 年 1 月 7 日编辑)根据评论请求

下面是我用来实现上述目标的核心代码。如果您使用它,则需要更改它。我也建议阅读上面的后台任务链接。后台线程使应用程序保持足够的活动状态,以便解析仍然可以处理其未完成的任务。

此代码块的目的是提供一个辅助函数来跟踪所有待解析的未完成请求。当它被调用时,它会启动一个后台线程(startPinMonitor)。仅启动一个监视器,这在 startPinMonitor 中进行检查。我的代码在 UI 线程上运行,但否则您可能需要一些同步逻辑。

- (void)store:(PFObject*) parseObject {
    if (parseObject) {
        if (self.outStandingPins == 0)
            [self startPinMonitor];

        self.outStandingPins++;

        [parseObject pinInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
            self.outStandingPins--;
            if(!succeeded)
                NSLog(@"Err : Pin failed?");
        }];

        [parseObject saveEventually];
    }
}
Run Code Online (Sandbox Code Playgroud)

这段代码的目的是让应用程序保持运行,直到处理完所有剩余的解析操作。它首先检查监视器是否已处于活动状态。“checkPinStatus”方法每 30 秒调用一次,并在处理完所有引脚后终止。未完成引脚的日志对于检测正确操作非常有用。您应该能够验证按主页键后应用程序仍在运行。如果您注释掉“beginBackgroundTaskWithExpirationHandler”方法,您可以确定此代码试图实现的行为差异。

-(void) startPinMonitor {
    if (![self.myTimer isValid]) {
        self.myTimer = [NSTimer scheduledTimerWithTimeInterval:30
                                                        target:self
                                  selector:@selector(checkPinStatus)
                                                      userInfo:nil
                                                       repeats:YES];

        self.myBackgroundTask = [[UIApplication sharedApplication]
                         beginBackgroundTaskWithExpirationHandler:^{
                             NSLog(@"Background tasks stopped");
                         }];
    }
}

-(void) checkPinStatus {
    NSLog(@"Current outStandingPins=%i", self.outStandingPins);

    if (self.outStandingPins == 0) {
        [self.myTimer invalidate];
        [[UIApplication sharedApplication]
            endBackgroundTask:self.myBackgroundTask];
    }
}
Run Code Online (Sandbox Code Playgroud)