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