Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

«Сверхзвуковая» загрузка фотографий в Облако с поддержкой собственного NSInputStream

Anna | 2.07.2014 | нет комментариев

Максимально стремительная загрузка фотографий и видео с устройства на сервер была нашим основным приоритетом при разработке мобильного приложения Облако Mail.Ru для iOS. Помимо того, с самой первой версии приложения мы предоставили пользователям вероятность включить механическую загрузку на сервер каждого содержимого системной галереи. Это дюже комфортно для тех, кто беспокоится о допустимой потере телефона, впрочем, как вы понимаете, увеличивает объем передаваемых данных в разы.

Выходит, мы поставили перед собой задачу сделать загрузку фото и видео из мобильного приложения Облака Mail.Ru не легко отличной, а близкой к безукоризненной. Итогом стала наша библиотека POSInputStreamLibrary, которая реализует потоковую загрузку в сеть фото и видео из системной галереи iOS. Вследствие ее узкой интеграции с фреймворками ALAssetLibrary и CFNetwork загрузка в приложении происходит дюже стремительно и не требует ни байта свободного места на устройстве. О реализации собственного преемника классаNSInputStream из iOS Developer Library я расскажу в этом посте.

За время службы на благо Облака Mail.Ru поток POSBlobInputStream оброс крайне богатой функциональностью:

  • инициализация потока URL-ом ALAsset
  • помощь синхронного и асинхронного режимов работы
  • механическая переинициализация позже инвалидации объекта ALAsset
  • кеширующее чтение данных из ALAsset
  • вероятность указать смещение, с которого будет начато чтение
  • вероятность интеграции с произвольным источником данных

Толк всякой из перечисленных вероятностей разъясняется в отдельном пункте. Перед их рассмотрением осталось только сказать, что начальный код библиотеки доступен тут, а также в основном репозитории CocoaPods.

Инициализация потока URL-ом ALAsset

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

@interface NSInputStream (NSInputStreamExtensions)
// ...
  (id)inputStreamWithFileAtPath:(NSString *)path;
// ...
@end
@interface NSMutableURLRequest (NSMutableHTTPURLRequest)
// ...
- (void)setHTTPBodyStream:(NSInputStream *)inputStream;
// ...
@end

Кликабельно:

Требование поддержать загрузку видеофайлов сделало данный подход непригодным. Большой размер роликов порождал следующие задачи:

  • для загрузки требовалось присутствие большого числа свободного места на устройстве
  • время сохранения видео во непостоянный файл могло добиваться 10 и больше минут

Для преодолевания этих неудобств был разработан класс POSBlobInputStream. Он инициализируется URL-ом объекта галереи и читает данные напрямую без создания временных файлов.

@interface NSInputStream (POS)
  (NSInputStream *)pos_inputStreamWithAssetURL:(NSURL *)assetURL;
  (NSInputStream *)pos_inputStreamWithAssetURL:(NSURL *)assetURL asynchronous:(BOOL)asynchronous;
  (NSInputStream *)pos_inputStreamForCFNetworkWithAssetURL:(NSURL*)assetURL;
@end

Кликабельно:

Вначале у меня было чувство, что реализация POSBlobInputStream займет минимум времени, от того что интерфейс его базового класса банален.

@interface NSInputStream : NSStream
- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len;
- (BOOL)getBuffer:(uint8_t **)buffer length:(NSUInteger *)len;
- (BOOL)hasBytesAvailable;
@end

Больше того, согласно документацииgetBuffer:length: поддерживать необязательно, так что, казалось бы, необходимо реализовать каждого 2 способа. Их отображение на интерфейс ALAssetRepresentation вопросов также не вызывало.

@interface ALAssetRepresentation : NSObject
// ...
- (long long)size;
- (NSUInteger)getBytes:(uint8_t *)buffer fromOffset:(long long)offset length:(NSUInteger)length error:(NSError **)error;
// ...
@end

Впрочем, спустив новоиспеченный POSBlobInputStream на воду, я был досадно поражен. Вызов всякого способа базового класса NSStream завершался исключением вида:

*** -propertyForKey: only defined for abstract class.  Define -[POSBlobInputStream propertyForKey:]

Повод заключается в том, что NSInputStream — это отвлеченный класс, а всякий из его init-способов создает объект одного из классов-преемников. В Objective-C данный паттерн именуется class cluster. Таким образом, реализация собственного потока требует реализации в том числе и всех способов NSStream, а их там полна горница.

@interface NSStream : NSObject
- (void)open;
- (void)close;
- (id <NSStreamDelegate>)delegate;
- (void)setDelegate:(id <NSStreamDelegate>)delegate;
- (id)propertyForKey:(NSString *)key;
- (BOOL)setProperty:(id)property forKey:(NSString *)key;
- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
- (void)removeFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
- (NSStreamStatus)streamStatus;
- (NSError *)streamError;
@end

