📜 ⬆️ ⬇️

"Supersonic" upload photos to the Cloud using your own NSInputStream



The fastest download of photos and videos from the device to the server was our main priority when developing the Cloud Mail.Ru mobile app for iOS . In addition, since the very first version of the application, we have provided users with the ability to enable automatic uploading of the entire contents of the system gallery to the server. This is very convenient for those who are worried about the possible loss of the phone, however, as you understand, it increases the amount of transmitted data at times.

So, we set ourselves the task of making the download of photos and videos from the Mail.Ru Clouds mobile application not just good, but close to perfect. The result was our library POSInputStreamLibrary , which implements streaming download to the network photos and videos from the system gallery iOS. Thanks to its tight integration with the ALAssetLibrary and CFNetwork frameworks, the load in the application is very fast and does not require a single byte of free space on the device. I will discuss the implementation of the native inheritor of the NSInputStream class from the iOS Developer Library in this post.

During the service for the benefit of Mail.Ru Cloud, the POSBlobInputStream stream has acquired a very rich functionality:
')

The meaning of each of these possibilities is explained in a separate paragraph. Before reviewing them, it remains only to say that the source code of the library is available here , as well as in the main CocoaPods repository .

Initializing the stream URL ALAsset


Until all the functionality of the application was limited to downloading photos, everything was simple. The image from the gallery was saved to a temporary file, on the basis of which the standard file stream was created. The latter was fed to the input of NSURLRequest for streaming into the network.

 @interface NSInputStream (NSInputStreamExtensions) // ... + (id)inputStreamWithFileAtPath:(NSString *)path; // ... @end 

 @interface NSMutableURLRequest (NSMutableHTTPURLRequest) // ... - (void)setHTTPBodyStream:(NSInputStream *)inputStream; // ... @end 

Clickable:



The requirement to support video downloads made this approach unusable. The huge size of the clips caused the following problems:


To overcome these inconveniences, the POSBlobInputStream class was developed. It is initialized by the URL of the gallery object and reads the data directly without creating temporary files.

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

Clickable:



At first, I had a feeling that the implementation of POSBlobInputStream would take the least amount of time, since the interface of its base class is trivial.

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

Moreover, according to the documentation , getBuffer:length: is not necessary to support, so it would seem that only 2 methods need to be implemented. Their mapping to the ALAssetRepresentation interface also did not cause ALAssetRepresentation issues.

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

However, after lowering the new POSBlobInputStream into the water, I was unpleasantly surprised. The call of any method of the NSStream base class ended with the exception of the form:
 *** -propertyForKey: only defined for abstract class. Define -[POSBlobInputStream propertyForKey:] 

The reason is that NSInputStream is an abstract class, and each of its init methods creates an object from one of the NSInputStream classes. In Objective-C, this pattern is called class cluster . Thus, the implementation of its own stream requires the implementation, including all NSStream methods, and there is a room full of them.

 @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 

Synchronous and asynchronous operation modes POSBlobInputStream


When developing POSBlobInputStream most difficult was to implement a mechanism for asynchronous notification of state changes. In NSStream the scheduleInRunLoop:forMode: removeFromRunLoop:forMode: and setDelegate: methods are responsible for it. Thanks to them, you can create threads that at the time of opening do not have a byte of information. POSBlobInputStream exploits this feature for the following purposes:


For illustrative purposes, below are implementations of the file checksum using POSBlobInputStream. We begin by considering the synchronous version.

 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]; 

For all its simplicity, this code has one invisible feature. If you execute it in the main thread, then deadlock will occur. The fact is that the open method blocks the calling thread until the iOS SDK returns ALAsset in the main thread. If the open function itself is called in the main thread, then we get a classic deadlock. Why a synchronous implementation of the flow was needed at all will be described below in the section “Features of Integration with NSURLRequest”.
The asynchronous version of the checksum count looks a bit more complicated.

 @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 sets itself up as a POSBlobInputStream event POSBlobInputStream . As soon as a stream has new data, or, on the contrary, ends, or an error occurs, it sends the corresponding events. Please note that it is possible to specify in which thread to send them. For example, in the code listing below, they will come into a workflow created by GCD.

Features of integration with ALAssetLibrary


When working with ALAssetLibrary, consider the following:



Features integration with NSURLRequest


The implementation of the network layer of the iOS SDK in general and the NSURLRequest in particular is based on the CFNetwork framework. Over the long years of his life, he has amassed quite a few cabinets with skeletons. But first things first.

