Способ загрузки файлов iCloud? Очень запутанно?

У меня есть базовая поддержка iCloud в моем приложении (синхронизация изменений, повсеместное распространение и т. Д.), Но до сих пор одним из важных упущений было отсутствие поддержки "загрузки" для файлов, которые существуют (или имеют изменения) в облаке, но не являются таковыми. в синхронизации с тем, что в настоящее время на диске.

Я добавил следующие методы в свое приложение, основываясь на некотором коде, предоставленном Apple, с парой настроек:

Методы загрузки:

- (BOOL)downloadFileIfNotAvailable:(NSURL*)file {
    NSNumber*  isIniCloud = nil;

    if ([file getResourceValue:&isIniCloud forKey:NSURLIsUbiquitousItemKey error:nil]) {
        // If the item is in iCloud, see if it is downloaded.
        if ([isIniCloud boolValue]) {
            NSNumber*  isDownloaded = nil;
            if ([file getResourceValue:&isDownloaded forKey:NSURLUbiquitousItemIsDownloadedKey error:nil]) {
                if ([isDownloaded boolValue])
                    return YES;

                // Download the file.
                NSFileManager*  fm = [NSFileManager defaultManager];
                NSError *downloadError = nil;
                [fm startDownloadingUbiquitousItemAtURL:file error:&downloadError];
                if (downloadError) {
                    NSLog(@"Error occurred starting download: %@", downloadError);
                }
                return NO;
            }
        }
    }

    // Return YES as long as an explicit download was not started.
    return YES;
}

- (void)waitForDownloadThenLoad:(NSURL *)file {
    NSLog(@"Waiting for file to download...");
    id<ApplicationDelegate> appDelegate = [DataLoader applicationDelegate];
    while (true) {
        NSDictionary *fileAttribs = [[NSFileManager defaultManager] attributesOfItemAtPath:[file path] error:nil];
        NSNumber *size = [fileAttribs objectForKey:NSFileSize];

        [NSThread sleepForTimeInterval:0.1];
        NSNumber*  isDownloading = nil;
        if ([file getResourceValue:&isDownloading forKey:NSURLUbiquitousItemIsDownloadingKey error:nil]) {
            NSLog(@"iCloud download is moving: %d, size is %@", [isDownloading boolValue], size);
        }

        NSNumber*  isDownloaded = nil;
        if ([file getResourceValue:&isDownloaded forKey:NSURLUbiquitousItemIsDownloadedKey error:nil]) {
            NSLog(@"iCloud download has finished: %d", [isDownloaded boolValue]);
            if ([isDownloaded boolValue]) {
                [self dispatchLoadToAppDelegate:file];
                return;
            }
        }

        NSNumber *downloadPercentage = nil;
        if ([file getResourceValue:&downloadPercentage forKey:NSURLUbiquitousItemPercentDownloadedKey error:nil]) {
            double percentage = [downloadPercentage doubleValue];
            NSLog(@"Download percentage is %f", percentage);
            [appDelegate updateLoadingStatusString:[NSString stringWithFormat:@"Downloading from iCloud (%2.2f%%)", percentage]];
        }
    }
}

И код, который запускает / проверяет загрузки:

 if ([self downloadFileIfNotAvailable:urlToUse]) {
            // The file is already available. Load.
            [self dispatchLoadToAppDelegate:[urlToUse autorelease]];
        } else {
            // The file is downloading. Wait for it.
            [self performSelector:@selector(waitForDownloadThenLoad:) withObject:[urlToUse autorelease] afterDelay:0];
        }

Насколько я могу судить, приведенный выше код выглядит нормально, но когда я делаю большое количество изменений на устройстве A, сохраните эти изменения, затем откройте устройство B (чтобы запросить загрузку на устройстве B), это то, что я вижу в консоли:

