iOS开发-gif图片上传

前言

项目中有个类似朋友圈的功能,需要支持gif图片的上传与展示,经过大概一天的摸索,也算有点成果,这里整理一下,希望可以给那些有需要的朋友一丢丢帮助。
另外说明下,我项目中用的是PhotoKit(iOS8.0之后推出的),如果你用的是ALAssetsLibrary,可能有些方法就不是很适用了,但是整体的思路应该是通用的。

一个误区

在开始之前,先说一个可能大家都会有的误区(如果你已经知道请无视)
当我们从浏览器里保存了一个gif图片到系统相册里后,我们在系统相册里看到的其实是一个静态的图片,这是因为iOS的系统相册是不支持gif图片预览的。但是这并不表示这个图片就不是gif图片了,实际上它还是gif图片,只是没法通过系统的“照片”这个APP预览出效果而已。

获取gif图片的核心思想

在明白上面那个“误区”后,我们应该就能知道,要想获得gif图片,核心点在于获取图片的原始数据,而不是任何经过压缩或者其他方式处理的图片文件。
在实际的开发过程中,遇到图片选择这种功能,我们一般都会使用一些第三方的开源库,有些开源库里自带了选择“源图片”的功能,有的则没有。比如我在项目中使用的QBImagePicker就没有选择“源图片”的功能,它返回的是一个PHAsset对象的数组,这里就需要我们利用PHAsset去获取源数据。
这里需要注意一点,我们要获取的是NSData数据,而不是UIImage对象,因为据我测试,如果返回的是UIImage对象,那只能获取到gif图片的第一针
由于项目中选择图片后需要对图片尺寸进行压缩(不然实在是太大了,相机拍照的照片源文件大概有2~3M),然后再上传到服务器,所以之前都是利用下面这个方法直接获得压缩后的图片(很高效,内存占用率比获取源图片后再压缩会少很多,之前为了解决多个图片上传时内存爆涨以至于收到内存警告,特意替换了图片选择这块的底层,改使用PhotoKit)

+ (NSMutableArray *)selectedAlbumPhotosWithAlbum:(NSArray *)assets{
    NSMutableArray *imageArray = [NSMutableArray array];
    CGSize targetSize = CGSizeMake(Max_Image_Width, Max_Image_Height);
    
    PHImageRequestOptions *options = [PHImageRequestOptions new];
    options.resizeMode = PHImageRequestOptionsResizeModeFast;
    options.synchronous = YES;
    
    PHCachingImageManager *imageManager = [[PHCachingImageManager alloc] init];
    for (PHAsset *asset in assets) {
        // Do something with the asset
        [imageManager requestImageForAsset:asset
                                targetSize:targetSize
                               contentMode:PHImageContentModeAspectFill
                                   options:options
                             resultHandler:^(UIImage *result, NSDictionary *info) {
                                 // 得到一张 UIImage,展示到界面上
                                 NSNumber *isDegraded = info[PHImageResultIsDegradedKey];
                                 
                                 if (!isDegraded.boolValue) {
                                     
                                     result = [self fixImageOrientation:result];
                                     
                                     ImageListModel *model = [ImageListModel modelWithUIImage:result];
                                     [imageArray addObject:model];
                                 }
                             }];
    }
    
    return imageArray;
}

但是这个方法返回的是UIImage对象,而且还是经过压缩后的图片,gif图片经过这么一折腾肯定就不是我们想要的那个了。因此当选择的图片不是gif图片时,我们可以用这个方法。于是下一个问题就归结到如何判断一个PHAsset对象是不是gif图片了。

如何判断PHAsset对象是不是gif图片

这里我也找到很多方法,但是有一些方法会存在使用私有API的风险

  • 利用未公开属性filename判断是否包含gif
[asset valueForKey:@"filename"]
  • 利用PHImageFileUTIKey判断UTI类型(iOS系统相册是根据 UTI 来区分资源类型的。UTI字面意思是:Uniform Type Identifiers,统一类型标示符)
    但是PHImageFileUTIKey也是未公开的