NSInputStream is one of the " toll-free bridged " classes of the iOS SDK. It can be brought to CFReadStreamRef and work with it in the future as with an object of this type. This property underlies the implementation of NSURLRequest . The latter issues POSBlobInputStream for its twin brother, and CFNetwork communicates with it already using the C-interface. In theory, all C-calls to CFReadStream should be CFReadStream to calls to the corresponding NSInputStream methods. However, in practice there are two serious deviations:

  1. Not all calls are proxied. For some, this procedure has to be done independently. I will not dwell on this here, since there are good articles on this subject on the Internet: How to implement a CoreFoundation toll-free bridget NSInputStream , Subclassing NSInputStream .
  2. Proxy CFReadStreamGetError causes the application to crash. This exclusive knowledge was obtained by analyzing the crash-logs of the application and meditating on the source code of CFStream . Apparently, for this reason, this function is marked outdated in the documentation, but, nevertheless, its use has not yet been eradicated from all places of CFNetwork. So, every time NSInputStream informs CFNetwork about an error, the framework tries to get its description using this unfortunate function. The result is sad.

To combat the second problem, there are not so many options. Since it is impossible to refactor CFNetwork, it remains only not to provoke him into hostile actions. In order for CFNetwork not to try to get a description of the error, you should under no circumstances tell it about its occurrence. For this reason, POSBlobInputStream got the property shouldNotifyCoreFoundationAboutStatusChange . If the flag is set, then:

  1. the thread will not send notifications about changing its status via callbacks C
  2. The streamStatus method streamStatus never return NSStreamStatusError

The only way to find out about the occurrence of an error when the flag is raised is to implement the NSStreamDelegate protocol by some class and set it as a delegate to the stream (see the example of checksum calculation above).

Another unpleasant discovery is that CFNetwork works with a stream in synchronous mode. Despite the fact that the framework subscribes to notifications, it is still for some reason engaged in its poll-ing. For example, the open method is called several times in a loop, and if the stream does not have time to go to the open state during this time interval, it is recognized as spoiled. This feature of the network framework was the reason for the POSBlobInputStream support of synchronous operation, albeit with limitations.

Support for reading data with offset


The Mail.Ru Cloud iOS application can load files. This functionality allows you to save traffic and user time in the case when part of the downloaded file is already in the repository. To implement this requirement, POSBlobInputStream was trained to read the contents of a photo not from the beginning, but from a certain position. The offset in it is set by the NSStreamFileCurrentOffsetKey property. Due to the fact that it is also used to shift the beginning of the standard file stream, it is possible to specify it uniformly.

Support for arbitrary data sources


POSBlobInputStream was created to upload photos and videos from the gallery. However, it is designed so that, if necessary, you can use other data sources. For streaming from other sources, it is necessary to implement the POSBlobInputStreamDataSource protocol.

 @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 

Properties are used not only to obtain the status of the data source, but also to inform the flow of its change using the KVO mechanism.

Total


During the work on the stream, I spent a lot of time on the network in search of any analogues. First of all, I didn’t want to reinvent the wheel, and secondly, things are going much faster if we keep a certain pattern in front of our eyes. Unfortunately, I could not find any good implementations. The scourge of most analogs is the implementation of asynchronous work. At best, as in the HSC countingInputStream , an internal object of one of the standard flows is used for event dispatching, which is incorrect. Often, asynchronous operation is not supported at all, as, for example, in NTVStreamMux :

 #pragma mark Undocumented but necessary NSStream Overrides (fuck you Apple) - (void) _scheduleInCFRunLoop:(NSRunLoop*) inRunLoop forMode:(id)inMode { /* FUCK YOU APPLE */ } - (void) _setCFClientFlags:(CFOptionFlags)inFlags callback:(CFReadStreamClientCallBack)inCallback context:(CFStreamClientContext)inContext { /* NO SERIOUSLY, FUCK YOU */ } 

POSBlobInputStream , in turn, is one of the key components of the Mail.Ru Cloud application. During the service, he was tested in battle by an army of users. Many rakes were collected and leveled, and at the moment the flow is one of the most stable components. Use, write extensions, and, of course, I will be glad to any feedback.

Pavel Osipov,
Cloud Development Team Leader for iOS

Source: https://habr.com/ru/post/216247/


All Articles