2012-03-18 12:45:55.858 MyApp[12363:707] Waiting for file to download...
2012-03-18 12:45:58.041 MyApp[12363:707] iCloud download is moving: 0, size is 101575
2012-03-18 12:45:58.041 MyApp[12363:707] iCloud download has finished: 0
2012-03-18 12:45:58.041 MyApp[12363:707] Download percentage is 0.000000
2012-03-18 12:45:58.143 MyApp[12363:707] iCloud download is moving: 0, size is 101575
2012-03-18 12:45:58.143 MyApp[12363:707] iCloud download has finished: 0
2012-03-18 12:45:58.144 MyApp[12363:707] Download percentage is 0.000000
2012-03-18 12:45:58.246 MyApp[12363:707] iCloud download is moving: 0, size is 101575
2012-03-18 12:45:58.246 MyApp[12363:707] iCloud download has finished: 0
2012-03-18 12:45:58.246 MyApp[12363:707] Download percentage is 0.000000
2012-03-18 12:45:58.347 MyApp[12363:707] iCloud download is moving: 0, size is 177127
2012-03-18 12:45:58.347 MyApp[12363:707] iCloud download has finished: 0
2012-03-18 12:45:58.347 MyApp[12363:707] Download percentage is 0.000000
2012-03-18 12:45:58.449 MyApp[12363:707] iCloud download is moving: 0, size is 177127
2012-03-18 12:45:58.449 MyApp[12363:707] iCloud download has finished: 0
2012-03-18 12:45:58.450 MyApp[12363:707] Download percentage is 0.000000

Так по какой-то причине:

  1. Загрузка файла начинается без ошибок
  2. Атрибуты файла для статуса загрузки файла всегда возвращают, что он не загружается, не завершил загрузку, и прогресс составляет 0 процентов.
  3. Я застрял в цикле навсегда, хотя размер файла меняется между проверками.

Что я делаю неправильно?

4 ответа

Решение

Спустя годы я все еще испытываю периодические (хотя гораздо реже после некоторых из обходных путей в других ответах) проблемы с загрузкой файлов. Итак, я связался с разработчиками в Apple с просьбой о техническом обзоре / обсуждении, и вот что я нашел.

Периодически проверять статус загрузки того же NSURL, даже если вы воссоздаете его, не является предпочтительным методом проверки статуса. Я не знаю, почему это не так - кажется, что это должно работать, но это не так. Вместо этого, как только вы начнете скачивать файл, вы должны зарегистрировать наблюдателя с NSNotificationCenter для прогресса этой загрузки, поддерживая ссылку на запрос для этого файла. Вот точный пример кода, который был предоставлен мне. Я реализовал это (с некоторыми специфическими настройками приложения) в своем приложении, и, похоже, оно работает намного более адекватно.

- (void)download:(NSURL *)url
{
    dispatch_queue_t q_default;
    q_default = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(q_default, ^{

        NSError *error = nil;
        BOOL success = [[NSFileManager defaultManager] startDownloadingUbiquitousItemAtURL:url error:&error];
        if (!success)
        {
            // failed to download
        }
        else
        {
            NSDictionary *attrs = [url resourceValuesForKeys:@[NSURLUbiquitousItemIsDownloadedKey] error:&error];
            if (attrs != nil)
            {
                if ([[attrs objectForKey:NSURLUbiquitousItemIsDownloadedKey] boolValue])
                {
                    // already downloaded
                }
                else
                {
                    NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
                    [query setPredicate:[NSPredicate predicateWithFormat:@"%K > 0", NSMetadataUbiquitousItemPercentDownloadedKey]];
                    [query setSearchScopes:@[url]]; // scope the search only on this item

                    [query setValueListAttributes:@[NSMetadataUbiquitousItemPercentDownloadedKey, NSMetadataUbiquitousItemIsDownloadedKey]];

                    _fileDownloadMonitorQuery = query;

                    [[NSNotificationCenter defaultCenter] addObserver:self
                                                             selector:@selector(liveUpdate:)
                                                                 name:NSMetadataQueryDidUpdateNotification
                                                               object:query];

                    [self.fileDownloadMonitorQuery startQuery];
                }
            }
        }
    });
}

