iOS开发-gif图片上传

作者: 是的蛮大人 | 来源:发表于2016-08-17 11:02 被阅读5626次

前言

项目中有个类似朋友圈的功能,需要支持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!

相关文章

网友评论

  • 帅气的烤鸡翅:直接打印PHAsset类的打印信息里面的mediatype,不同类型显示不一样,PNG是1/4,JPG是1/0,GIF是1/32。但是使用asset.mediatype获取得到的的确实是枚举类型,这好奇怪呀,不然mediatype也可以用来判断类型了
    帅气的烤鸡翅:找到了,后面的是mediaSubtypes,不过5-15的位掩码被隐藏了,苹果没开放出来
  • c2fffd2b0090:result = [self fixImageOrientation:result]; 这个方法是什么啊?
  • 酱油之神:你确定这个方法可以展示网络GIF?
    - (void)sd_setImageWithURL:(NSURL *)url;

    http://img.xintusee.com/Uploads/2017-11-09/5a044664cf220.GIF
  • runsuc:获取到的gif图片的imageData可以在模拟器上显示,在真机上不行。我用的第三方显示gif图片,工程中的gif都可以显示。用这个imageData在真机上显示不了
  • 薛定谔的黑猫警长:我用了你的方法,怎么保存不了GIF格式的图片呢 ?
  • oo上海:感谢分享,虽然是OC。想问一个问题,比如我想只显示 PhotoLibrary 里面的gif,因为默认的参数只能filter 视频和图像,请问您会考虑怎么做呢?

    我的想法比较😓 → 利用PHAsset来比较判断,生成gif asset Array,再grid显示。
    oo上海:@是的蛮大人 其实文章对我帮助已经很大了。。感谢
    是的蛮大人:@oo上海 这个我到没研究过,不过系统本身应该没提供相关的api,要实现的话应该只能取出所有PHAsset,然后逐个判断了吧:joy::joy: 没帮上你,抱歉,哈哈哈
  • 一个正直的小龙猫:楼主有试过 如果多张图片在一个页面里 用sdwebImage 会不会内存暴涨?
    一个正直的小龙猫:@是的蛮大人 我测试过 图片多内存会暴涨 这个属于sdwebimage本身的内存问题
    是的蛮大人:一般来说不会出现,但是也不排除图片较多而且单张图片较大的时候会出现,可以考虑让服务端返回较小一点的图片,这样显示速度也会快一些。
  • 卟师:写的挺好,我转载分享了哈,我会标注上出处和作者
    是的蛮大人:@卟师 谢谢!
  • 云逸枫林:正好是我想要的, 多谢啦
  • 判若两人丶:你的真实身份是下一代的魁拔!
    是的蛮大人:@判若两人丶 被你发现了,让我如何是好 :grin: :grin:
  • Detailscool:最后的保存建议用 宏 判断 不然8.0环境编译直接报错
    是的蛮大人:@Detailscool 已经修改了,增加了宏判断
    是的蛮大人:@Detailscool 嗯,我刚检查了一下,原来PHAssetCreationRequest是在iOS9里才有的,这也就解释了为何iOS8下这个方法会保存失败了,谢谢提醒,稍后我修改下。
  • c4ibD3:mark

本文标题:iOS开发-gif图片上传

本文链接:https://www.haomeiwen.com/subject/dsvpsttx.html