Синхронный и асинхронный режимы работы POSBlobInputStream

При разработке POSBlobInputStream особенно трудным было реализовать механизм асинхронного уведомления об изменении состояния. В NSStream за него отвечают способы scheduleInRunLoop:forMode:,removeFromRunLoop:forMode: и setDelegate:. Вследствие им дозволено создавать такие потоки, которые на момент открытия не располагают ни байтом информации. POSBlobInputStream эксплуатирует эту вероятность для следующих целей:

  • Реализация неблокирующей версии способа openPOSBlobInputStream считается открытым, как только ему удалось получить объект ALAssetRepresentation по его NSURL. Как вестимо, с поддержкой iOS SDK это дозволено сделать только асинхронно. Таким образом, присутствие механизма для асинхронного уведомления об изменении состояния потока с NSStreamStatusNotOpen на NSStreamStatusOpen либоNSStreamStatusError тут как невозможно кстати.
  • Информирование о наличии у потока данных для чтения посредством отправки событияNSStreamEventHasBytesAvailable.

В иллюстративных целях ниже приводятся реализации подсчета контрольной суммы файла с применением POSBlobInputStream. Начнем с рассмотрения синхронного варианта.

NSInputStream *stream = [NSInputStream pos_inputStreamWithAssetURL:assetURL asynchronous:NO];
[stream open];
if ([stream streamStatus] == NSStreamStatusError) {
    /* Оповещаем об ошибке */
    return;
}
NSParameterAssert([stream streamStatus] == NSStreamStatusOpen);
while ([stream hasBytesAvailable]) {
    uint8_t buffer[kBufferSize];
    const NSInteger readCount = [stream read:buffer maxLength:kBufferSize];
    if (readCount < 0) {
        /* Сообщаем об ошибке */
        return;
    } else if (readCount > 0) {
        /* Логика подсчета контрольной суммы */
    }
}
if ([stream streamStatus] != NSStreamStatusAtEnd) {
    /* Оповещаем об ошибке */
    return;
}
[stream close];

При каждой простоте у этого кода есть одна заметная специфика. Если исполнять его в основном треде, то произойдет deadlock. Дело в том, что способ open блокирует дерзкий тред до тех пор, пока iOS SDK не вернет в основном потоке ALAsset. Если же функция open сама по себе будет вызвана в основном потоке, то получится классическая взаимоблокировка. Для чего вообще потребовалась синхронная реализация потока, будет описано ниже в разделе “Особенности интеграции с NSURLRequest”.
Асинхронная версия подсчета контрольной суммы выглядит немножко труднее.

@interface ChecksumCalculator () <NSStreamDelegate>
@end

@implementation ChecksumCalculator

- (void)calculateChecksumForStream:(NSInputStream *)aStream {
    aStream.delegate = self;
    [aStream open];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
            [aStream scheduleInRunLoop:runLoop forMode:NSDefaultRunLoopMode];
            for (;;) { @autoreleasepool {
                if (![runLoop runMode:NSDefaultRunLoopMode
                                 beforeDate:[NSDate dateWithTimeIntervalSinceNow:kRunLoopInterval]]) {
                    break;
                }
                const NSStreamStatus streamStatus = [aStream streamStatus];
                if (streamStatus == NSStreamStatusError || streamStatus == NSStreamStatusClosed) {
                    break;
                }
            }}
    });
}

#pragma mark - NSStreamDelegate

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
    switch (eventCode) {
        case NSStreamEventHasBytesAvailable: {
            [self updateChecksumForStream:aStream];
        } break;
        case NSStreamEventEndEncountered: {
            [self notifyChecksumCalculationCompleted];
            [_stream close];
        } break;
        case NSStreamEventErrorOccurred: {
            [self notifyErrorOccurred:[_stream streamError]];
            [_stream close];
        } break;
    }
}

@end

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

Особенности интеграции с ALAssetLibrary

При работе с ALAssetLibrary следует рассматривать следующее:

  • Вызовы способов ALAssetRepresentation обходятся дюже дорого. POSBlobInputStream усердствует минимизировать их число за счет кеширования полученных итогов. Скажем, существует наименьший блок данных, тот, что будет вычитан при вызове способа read:maxLength:, и только по его исчерпании произойдет новое обращение.
  • ALAssetRepresentation может становиться недействительным. Так, на iOS 5.x это происходит при сохранении фотографии в галерею телефона. С точки зрения клиентского кода это выглядит как возврат нулевого значения способом getBytes:fromOffset:length:error: объекта ALAssetRepresentation. При этом заведомо вестимо, что данные до конца не прочитаны. В этом случае POSBlobInputStream получаетALAssetRepresentation снова. Нелишним будет подметить, что при работе в синхронном режиме на время переинициализации дерзкий поток блокируется, а в асинхронном — нет.