- (void)liveUpdate:(NSNotification *)notification
{
    NSMetadataQuery *query = [notification object];

    if (query != self.fileDownloadMonitorQuery)
        return; // it's not our query

    if ([self.fileDownloadMonitorQuery resultCount] == 0)
        return; // no items found

    NSMetadataItem *item = [self.fileDownloadMonitorQuery resultAtIndex:0];
    double progress = [[item valueForAttribute:NSMetadataUbiquitousItemPercentDownloadedKey] doubleValue];
    NSLog(@"download progress = %f", progress);

    // report download progress somehow..

    if ([[item valueForAttribute:NSMetadataUbiquitousItemIsDownloadedKey] boolValue])
    {
        // finished downloading, stop the query
        [query stopQuery];
        _fileDownloadMonitorQuery = nil;
    }
}

Это известная проблема. Это было воспроизведено здесь и здесь.

Кажется, добавление задержки помогает смягчить неизвестное состояние гонки, но пока не существует известного обходного пути. С 21 марта:

Во всяком случае, мне было интересно, если вы когда-нибудь проходили мимо вашей основной проблемы в этой статье? То есть со временем синхронизация кажется ухудшенной, а уведомления об импорте перестают поступать или являются неполными. Вы определили, что добавление задержки при ответе на уведомление об импорте имело определенную ценность, но в итоге оказалось ненадежным.

И из ОП связанной статьи:

Проблема, которую я, казалось, была вызвана состоянием гонки. Иногда я получал уведомление о том, что мое постоянное хранилище было обновлено из iCloud, но обновленная информация еще не была бы доступна. Похоже, это происходило примерно в 1/4 времени без задержки и примерно в 1/12 времени с задержкой.

Это не было похоже на ухудшение стабильности... система всегда получала обновления при следующем запуске приложения, и автоматическое разрешение конфликтов решало проблему. И тогда он продолжит нормально функционировать. Но, в конце концов, это приведет к другому обновлению.

...

На некотором уровне, я думаю, нам просто нужно верить, что iCloud в конечном итоге вытолкнет информацию, а наш код разрешения конфликтов (или автоматическое разрешение конфликтов основных данных) исправит любые возникающие проблемы.

Тем не менее, я надеюсь, что Apple исправит ошибку в состоянии гонки. Такого просто не должно быть.

Поэтому, по крайней мере, на момент написания этой статьи, загрузки iCloud с использованием этого API следует рассматривать как ненадежные.

( Дополнительная ссылка)

Недавно столкнулся с такой же ситуацией - как и выше, я видел, что загрузка явно происходит, но NSURLUbiquitousItemIsDownloadingи все остальные ключи всегда возвращаются false, Коллега упомянул, что инженер iCloud рекомендовал создать новый NSURL, чтобы проверить эти NSURLUbiquitousItem ключи, так как метаданные не могут (и, очевидно, не будут) обновляться после его создания. Я создал новый NSURL перед проверкой, и он действительно отражает текущее состояние.

Не стоит сбрасывать со счетов условия гонки, упомянутые @MrGomez, которые являются серьезными проблемами (особенно распространенными в iOS 5 и вызвавшими у нас много головной боли), но я не верю, чтобы объяснить проблему, описанную выше.

РЕДАКТИРОВАТЬ: Для того, чтобы создать новый NSURL я использовал [NSURL fileURLWithPath:originalURL.path], Хотя это было немного грязно, это было первое, что работало надежно. Я только что попробовал [originalURL copy] и снова оказались со старыми метаданными, так что, очевидно, они тоже были скопированы.

В целях безопасности или до тех пор, пока это не будет задокументировано, я планирую предположить, что если NSURL создается до любого getResourceValue:forKey: вызов, устаревшие метаданные будут возвращены.

Я столкнулся с той же проблемой, и я потратил много дней, пытаясь найти обходной путь для этой ошибки...

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

Поэтому для меня решение состоит в том, чтобы открыть документ дважды таким образом:

  • Сначала сделайте слепое открытие;
  • Сделать слепое закрытие;
  • Ожидание загруженного состояния;
  • Затем откройте документ.

Я знаю, что это очень грязный способ сделать работу, но она отлично работает для меня:-)

Другие вопросы по тегам