[imageManager requestImageForAsset:asset
                                targetSize:targetSize
                               contentMode:PHImageContentModeAspectFill
                                   options:options
                             resultHandler:^(UIImage *result, NSDictionary *info) {
                                 if ([info[@"PHImageFileUTIKey"] isEqualToString:(__bridge NSString *)kUTTypeGIF]) {
                                     //gif
                                 }
                             }];
  • 利用PHAssetResource,但是PHAssetResource是iOS9.0以后才可用
NSArray *resources = [PHAssetResource assetResourcesForAsset:asset];
NSString *orgFilename = ((PHAssetResource*)resources[0]).originalFilename;

出于对私有API的忌惮(怕被拒啊T.T),我没有采用上面的方法,最后我使用了下面这种方法(至少都是用的公开的API):
还是利用UTI,只不过这个方法是公开的API(其中kUTTypeGIF定义在<MobileCoreServices/UTCoreTypes.h>里)

 [imageManager requestImageDataForAsset:asset
                                       options:options
                                 resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) {
                                     
                                     DDLogDebug(@"dataUTI:%@",dataUTI);
                                     
                                     //gif 图片
                                     if ([dataUTI isEqualToString:(__bridge NSString *)kUTTypeGIF]) {
                                         //这里获取gif图片的NSData数据
                                     }
                                     else {
                                         //其他格式的图片
                                     }
                                     
 }];

获取gif图片数据

判断出是gif图片后,我们就可以直接拿到NSData数据了,为了跟其他图片区分出,我创建了一个model来保存
(下面这段代码就是对应上面代码里获取gif图片数据的)

//gif 图片
if ([dataUTI isEqualToString:(__bridge NSString *)kUTTypeGIF]) {
    BOOL downloadFinined = (![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey]);
    if (downloadFinined && imageData) {
        ImageListModel *model = [[ImageListModel alloc] init];
        model.imageData = imageData;
        model.image = [UIImage imageWithData:imageData];
        model.isGif = YES;
        [imageArray addObject:model];
     }
}

兼容非gif图片选择

gif图片我们已经可以正确拿到NSData了,对于非gif图片我们也是要兼容的。我们知道在判断是不是gif图片那个方法里已经得到了图片的NSData数据,因此我们可以利用NSData来获得UIImage对象,然后再压缩到目标尺寸:

UIImage *image = [UIImage imageWithData:imageData];
image = [UIImage imageCompressForSize:image targetSize:targetSize];
ImageListModel *model = [ImageListModel modelWithUIImage:image];
[imageArray addObject:model];

但是这个方法有个致命的问题,就是内存问题!因为用了原图的NSData来实例化了UIImage对象,会造成内存猛增,所以我不推荐这种方法。
我的处理方法是再请求一遍压缩图,于是,完整的处理方法是:

/**
 *  选择相册图片(包括gif图片)
 *
 *  @param assets PHAsset对象数组
 */
+ (NSMutableArray *)selectedAlbumPhotosIncludingGifWithPHAssets:(NSArray*)assets {

    NSMutableArray *imageArray = [NSMutableArray array];
    CGSize targetSize = CGSizeMake(Max_Image_Width, Max_Image_Height);
    
    PHImageRequestOptions *options = [PHImageRequestOptions new];
    options.resizeMode = PHImageRequestOptionsResizeModeFast;
    options.synchronous = YES;
    
    PHCachingImageManager *imageManager = [[PHCachingImageManager alloc] init];
    for (PHAsset *asset in assets) {
        
        [imageManager requestImageDataForAsset:asset
                                       options:options
                                 resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) {
                                     
                                     DDLogDebug(@"dataUTI:%@",dataUTI);
                                     
                                     //gif 图片
                                     if ([dataUTI isEqualToString:(__bridge NSString *)kUTTypeGIF]) {
                                         BOOL downloadFinined = (![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey]);
                                         if (downloadFinined && imageData) {
                                             ImageListModel *model = [[ImageListModel alloc] init];
                                             model.imageData = imageData;
                                             model.image = [UIImage imageWithData:imageData];
                                             model.isGif = YES;
                                             [imageArray addObject:model];
                                         }
                                     }
                                     else {
                                         //其他格式的图片,直接请求压缩后的图片
                                         [imageManager requestImageForAsset:asset
                                                                 targetSize:targetSize
                                                                contentMode:PHImageContentModeAspectFill
                                                                    options:options
                                                              resultHandler:^(UIImage *result, NSDictionary *info) {
                                                                  // 得到一张 UIImage,展示到界面上
                                                                  NSNumber *isDegraded = info[PHImageResultIsDegradedKey];
                                                                  if (!isDegraded.boolValue) {
                                                                      result = [self fixImageOrientation:result];
                                                                      ImageListModel *model = [ImageListModel modelWithUIImage:result];
                                                                      [imageArray addObject:model];
                                                                  }
                                        }]; 
                                     }
                                     
                                 }];
        
    }
    
    return imageArray;
}