Особенности интеграции с NSURLRequest

В основе реализации сетевого яруса iOS SDK в целом и NSURLRequest в частности лежит фреймворк CFNetwork. За длинные годы жизни он собрал много шкафов со скелетами. Но обо каждому по порядку.

NSInputStream является одним из “toll-free bridged” классов iOS SDK. Его дозволено привести кCFReadStreamRef и трудиться с ним в последующем как с объектом данного типа. Это качество лежит в основе реализации NSURLRequest. Конечный выдает POSBlobInputStream за своего брата-близнеца, и CFNetwork общается с ним теснее с поддержкой С-интерфейса. В теории все C-вызовы к CFReadStreamобязаны проксироваться на вызовы соответствующих им способов NSInputStream. Впрочем на практике есть два серьезных отклонения:

  1. Не все вызовы проксируются. Для некоторых эту процедуру доводится делать независимо. Останавливаться на этом тут не буду, от того что в интернете есть отличные статьи на эту тему: How to implement a CoreFoundation toll-free bridget NSInputStreamSubclassing NSInputStream.
  2. Проксирование CFReadStreamGetError приводит к падению приложения. Это эксклюзивное познание было получено путем обзора crash-логов приложения и медитаций над исходниками CFStream. Видимо, по этой причине указанная функция помечена в документации устаревшей, но, тем не менее, ее применение еще не искоренено изо всех мест CFNetwork. Так, всякий раз, когда NSInputStream оповещает CFNetwork об ошибке, фреймворк пытается получить ее изложение, применяя эту несчастную функцию. Результат грустен.

Для борьбы со 2-й задачей вариантов не так много. От того что отрефакторить CFNetwork немыслимо, остается только не провоцировать его на недружелюбные действия. Дабы CFNetwork не пытался получить изложение ошибки, необходимо ни при каких условиях не информировать ему о ее возникновении. По этой причине POSBlobInputStream обзавелся свойством shouldNotifyCoreFoundationAboutStatusChange. Если флаг выставлен, то:

  1. поток не будет слать уведомления об изменении своего ранга посредством callback-ов C
  2. способ streamStatus никогда не вернет значение NSStreamStatusError

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

Еще одним неприятным открытием стало то, что CFNetwork работает с потоком в синхронном режиме. Невзирая на то, что фреймворк подписывается на уведомления, он все равно для чего-то занимается его poll-ингом. Скажем, способ open вызывается в цикле несколько раз, и если поток за данный промежуток времени не поспевает перейти в открытое состояние, он сознается испорченным. Эта специфика сетевого фреймворка и была поводом поддержки в POSBlobInputStream синхронного режим работы, пускай и с ограничениями.

Помощь чтения данных со смещением

iOS-приложение Облака Mail.Ru может дозагружать файлы. Данная функциональность разрешает экономить трафик и время пользователя в случае, когда часть загружаемого файла теснее находится в хранилище. Для реализации этого требования POSBlobInputStream был обучен считыванию содержимого фотографии не с начала, а с некоторой позиции. Смещение в нем задается свойством NSStreamFileCurrentOffsetKey. Вследствие тому, что оно же применяется для сдвига начала стандартного файлового потока, возникает вероятность указывать его единообразно.

Помощь произвольных источников данных

POSBlobInputStream был сделан для загрузки фото и видео из галереи. Впрочем спроектирован он таким образом, Дабы в случае необходимости дозволено было применять и другие источники данных. Для стриминга из других источников нужно реализовать протокол POSBlobInputStreamDataSource.

@protocol POSBlobInputStreamDataSource <NSObject>
//
// Self-explanatory KVO-compliant properties.
@property (nonatomic, readonly, getter = isOpenCompleted) BOOL openCompleted;
@property (nonatomic, readonly) BOOL hasBytesAvailable;
@property (nonatomic, readonly, getter = isAtEnd) BOOL atEnd;
@property (nonatomic, readonly) NSError *error;
//
// This selector will be called before anything else.
- (void)open;
//
// Data Source configuring.
- (id)propertyForKey:(NSString *)key;
- (BOOL)setProperty:(id)property forKey:(NSString *)key;
//
// Data Source data.
// The contracts of these selectors are the same as for NSInputStream.
- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)maxLength;
- (BOOL)getBuffer:(uint8_t **)buffer length:(NSUInteger *)bufferLength;
@end

Свойства применяются не только для приобретения состояния источника данных, но и для информирования потока о его изменении с поддержкой механизма KVO.

Результат

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

 

Источник: programmingmaster.ru

 

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB