├── QNResumeUpload+ALAssetSupport.h ├── QNResumeUpload+ALAssetSupport.m ├── QNUploadManager+ALAssetSupport.h ├── QNUploadManager+ALAssetSupport.m └── README.md /QNResumeUpload+ALAssetSupport.h: -------------------------------------------------------------------------------- 1 | // 2 | // QNResumeUpload+ALAssetSupport.h 3 | // QiChengNew 4 | // 5 | // Created by 乐星宇 on 14/11/14. 6 | // Copyright (c) 2014年 奇橙百优. All rights reserved. 7 | // 8 | 9 | #import "QNResumeUpload.h" 10 | 11 | @class ALAsset; 12 | 13 | @interface QNResumeUpload (ALAssetSupport) 14 | @property (nonatomic, strong) ALAsset *asset; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /QNResumeUpload+ALAssetSupport.m: -------------------------------------------------------------------------------- 1 | // 2 | // QNResumeUpload+ALAssetSupport.m 3 | // QiChengNew 4 | // 5 | // Created by 乐星宇 on 14/11/14. 6 | // Copyright (c) 2014年 奇橙百优. All rights reserved. 7 | // 8 | 9 | #import "QNResumeUpload+ALAssetSupport.h" 10 | #import "QNHttpManager.h" 11 | #import "QNCrc32.h" 12 | #import "QNConfig.h" 13 | 14 | #import 15 | 16 | #import 17 | #import 18 | 19 | static char kAssetKey; 20 | 21 | @implementation QNResumeUpload (ALAssetSupport) 22 | @dynamic asset; 23 | 24 | - (void)makeBlock:(NSString *)uphost 25 | offset:(UInt32)offset 26 | blockSize:(UInt32)blockSize 27 | chunkSize:(UInt32)chunkSize 28 | progress:(QNInternalProgressBlock)progressBlock 29 | complete:(QNCompleteBlock)complete 30 | { 31 | NSData *data = [self dataFromALAssetAtOffset:offset size:chunkSize];//[self.data subdataWithRange:NSMakeRange(offset, (unsigned int)chunkSize)]; 32 | NSString *url = [[NSString alloc] initWithFormat:@"http://%@/mkblk/%u", uphost, (unsigned int)blockSize]; 33 | 34 | UInt32 crc = [QNCrc32 data:data]; 35 | [self setChunkCrcValue:crc]; 36 | 37 | #pragma clang diagnostic push 38 | #pragma clang diagnostic ignored "-Wundeclared-selector" 39 | SEL selector = @selector(post:withData:withCompleteBlock:withProgressBlock:); 40 | #pragma clang diagnostic pop 41 | void (*typed_msgSend)(id, SEL, NSString *, NSData *, QNCompleteBlock, QNInternalProgressBlock) = (void *)objc_msgSend; 42 | typed_msgSend(self, selector, url, data, complete, progressBlock); 43 | } 44 | 45 | - (void)putChunk:(NSString *)uphost 46 | offset:(UInt32)offset 47 | size:(UInt32)size 48 | context:(NSString *)context 49 | progress:(QNInternalProgressBlock)progressBlock 50 | complete:(QNCompleteBlock)complete { 51 | NSData *data = [self dataFromALAssetAtOffset:offset size:size];//[self.data subdataWithRange:NSMakeRange(offset, (unsigned int)size)]; 52 | UInt32 chunkOffset = offset % kQNBlockSize; 53 | NSString *url = [[NSString alloc] initWithFormat:@"http://%@/bput/%@/%u", uphost, context, (unsigned int)chunkOffset]; 54 | 55 | UInt32 crc = [QNCrc32 data:data]; 56 | [self setChunkCrcValue:crc]; 57 | 58 | #pragma clang diagnostic push 59 | #pragma clang diagnostic ignored "-Wundeclared-selector" 60 | SEL selector = @selector(post:withData:withCompleteBlock:withProgressBlock:); 61 | #pragma clang diagnostic pop 62 | void (*typed_msgSend)(id, SEL, NSString *, NSData *, QNCompleteBlock, QNInternalProgressBlock) = (void *)objc_msgSend; 63 | typed_msgSend(self, selector, url, data, complete, progressBlock); 64 | } 65 | 66 | #pragma mark 读取ALAsset中的data 67 | - (NSData *)dataFromALAssetAtOffset:(NSInteger)offset size:(NSInteger)size 68 | { 69 | ALAssetRepresentation *rep = [self.asset defaultRepresentation]; 70 | Byte *buffer = (Byte *)malloc(size); 71 | NSUInteger buffered = [rep getBytes:buffer fromOffset:offset length:size error:nil]; 72 | 73 | return [NSData dataWithBytesNoCopy:buffer length:buffered freeWhenDone:YES]; 74 | } 75 | 76 | #pragma mark CRC 77 | - (void)setChunkCrcValue:(UInt32)crc 78 | { 79 | [self setValue:@(crc) forKey:@"chunkCrc"]; 80 | } 81 | 82 | #pragma mark Asset setter and getter 83 | - (void)setAsset:(ALAsset *)asset 84 | { 85 | objc_setAssociatedObject(self, &kAssetKey, asset, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 86 | } 87 | 88 | - (ALAsset *)asset 89 | { 90 | return objc_getAssociatedObject(self, &kAssetKey); 91 | } 92 | 93 | 94 | @end 95 | -------------------------------------------------------------------------------- /QNUploadManager+ALAssetSupport.h: -------------------------------------------------------------------------------- 1 | // 2 | // QNUploadManager+ALAssetSupport.h 3 | // QiChengNew 4 | // 5 | // Created by 乐星宇 on 14/11/14. 6 | // Copyright (c) 2014年 奇橙百优. All rights reserved. 7 | // 8 | 9 | #import "QNUploadManager.h" 10 | 11 | @class ALAsset; 12 | 13 | @interface QNUploadManager (ALAssetSupport) 14 | 15 | - (void)putALasset:(ALAsset *)asset 16 | key:(NSString *)key 17 | token:(NSString *)token 18 | complete:(QNUpCompletionHandler)completionHandler 19 | option:(QNUploadOption *)option; 20 | 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /QNUploadManager+ALAssetSupport.m: -------------------------------------------------------------------------------- 1 | // 2 | // QNUploadManager+ALAssetSupport.m 3 | // QiChengNew 4 | // 5 | // Created by 乐星宇 on 14/11/14. 6 | // Copyright (c) 2014年 奇橙百优. All rights reserved. 7 | // 8 | 9 | #import "QNUploadManager+ALAssetSupport.h" 10 | #import "QNAsyncRun.h" 11 | #import 12 | #import "QNResponseInfo.h" 13 | #import "QNResumeUpload.h" 14 | #import "QNResumeUpload+ALAssetSupport.h" 15 | 16 | 17 | @implementation QNUploadManager (ALAssetSupport) 18 | 19 | + (BOOL)checkAndNotifyError:(NSString *)key 20 | token:(NSString *)token 21 | data:(NSData *)data 22 | asset:(ALAsset *)asset 23 | complete:(QNUpCompletionHandler)completionHandler 24 | { 25 | NSString *desc = nil; 26 | if (completionHandler == nil) 27 | { 28 | @throw [NSException exceptionWithName:NSInvalidArgumentException 29 | reason:@"no completionHandler" userInfo:nil]; 30 | return YES; 31 | } 32 | 33 | if (data == nil && asset == nil) 34 | { 35 | desc = @"no input data"; 36 | } 37 | else if (token == nil || [token isEqualToString:@""]) { 38 | desc = @"no token"; 39 | } 40 | 41 | if (desc != nil) { 42 | QNAsyncRun ( ^{ 43 | completionHandler([QNResponseInfo responseInfoWithInvalidArgument:desc], key, nil); 44 | }); 45 | return YES; 46 | } 47 | 48 | return NO; 49 | } 50 | 51 | - (void)putALasset:(ALAsset *)asset 52 | key:(NSString *)key 53 | token:(NSString *)token 54 | complete:(QNUpCompletionHandler)completionHandler 55 | option:(QNUploadOption *)option 56 | { 57 | if ([QNUploadManager checkAndNotifyError:key token:token data:nil asset:asset complete:completionHandler]) 58 | { 59 | return; 60 | } 61 | 62 | @autoreleasepool { 63 | QNUpCompletionHandler complete = ^(QNResponseInfo *info, NSString *key, NSDictionary *resp) 64 | { 65 | completionHandler(info, key, resp); 66 | }; 67 | 68 | //无法获取最后修改时间,用创建时间先凑合一下 69 | NSDate *modifyTime = [asset valueForProperty:ALAssetPropertyDate];//fileAttr[NSFileModificationDate]; 70 | NSString *recorderKey = key; 71 | QNRecorderKeyGenerator recorderKeyGen = [self getRecorderKeyGeneratorProperty]; 72 | id recorder = [self getRecorderProperty]; 73 | if (recorder != nil && recorderKeyGen != nil) 74 | { 75 | //TODO:断点续传是基于文件来做的,因此这里保存了文件的路径,这里改成asset的URL,意味着恢复时也需要做对应的调整 76 | recorderKey = recorderKeyGen(key, [asset.defaultRepresentation.url absoluteString]); 77 | } 78 | 79 | QNResumeUpload *up = [[QNResumeUpload alloc] 80 | initWithData:nil 81 | withSize:(UInt32)asset.defaultRepresentation.size 82 | withKey:key 83 | withToken:token 84 | withCompletionHandler:complete 85 | withOption:option 86 | withModifyTime:modifyTime 87 | withRecorder:recorder 88 | withRecorderKey:recorderKey 89 | withHttpManager:[self getHttpManagerProperty]]; 90 | up.asset = asset; 91 | 92 | QNAsyncRun ( ^{ 93 | [up run]; 94 | }); 95 | } 96 | } 97 | 98 | #pragma mark Private property getter 99 | - (id)getRecorderProperty 100 | { 101 | return [self valueForKey:@"recorder"]; 102 | } 103 | 104 | - (QNRecorderKeyGenerator)getRecorderKeyGeneratorProperty 105 | { 106 | return [self valueForKey:@"recorderKeyGen"]; 107 | } 108 | 109 | - (QNHttpManager *)getHttpManagerProperty 110 | { 111 | return [self valueForKey:@"httpManager"]; 112 | } 113 | 114 | 115 | @end 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | >Update: 七牛的SDK已经添加了对 ALAsset 的支持,不再需要使用此库。换了公司后暂时没有用到七牛,我也懒得更新了。。 2 | 3 | ## 问题描述 4 | 七牛 iOS SDK 的上传 API 只有两个 5 | ```objc 6 | @interface QNUploadManager : NSObject 7 | 8 | - (void)putData:(NSData *)data 9 | key:(NSString *)key 10 | token:(NSString *)token 11 | complete:(QNUpCompletionHandler)completionHandler 12 | option:(QNUploadOption *)option; 13 | 14 | - (void)putFile:(NSString *)filePath 15 | key:(NSString *)key 16 | token:(NSString *)token 17 | complete:(QNUpCompletionHandler)completionHandler 18 | option:(QNUploadOption *)option; 19 | 20 | @end 21 | ``` 22 | 其中 putFileXXX 是针对文件上传的,这个方法内部是依赖 NSFileManager 来获取文件信息的 23 | ```objc 24 | NSError *error = nil; 25 | NSDictionary *fileAttr = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error]; 26 | 27 | NSNumber *fileSizeNumber = fileAttr[NSFileSize]; 28 | UInt32 fileSize = [fileSizeNumber intValue]; 29 | NSData *data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&error]; 30 | ``` 31 | 那么问题来了,对于 ALAsset,即系统相册中的图片或视频,获取到的 assetURL 是类似于如下形式的: 32 | ```sh 33 | assets-library://asset/asset.MOV?id=A16D4A3B-664E-4A75-90E8-37EA3F04FF2E&ext=MOV 34 | ``` 35 | NSFileManager 无法处理,因而无法正确获取文件大小等信息,更不用说上传了。 36 | 37 | ## 解决方案 38 | 为便于说明,假定有 ALAsset 实例 asset。 39 | 首先,通过 asset.defaultRepresentation.size 能够获取到对应文件的大小。为 QNUploadManager 创建一个 category,如下 40 | ```objc 41 | @interface QNUploadManager (ALAssetSupport) 42 | 43 | - (void)putALasset:(ALAsset *)asset 44 | key:(NSString *)key 45 | token:(NSString *)token 46 | complete:(QNUpCompletionHandler)completionHandler 47 | option:(QNUploadOption *)option; 48 | 49 | 50 | @end 51 | ``` 52 | 具体实现如下 53 | ```objc 54 | - (void)putALasset:(ALAsset *)asset 55 | key:(NSString *)key 56 | token:(NSString *)token 57 | complete:(QNUpCompletionHandler)completionHandler 58 | option:(QNUploadOption *)option 59 | { 60 | //other code... 61 | 62 | 63 | QNResumeUpload *up = [[QNResumeUpload alloc] 64 | initWithData:nil 65 | withSize:(UInt32)asset.defaultRepresentation.size 66 | withKey:key 67 | withToken:token 68 | withCompletionHandler:complete 69 | withOption:option 70 | withModifyTime:modifyTime 71 | withRecorder:recorder 72 | withRecorderKey:recorderKey 73 | withHttpManager:[self getHttpManagerProperty]]; 74 | up.asset = asset; 75 | } 76 | } 77 | ``` 78 | 79 | 从上面的代码可以看到,QNUploadManager 实际上只是获取文件信息,做一些预处理,而真正的上传过程是由 QNResumeUpload 完成的。QNResumeUpload 的初始化入参很多,需要注意的是 data 和 size,一个简化版的 initXXX 如下 80 | ```objc 81 | - (instancetype)initWithData:(NSData *)data 82 | withSize:(UInt32)size 83 | withOtherParameters:(XXX *)XXX; 84 | ``` 85 | 其中data是文件句柄打开后的二进制数据,size是 数据长度。 86 | 你一定已经发现我在 putALassetXXX 里很弱智地在 data 这个入参上传入了 nil,这是有原因的。 87 | 打开 QNResumeUpload.m,搜索data后发现,data 本身只在 2 个获取分块数据的方法里涉及到 88 | ```objc 89 | - (void)makeBlock:(NSString *)uphost 90 | offset:(UInt32)offset 91 | blockSize:(UInt32)blockSize 92 | chunkSize:(UInt32)chunkSize 93 | progress:(QNInternalProgressBlock)progressBlock 94 | complete:(QNCompleteBlock)complete; 95 | 96 | - (void)putChunk:(NSString *)uphost 97 | offset:(UInt32)offset 98 | size:(UInt32)size 99 | context:(NSString *)context 100 | progress:(QNInternalProgressBlock)progressBlock 101 | complete:(QNCompleteBlock)complete; 102 | } 103 | ``` 104 | 只要 override 它们,让它们支持 ALAsset 即可。 105 | 首先为 QNResumeUpload 添加 property 106 | ```objc 107 | @class ALAsset; 108 | 109 | @interface QNResumeUpload (ALAssetSupport) 110 | @property (nonatomic, strong) ALAsset *asset; 111 | 112 | @end 113 | ``` 114 | 在 putALasset 方法里会为此属性赋值。 115 | 接着写一个获取指定 offset 和 length 的 data 的方法 116 | ```objc 117 | - (NSData *)dataFromALAssetAtOffset:(NSInteger)offset size:(NSInteger)size 118 | { 119 | ALAssetRepresentation *rep = [self.asset defaultRepresentation]; 120 | Byte *buffer = (Byte *)malloc(size); 121 | NSUInteger buffered = [rep getBytes:buffer fromOffset:offset length:size error:nil]; 122 | 123 | return [NSData dataWithBytesNoCopy:buffer length:buffered freeWhenDone:YES]; 124 | } 125 | ``` 126 | 最后在 makeBlock 和 putChunk 里调用此方法即可,以 makeBlock 为例 127 | ```objc 128 | - (void)makeBlock:(NSString *)uphost 129 | offset:(UInt32)offset 130 | blockSize:(UInt32)blockSize 131 | chunkSize:(UInt32)chunkSize 132 | progress:(QNInternalProgressBlock)progressBlock 133 | complete:(QNCompleteBlock)complete 134 | { 135 | NSData *data = [self dataFromALAssetAtOffset:offset size:chunkSize];//[self.data subdataWithRange:NSMakeRange(offset, (unsigned int)chunkSize)]; 136 | NSString *url = [[NSString alloc] initWithFormat:@"http://%@/mkblk/%u", uphost, (unsigned int)blockSize]; 137 | 138 | UInt32 crc = [QNCrc32 data:data]; 139 | [self setChunkCrcValue:crc]; 140 | 141 | #pragma clang diagnostic push 142 | #pragma clang diagnostic ignored "-Wundeclared-selector" 143 | SEL selector = @selector(post:withData:withCompleteBlock:withProgressBlock:); 144 | #pragma clang diagnostic pop 145 | void (*typed_msgSend)(id, SEL, NSString *, NSData *, QNCompleteBlock, QNInternalProgressBlock) = (void *)objc_msgSend; 146 | typed_msgSend(self, selector, url, data, complete, progressBlock); 147 | } 148 | ``` 149 | --------------------------------------------------------------------------------