可以看到,这个方法有个很明显的缺陷,就是每次都是先请求了原图来判断是不是gif图片,当不是gif图片时再请求了一遍缩略图。目前,我也没想到其他比较好的处理方式 :(

gif图片上传

gif图片上传跟普通的图片上传大体上是一样的,唯一要注意的一点是文件的扩展名和mimeType
下面这段代码是我项目里使用的,因为服务端提供的图片上传接口支持批量上传(一次性上传多个图片),也许你的服务端接口可能会不一样,但是思想应该差不多。
说几点图片上传要注意的事项吧:

  • name是需要跟服务端约定好的
  • 原则上fileName没什么特殊要求,也不需要跟服务端统一,但是fileName的扩展名最好不要弄错,一般服务端都是会保留原格式(就是你这里指定的扩展名),尤其是gif图片,一定不要写成jpg等其他格式
  • gif图片上传只能用NSData对象传参,不能通过UIImage对象。
/**
 *  图片上传(ImageListModel数组)
 *
 *  @param urlString  上传地址
 *  @param parameters 参数
 *  @param images     ImageListModel数组
 */
- (NSURLSessionUploadTask *)upload:(NSString *)urlString
                        parameters:(id)parameters
                            images:(NSArray *)images
                          complete:(void (^)(ResponseData *response))complete {

    NSMutableDictionary *params = [NSMutableDictionary dictionaryWithDictionary:parameters];
    NSString *token = [UserDefaultHelper stringForKey:kUserDefaultUserToken];
    if (token.length) {
        [params safeValue:token forKey:@"token"];
    }
    
    NSMutableURLRequest *request = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST" URLString:[NSString stringWithFormat:@"%@%@",BASE_URL_API,urlString] parameters:params constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
        NSString *name = @"upload[]";

        NSInteger index=0;
        for (ImageListModel *model in images) {
            
            if (model.isGif) { //gif图片
                [formData appendPartWithFileData:model.imageData name:name fileName:[NSString stringWithFormat:@"upload%@.gif",@(index++)] mimeType:@"image/gif"];
            }
            else {
                if (model.imageData) {
                    [formData appendPartWithFileData:model.imageData name:name fileName:[NSString stringWithFormat:@"upload%@.jpg",@(index++)] mimeType:@"image/jpeg"];
                }
                else if (model.image) {
                    [formData appendPartWithFileData:UIImageJPEGRepresentation(model.image, 0.8) name:name fileName:[NSString stringWithFormat:@"upload%@.jpg",@(index++)] mimeType:@"image/jpeg"];
                }
            }
        }
        
    } error:nil];
    
    request.timeoutInterval = kHttpRequestTimeoutInterval;
    
    NSURLSessionUploadTask *uploadTask = [self uploadTaskWithStreamedRequest:request progress:^(NSProgress * _Nonnull uploadProgress) {
        //        DDLogDebug(@"upload progress:%@",uploadProgress);
    } completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
        DDLogDebug(@"url:%@,param:%@,resp:%@",request.URL.absoluteString,params,responseObject);
        if (error) {
            [self handleError:error complete:complete];
        }
        else {
            [self handleSuccess:responseObject complete:complete];
        }
    }];
    
    [uploadTask resume];
    
    return uploadTask;
}

