我有一个已经使用了很多年的应用程序。
为了使离线状态下100%正常运行,此应用程序仅需要一次下载数十万张图像(每个对象1个)(需要根据需要处理增量更新)。
对象数据本身掉下来没有问题。
但是,最近,我们的应用程序仅下载图像时就开始崩溃,但仅在较新的iPad(具有足够存储空间的第三代iPad Pro)上崩溃。
图像下载过程在NSOperationQueue中使用NSURLSession下载任务。
[我们开始看到Energy Logs指出CPU使用率太高,因此我们修改了参数以使用[]在每个图像之间以及每批图像之间添加一个分隔符>
[[NSThread sleepForTimeInterval:someTime];
这将我们的CPU使用率从95%以上(这很合理)降低到18%以下!
[不幸的是,仅几个小时后,该应用仍会在较新的iPad上崩溃。但是,即使在下载24小时后,在我们的2016 iPad Pro 1st Gen上,该应用程序也完全不会崩溃。
[从设备中获取崩溃日志时,我们所看到的是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使用率。
编辑3:运行应用程序,直到控制台出现故障,观察到内存问题
好吧!终于在下载图像超过一个小时后,观察到我的iPhone 11 Pro崩溃,这与我的其他测试人员报告的情况相符。
控制台报告我的应用程序因使用过多内存而被杀死。如果我正确阅读此报告,则我的应用程序使用了2 GB以上的RAM。我假设这与NSURLSESSIOND的内部管理有更多关系,因为在使用Xcode或Instruments进行调试时,它没有显示此泄漏。
控制台报告:“内核232912.788 memorystatus:killing_specific_process pid 7075 [PharosSales](每个进程限制为10)2148353KB-memorystatus_available_pages:38718”
]幸好,我在1小时左右开始收到内存警告。我应该能够暂停(挂起)我的操作队列一段时间(例如30秒),以便让系统清除其内存。
或者,我可以调用stop,在调用再次启动后进行gcd调度。
你们对此解决方案有何看法?有没有更优雅的方式来响应内存警告?
您认为此内存使用量来自何处?
我有一个应用程序已经问世了很多年。为了在离线时100%正常运行,此应用仅需要一次下载数十万张图像(每个对象1个)...
编辑4:尤里卡!发现内部Apple API内存泄漏