简介
- 用苹果iPhone13手机拍照,传到公司后台,供网站和APP浏览。这个照片清晰度比较高,对于电商推广助力很大,是公司产品的亮点之一。
- 照片的规格是4032*3024,图片质量高,体积也大,给拍照和上传也带来了一些麻烦。
- 2022年4月开始第一个版本,那个时候只有Object-C熟练,现在就一路沿用下来。如果时间充裕的话,重来一次,可以考虑用Flutter写,定制拍照部分可以用Swift插件实现。
第1版:后台转发
- 为了赶进度,采用后台转发的方式上传照片,就像一个普通的后台接口那样;
- 一般情况,图片接口采用表单数据的方式,只是我们没有采用;
- 将UIImage转NSData,然后Base64转字符串,就像是一个普通的字符串参数的后端接口;
- 对于客户端来说,这是大大降低了开发难度,速度也快了很多。
第2版:阿里OSS上传
- 运行了一段时间,后端服务器不堪重负,需要改成OSS上传的方式
- 照片数据最终是存储在阿里云上的,后端转一手,本来也没什么,但是现在后端资源不够,所以需要去掉这个中间商。
- 一个订单可以有6张照片,没张照片又有原图和水印图两份,每张图片在PNG格式下有20M左右,我们采用的是webp格式,每张图片的大小1M左右。
- 阿里云提供了AliyunOSSiOS Pod库,是Object-C写的,引入进来之后,上传速度比较快,后端服务器压力也随之减小。
第3版:时间优化
- 对于Object-C来说,PNG和JPEG格式非常方便,有现成的API可以用。但是webp就很少了,幸亏有YYImage可以用,但是耗时长,内存占用大。
/**
* WebP图片压缩
*
* @param image 需要压缩的图片
* @param compressionQuality 压缩系数;0到1之间
*
* 备注:WebP的压缩比较耗时,需要放入工作者线程中
*
*/
+ (nullable NSData *)webpDataWithImage:(UIImage *)image compressionQuality:(CGFloat)compressionQuality {
// 参数检查
if (image == nil) {
return nil;
}
CGFloat quality = compressionQuality;
if (quality < 0) {
quality = 0;
}
if (quality > 1) {
quality = 1;
}
/// webp压缩耗时耗内存,可能会导致崩溃,这里放入一个try结构
@try {
NSData *webpData = [YYImageEncoder encodeImage:image type:YYImageTypeWebP quality:quality];
NSLog(@"webpData; quality:%0.2f; length:%ld", quality, webpData.length);
return webpData;
} @catch (NSException *exception) {
NSLog(@"webp压缩出错:%@", exception.description);
return nil;
}
}
- 测试环境下,按照拍6张照片的情况,试了一下,webp压缩大约要3秒,阿里云OSS上传只要2秒,合计大约5秒一次,感觉速度还不错。
- 实际工厂反馈说速度有点慢,估计是仓库实际使用环境的网络比较慢,总时间比测试环境要长。
- 采取的方式把压缩和上传的任务放后台,操作员拍完照只要提交就好了,压缩和上传任务在后台完成。
- 提供一个测试记录页面随时查看上传情况:分为“成功,失败,上传中三种状态。
第4版:内存优化
- 后来引入定制拍照,数量没有6个限制,导致时间更长,内存占用更大
- 仓库实际的一个例子,要拍10张照片,引起程序闪退,分析原因是因为内存占用过大。
- 测试环境下,开启XCode的内存监控,发现确实内存有超过2G的情况,确实有可能导致崩溃。
- 由于内存占用最大的就是webp的压缩过程,所以采取的措施是把拍摄之后得到的UIImage转为webp之后,存储在本地的Document目录。上传的时候,再从Document目录读取对应的文件。这样的话,UIImage和NSData这些大数量都不需要保存在模型中了。
/**
* 保存图片数据到本地;采用webp压缩
* webp压缩过程耗时,调用时需要放到后台进程
*
* @param image 图片数据
*
* @return 文件路径
*/
+ (nullable NSString *)saveImageToLocal:(UIImage *)image withFileName:(NSString *)fileName {
NSDate *startDateItem = [NSDate date];
/// 获取Documents目录路径
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
/// 创建完整的文件路径
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:fileName];
filePath = [filePath stringByAppendingPathExtension:@"webp"];
/// webp格式,0.8是压缩质量(0.0-1.0)
NSData *imageData = [self webpDataWithImage:image compressionQuality:0.8];
/// 写入文件
BOOL success = [imageData writeToFile:filePath atomically:YES];
if (success) {
NSDate *endDateItem = [NSDate date];
NSTimeInterval timeInterval = [endDateItem timeIntervalSinceDate:startDateItem];
NSLog(@"图片保存成功,路径:%@,耗时: %0.2f 秒", filePath, timeInterval);
return filePath;
} else {
NSLog(@"图片保存失败");
return nil;
}
}
/**
* 从本地读取图片数据
*
* @param filePath 文件路径
*
* @return 文件数据
*/
+ (nullable NSData *)readDataFromLocal:(NSString *)filePath {
/// 检查文件是否存在
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:filePath]) {
NSLog(@"文件不存在: %@", filePath);
return nil;
}
/// 读取文件数据
NSData *fileData = [NSData dataWithContentsOfFile:filePath];
if (fileData) {
NSLog(@"文件读取成功,大小: %lu 字节", (unsigned long)fileData.length);
} else {
NSLog(@"文件读取失败");
}
return fileData;
}
/**
* 从本地读取图片对象
*
* @param filePath 文件路径
*
* @return 图片对象
*/
+ (nullable UIImage *)readImageFromLocal:(NSString *)filePath {
NSData *imageData = [self readDataFromLocal:filePath];
if (imageData) {
UIImage *image = [UIImage imageWithData:imageData];
return image;
} else {
return nil;
}
}
- 普通拍照最多6张,定制拍照虽然总数很多,但是可以一张一张拍摄,这样就把内存占用限制在可控范围之内。
- 调试的时候发现,YYWebImage或者SDWebImage这些第三方的图片展示库,随着照片数量的增加,内存占用也很多,需要加以限制。我们用的是YYWebImage,加了如下设置之后,内存占用明显减少:
// 配置YYWebImage的缓存参数
+ (void)configYYWebImageCache {
// 获取YYWebImage的默认缓存实例
YYImageCache *imageCache = [YYWebImageManager sharedManager].cache;
// 1. 设置磁盘缓存最大容量(例如:500MB)
// 1MB = 1024 * 1024 字节
imageCache.diskCache.costLimit = 500 * 1024 * 1024;
// 3. 可选:设置内存缓存最大容量(例如:50MB)
// 内存缓存满时会按LRU策略清理
imageCache.memoryCache.costLimit = 50 * 1024 * 1024;
// 4. 可选:设置内存缓存的自动清理时机
imageCache.memoryCache.shouldRemoveAllObjectsOnMemoryWarning = YES; // 内存警告时清空
imageCache.memoryCache.shouldRemoveAllObjectsWhenEnteringBackground = YES; // 进入后台时清空
}