我有一个已经在野外运行了很多年的应用程序。
这个应用程序,为了100%的功能,离线时,需要下载数十万张图片(每个对象一张),只需一次(根据需要处理增量更新)。
对象数据本身是没有问题的。
然而,最近,我们的应用程序开始崩溃时,只下载图片,但只在较新的iPads (第3代iPad Pros,有大量的存储)。
图像下载过程在NSURLSession内部使用NSOperationQueue下载任务。
我们开始看到能源日志显示CPU使用率过高,因此我们修改了参数以在每个映像之间以及每一批映像之间添加一个中断,使用
NSThread睡眠时间间隔:有时;
这使我们的CPU使用率从95%以上(这很公平)降到了18%以下!
不幸的是,该应用程序在几个小时后仍会在较新的iPads上崩溃。然而,在我们2016年的iPad Pro第一代,应用程序没有崩溃,即使在24小时的下载。
当从设备中提取崩溃日志时,我们看到的是CPU使用率超过50%,超过3分钟。没有其他坠机记录出现。
这些设备都已接通电源,锁定时间设置为“永不”,以便让iPad保持清醒,并将我们的应用程序放在前台。
为了解决这个问题,我们拒绝了我们的性能,基本上在每个图像之间等待30秒,在每一批图像之间等待整整2分钟。这是有效的,并停止了崩溃,然而,这将需要几天来下载我们的所有图像。
我们试图找到一个快乐的媒介,在那里的性能是合理的,并没有崩溃的应用程序。
然而,困扰我的是,无论设置如何,即使是在全面的性能上,应用程序也不会在旧设备上崩溃,它只会在较新的设备上崩溃。
传统观点认为,这是不可能的。
我在这里错过了什么?
当我使用仪器分析的时候,我看到这个应用程序在下载的时候平均只占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: CPU使用截图从仪器。峰值~50%,山谷~13% CPU使用率。

编辑3:运行应用程序直到控制台失败,注意到内存问题
好了!最后,在下载了一个多小时的图片后,在我的iPhone 11 Pro上观察到了崩溃,这与我的其他测试人员报告的场景相匹配。
控制台报告说,我的应用程序是专门因为使用太多内存而被杀死的。如果我是正确地阅读这个报告,我的应用程序使用超过2GB的RAM。我假设这与NSURLSESSIOND的内部管理有更多关系,因为它在使用Xcode或仪器进行调试时没有显示此泄漏。
控制台报告:“内核232912.788内存状态: killing_specific_process pid 7075 PharosSales 2148353KB - memorystatus_available_pages: 38718"
谢天谢地,我开始收到1小时左右的记忆警告。我应该能够暂停(挂起)我的操作队列一段时间(比如说30秒),以便让系统清除它的内存。
或者,我可以呼叫停止,通过一个gcd调度一个接一个电话,以重新开始。
你们觉得这个解决方案怎么样?是否有一种更优雅的方式来响应内存警告?
您认为这种内存使用是从哪里来的?
发布于 2019-10-16 18:15:54
编辑4:尤里卡!!发现内部Apple内存泄漏
在深入了解我“杀死特定进程”内存相关控制台消息之后,我找到了以下帖子:
Stack Overflow NSData leak discussion
基于围绕使用NSData writeToFile:error:的讨论,我环顾四周,看看是否在以某种方式使用此函数。
结果,我用来从原始图像生成缩略图的逻辑使用这个语句将生成的缩略图写入磁盘。
如果我注释掉这个逻辑,这个应用程序就不会再崩溃了(能够在没有失败的情况下拉下所有的图像!)
我已经计划将这段遗留的核心图形代码交换给WWDC 2018-演示了ImageIO的用法。
在重新编码这个函数使用ImageIO后,我很高兴地报告,应用程序不再崩溃,缩略图逻辑也是超级优化的!
谢谢你的帮助!
https://stackoverflow.com/questions/58383911
复制相似问题