ios - iPad Pro 第三代无缘无故杀死前台应用程序

标签 ios xcode cpu kill

我有一个已经流行多年的应用程序。

为了在离线状态下 100% 正常运行,该应用程序只需下载一次数十万张图像(每个对象 1 张)(增量更新根据需要进行处理)。

对象数据本身没有问题。

但是,最近,我们的应用程序在仅下载图像时开始崩溃,但仅限于较新的 iPad(具有充足存储空间的第三代 iPad Pro)。

图像下载过程使用 NSOperationQueue 内的 NSURLSession 下载任务。

我们开始看到能源日志指出 CPU 使用率过高,因此我们修改了参数,使用

在每个图像之间以及每批图像之间添加了一个中断

[NSThread sleepForTimeInterval:someTime];

这将我们的 CPU 使用率从远高于 95%(这很公平)降低到低于 18%!

不幸的是,仅几个小时后,该应用程序在较新的 iPad 上仍然会崩溃。然而,在我们的 2016 iPad Pro 第一代上,即使下载 24 小时后,该应用程序也根本不会崩溃。

从设备中提取崩溃日志时,我们看到的是 CPU 使用率超过 50% 的时间超过 3 分钟。没有出现其他崩溃日志。

这些设备均已插入电源,并将其锁定时间设置为“从不”,以便让 iPad 保持唤醒状态并让我们的应用程序处于前台。

为了解决这个问题,我们降低了性能,基本上在每个图像之间等待 30 秒,在每批图像之间等待 2 分钟。这有效并且崩溃停止了,但是,这需要几天时间才能下载我们所有的图像。

我们正在尝试找到一种折衷方案,其中性能合理,并且应用程序不会崩溃。

然而,令我困扰的是,无论设置如何,即使在全速性能下,该应用程序也不会在旧设备上崩溃,只会在较新设备上崩溃。

传统观点认为这是不可能的。

我在这里缺少什么?

当我使用 Instruments 进行分析时,我发现应用程序在下载时的平均速度为 13%,而且批处理之间有 20 秒的间隔,因此 iPad 应该有足够的时间来进行清理。

有人有什么想法吗?请随意索取更多信息,我不确定还有什么会有帮助。

编辑 1:下载器代码如下:

//Assume the following instance variables are set up:

self.operationQueue = NSOperationQueue to download the images.

self.urlSession = NSURLSession with ephemeralSessionConfiguration, 60 second timeoutIntervalForRequest

self.conditions = NSMutableArray to house the NSConditions used below.

self.countRemaining = NSUInteger which keeps track of how many images are left to be downloaded.

//Starts the downloading process by setting up the variables needed for downloading.
-(void)startDownloading
{
    //If the operation queue doesn't exist, re-create it here.
    if(!self.operationQueue)
    {
        self.operationQueue = [[NSOperationQueue alloc] init];
        [self.operationQueue addObserver:self forKeyPath:KEY_PATH options:0 context:nil];
        [self.operationQueue setName:QUEUE_NAME];
        [self.operationQueue setMaxConcurrentOperationCount:2];
    }

    //If the session is nil, re-create it here.
    if(!self.urlSession)
    {
        self.urlSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]
                                                        delegate:self
                                                   delegateQueue:nil];
    }

    if([self.countRemaining count] == 0)
    {
        [self performSelectorInBackground:@selector(startDownloadForNextBatch:) withObject:nil];

        self.countRemaining = 1;
    }
}

//Starts each batch. Called again on observance of the operation queue's task count being 0.
-(void)startDownloadForNextBatch:
{
    [NSThread sleepForTimeInterval:20.0]; // 20 second gap between batches

    self.countRemaining = //Go get the count remaining from the database.

    if (countRemaining > 0)
    {
        NSArray *imageRecordsToDownload = //Go get the next batch of URLs for the images to download from the database.

        [imageRecordsToDownload enumerateObjectsUsingBlock:^(NSDictionary *imageRecord,
                                                                          NSUInteger index,
                                                                          BOOL *stop)
         {
            NSInvocationOperation *invokeOp = [[NSInvocationOperation alloc] initWithTarget:self
                                                                                   selector:@selector(downloadImageForRecord:)
                                                                                     object:imageRecord];
            [self.operationQueue addOperation:invokeOp];
         }];
    }
}