gif图片展示

我相信大家的项目中一定不会少了SDWebImage这个库,有了这个,gif图片展示很简单。我们都知道可以利用下面这个方法来显示一个网络图片(或者它的变种方法):

- (void)sd_setImageWithURL:(NSURL *)url;

其实,我们不需要修改任何参数,还是这个方法,只要url是gif图片,用了这个方法UIImageView就能正常显示gif图片。

保存gif图片到相册里

保存gif图片也是一个原则,必须获取到图片的NSData,不能用UIImage。我用了SDWebImage里的SDWebImageDownloader来下载图片的NSData数据:
注:

  • 9.0以上利用PHPhotoLibrary保存(因为PHAssetCreationRequest是iOS9.0才有的,这里加了一个宏判断,解决低版本SDK编译时报错的问题,__IPHONE_OS_VERSION_MAX_ALLOWED:这个值等于Base SDK,还有一个__IPHONE_OS_VERSION_MIN_REQUIRED:这个值等于Deployment Target,检查支持的最小系统版本)
  • 9.0以下还是用了ALAssetsLibrary
        //gif 图片
        if ([imgUrl.absoluteString hasSuffix:@".gif"]) {

            [[SDWebImageDownloader sharedDownloader] downloadImageWithURL:imgUrl options:SDWebImageDownloaderUseNSURLCache progress:nil completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000
                if ([UIDevice currentDevice].systemVersion.floatValue >= 9.0f) {
                    
                    [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
                        PHAssetResourceCreationOptions *options = [[PHAssetResourceCreationOptions alloc] init];
                        options.shouldMoveFile = YES;
                        [[PHAssetCreationRequest creationRequestForAsset] addResourceWithType:PHAssetResourceTypePhoto data:data options:options];
                    } completionHandler:^(BOOL success, NSError * _Nullable error) {
                        dispatch_sync(dispatch_get_main_queue(), ^{
                            [self image:nil didFinishSavingWithError:error contextInfo:NULL];
                        });
                    }];
                }
                else {
                    ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
                    [library writeImageDataToSavedPhotosAlbum:data metadata:nil completionBlock:^(NSURL *assetURL, NSError *error) {
                        dispatch_sync(dispatch_get_main_queue(), ^{
                            [self image:nil didFinishSavingWithError:error contextInfo:NULL];
                        });
                    }];
                }
#else
                ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
                [library writeImageDataToSavedPhotosAlbum:data metadata:nil completionBlock:^(NSURL *assetURL, NSError *error) {
                    dispatch_sync(dispatch_get_main_queue(), ^{
                        [self image:nil didFinishSavingWithError:error contextInfo:NULL];
                    });
                }];
#endif
            }];
    
            return;
        }

结束语

好了,以上就是我一天来摸索gif所有涉及到的技术点了,希望对你有一丢丢帮助!如果写错了、有问题或者有更好的建议,非常欢迎你能指出来,我会及时改进!
另外,这里没法提供比较完整的demo了,因为中间涉及到服务端接口的配合,见谅!
最后,enjoy yourself!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,698评论 5 463
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,711评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,795评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,699评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,567评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,423评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,822评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,467评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,755评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,789评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,577评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,418评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,837评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,054评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,355评论 1 254
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,820评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,027评论 2 338

推荐阅读更多精彩内容

  • iOS开发系列--网络开发 概览 大部分应用程序都或多或少会牵扯到网络开发,例如说新浪微博、微信等,这些应用本身可...
    lichengjin阅读 3,628评论 2 7
  • 技术无极限,从菜鸟开始,从源码开始。 由于公司目前项目还是用OC写的项目,没有升级swift 所以暂时SDWebI...
    充满活力的早晨阅读 12,606评论 0 2
  • 1、禁止手机睡眠[UIApplication sharedApplication].idleTimerDisabl...
    DingGa阅读 1,109评论 1 6
  • 1、设置UILabel行间距 NSMutableAttributedString* attrString = [[...
    FF_911阅读 1,350评论 0 3
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,108评论 29 470