Приложение iPad Pro 3rd Gen Killing Foreground без причины - PullRequest
0 голосов
/ 14 октября 2019

У меня есть приложение, которое не использовалось в течение многих лет.

Для того, чтобы на 100% функционировать в автономном режиме, необходимо загрузить сотни тысяч изображений (по 1 на каждый объект). ) только один раз (дельта-обновления обрабатываются по мере необходимости).

Сами данные объекта сбрасываются без проблем.

Однако в последнее время наше приложение начало сбой при загрузке только изображений, нотолько на новых iPad (iPad Pro 3-го поколения с большим объемом памяти).

В процессе загрузки образа используются задачи загрузки NSURLSession внутри NSOperationQueue.

Мы начинали видеть журналы Energy, в которых указывается загрузка ЦПбыло слишком высоким, поэтому мы изменили наши параметры, чтобы добавить разрыв между каждым изображением, а также между каждым пакетом изображения, используя

[NSThread sleepForTimeInterval: someTime];

Это снизило загрузку нашего процессора с 95% (что вполне справедливо) до 18%!

К сожалению, приложение все равно зависало бы на новых iPadвсего через пару часов. Однако на нашем iPad Pro 1st Gen 2016 года приложение не падает, даже после 24 часов загрузки.

При извлечении журналов сбоев с устройств все, что мы видим, это то, что загрузка ЦП превышала 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: скриншот загрузки процессора от Instruments. Пики составляют ~ 50%, значения долей составляют ~ 13% загрузки процессора.

enter image description here

РЕДАКТИРОВАТЬ 3: запуск приложения до сбоя вКонсоль, наблюдаемая проблема с памятью

Хорошо! Наконец, в течение часа после загрузки изображений наблюдался сбой на моем iPhone 11 Pro, что соответствует сценарию, описанному другими моими тестировщиками.

Консоль сообщает, что мое приложение было убито специально из-за использования слишком большого объема памяти. Если я правильно читаю этот отчет, мои приложения использовали более 2 ГБ оперативной памяти. Я предполагаю, что это больше связано с внутренним управлением NSURLSESSIOND, так как он не показывает эту утечку во время отладки с помощью Xcode или Instruments.

Отчеты консоли: "kernel 232912.788 memorystatus: killing_specific_processpid 7075 [PharosSales] (ограничение на процесс 10) 2148353KB - memorystatus_available_pages: 38718 "

К счастью, я начинаю получать предупреждения о памяти около 1 часа. Я должен быть в состоянии приостановить (приостановить) свою очередь на некоторое время (скажем, на 30 секунд), чтобы система могла очистить свою память.

В качестве альтернативы, я мог бы вызвать stop, отправив gcd после вызова, чтобы начать снова.

Что вы, ребята, думаете об этом решении? Есть ли более элегантный способ реагировать на предупреждения памяти?

Как вы думаете, откуда происходит это использование памяти?

1 Ответ

0 голосов
/ 16 октября 2019

РЕДАКТИРОВАТЬ 4: Эврика! Обнаружена внутренняя утечка памяти в Apple API

После того, как я обнаружил консольное сообщение «Убить определенный процесс», я обнаружил следующее сообщение:

Обсуждение утечки стека NSData

Основываясь на этом обсуждении, связанном с использованием NSData writeToFile: error:, я оглянулся вокруг, чтобы посмотреть, не использую ли я эту функцию.

Оказывается, логика, которую я использовал для создания эскиза изисходное изображение использовало этот оператор, чтобы записать сгенерированное миниатюрное изображение на диск.

Если я закомментировал эту логику, приложение больше не зависало (смогло сбрасывать все изображения без сбоев!).

Я уже планировал выгрузить этот устаревший код Core Graphics для использования ImageIO, продемонстрированного на WWDC 2018 г.

После перекодирования этой функции для использования ImageIO я рад сообщить, что приложениебольше не вылетает, а логика миниатюр также супер-оптимизирована!

Спасибо за вашу помощь!

...