//Performs one image download.
-(void)downloadImageForRecord:(NSDictionary *)imageRecord
{
    NSCondition downloadCondition = [[NSCondition alloc] init];
    [self.conditions addObject:downloadCondition];

    [[self.urlSession downloadTaskWithURL:imageURL
                        completionHandler:^(NSURL *location,
                                            NSURLResponse *response,
                                            NSError *error)
              {
                  if(error)
                  {
                    //Record error below.
                  }
                  else
                  {
                    //Move the downloaded image to the correct directory.
                    NSError *moveError;
                    [[NSFileManager defaultManager] moveItemAtURL:location toURL:finalURL error:&moveError];

                    //Create a thumbnail version of the image for use in a search grid.
                  }

                  //Record the final outcome for this record by updating the database with either an error code, or the file path to where the image was saved.

                  //Sleep for some time to allow the CPU to rest.
                  [NSThread sleepForTimeInterval:0.05]; // 0.05 second gap between images.

                  //Finally, signal our condition.
                  [downloadCondition signal];
              }]
        resume];

    [downloadCondition lock];
    [downloadCondition wait];
    [downloadCondition unlock];
}

//If the downloads need to be stopped, for whatever reason (i.e. the user logs out), this function is called to stop the process entirely:
-(void)stopDownloading
{
    //Immediately suspend the queue.
    [self.operationQueue setSuspended:YES];

    //If any conditions remain, signal them, then remove them. This was added to avoid deadlock issues with the user logging out and then logging back in in rapid succession.
    [self.conditions enumerateObjectsUsingBlock:^(NSCondition *condition,
                                                  NSUInteger idx,
                                                  BOOL * _Nonnull stop)
     {

         [condition signal];
     }];

    [self setConditions:nil];
    [self setConditions:[NSMutableArray array]];
    [self.urlSession invalidateAndCancel];
    [self setImagesRemaining:0];
    [self.operationQueue cancelAllOperations];
    [self setOperationQueue:nil];
}

编辑 2:来自 Instruments 的 CPU 使用情况屏幕截图。峰值约为 50%,谷值约为 13% CPU 使用率。

enter image description here

编辑 3:运行应用程序直至控制台失败,观察到内存问题

好吧!下载图像一个多小时后,终于观察到我的 iPhone 11 Pro 发生崩溃,这与我其他测试人员报告的情况相符。

控制台报告我的应用程序因使用过多内存而被专门终止。如果我正确阅读此报告,我的应用程序使用了超过 2 GB 的 RAM。我假设这与 NSURLSESSIOND 的内部管理有更多关系,因为它在使用 Xcode 或 Instruments 进行调试期间没有显示此泄漏。

控制台报告:“内核 232912.788 内存状态:killing_specific_process pid 7075 [PharosSales](每个进程限制 10)2148353KB - memorystatus_available_pages:38718”

幸运的是,我在 1 小时左右开始收到内存警告。我应该能够暂停(挂起)我的操作队列一段时间(假设 30 秒),以便让系统清除其内存。

或者,我可以调用 stop,并在调用重新开始后使用 gcd 调度。

你们觉得这个解决方案怎么样?有没有更优雅的方式来响应内存警告?

您认为这些内存使用量来自哪里?

最佳答案

编辑 4: Eureka !!发现Apple API内部内存泄漏

在挖掘了“杀死特定进程”内存相关的控制台消息后,我发现了以下帖子:

Stack Overflow NSData leak discussion

基于围绕使用 NSData writeToFile:error: 的讨论,我环顾四周,看看我是否以某种方式使用了这个函数。

事实证明,我用来从原始图像生成缩略图的逻辑使用此语句将生成的缩略图写入磁盘。

如果我注释掉这个逻辑,应用程序就不再崩溃(能够毫无故障地拉下所有图像!)。

我已经计划将这个旧版 Core Graphics 代码替换为 WWDC 2018 演示的 ImageIO 用法。

重新编码此函数以使用 ImageIO 后,我很高兴地报告应用程序不再崩溃,并且缩略图逻辑也得到了 super 优化!

感谢您的帮助!

关于ios - iPad Pro 第三代无缘无故杀死前台应用程序,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58383911/

相关文章:

ios - 如何检查 View 是否有标签?

multithreading - 我有一个看起来像cpu缓存一致性的问题,我不知道该如何解决。两个cpus看到同一个内存的不同内容

python - 从 Python 查询 CPU ID?

swift - 除了 Xcode,还有其他地方可以看到我的应用程序崩溃吗?

caching - 当存在多级私有(private)缓存和共享内存时,缓存一致性如何工作?

ios - 如何根据 Objective-C 中的标志在动态 TableView 部分添加单元格

iphone - NSUserdefault疑惑

使用中的 iOS Xcode 模拟器

arrays - 无法使用 Swift 在 Xcode 中的类中创建数组

c++ - 结构的 Xcode typedef 创建错误/警告