解决 IOS HEIC原图上传 OSS 后无法通过链接访问显示的问题
摘要
在 iOS 平台上,用户选择原图上传至阿里云 OSS 后,其他平台(如 Android、H5)无法通过链接访问和显示该图片。主要原因是 iOS 原图文件较大,且可能包含非标准格式的元数据,导致其他平台无法正确解析HEIC格式图片。本文将提供一套完整的解决方案,涵盖以下关键步骤:
- iOS 端上传前压缩图片:确保图片大小在 20M 以内,最好是 1M 或 2M 以内。
- 特殊后缀标记:在文件名中加入
_IOS
后缀,便于服务端识别和处理。 - 服务端图片处理接口:将标记有特殊后缀的图片进行格式转换(如 JPG、PNG),并持久化到 OSS。
- 异步处理与生产者-消费者模型:通过分布式队列加速图片处理任务。
- 限速机制:使用 Redisson 的
RRateLimiter
控制请求速率,避免超过 OSS 的 50 QPS 限制。
通过以上方案,您可以有效解决 iOS 原图上传 OSS 后无法通过链接访问显示的问题。
技术亮点
- 利用OSS图片处理持久化API
- 服务端无需下载再上传oss
- 分布式处理,异步无阻塞可重试
解决方案
1. iOS 端上传前压缩图片
iOS 端在上传图片前,需要对图片进行压缩,确保图片大小在 20M 以内,最好是 1M 或 2M 以内。可以使用以下代码进行图片压缩:
func compressImage(image: UIImage, maxFileSize: Int) -> Data? {
var compression: CGFloat = 1.0
let maxCompression: CGFloat = 0.1
let maxSize = maxFileSize * 1024 * 1024 // 转换为字节
var imageData = image.jpegData(compressionQuality: compression)
while (imageData?.count ?? 0) > maxSize && compression > maxCompression {
compression -= 0.1
imageData = image.jpegData(compressionQuality: compression)
}
return imageData
}
2. 特殊后缀标记
iOS 端在上传原图时,在文件名中加入特殊后缀 _IOS
,以便服务端识别并进行处理。
let originalFileName = "example.jpg"
let markedFileName = originalFileName.replacingOccurrences(of: ".", with: "_IOS.")
3. 服务端图片处理接口
服务端需要开发一个图片处理接口,用于将标记有特殊后缀的 iOS 原图进行格式转换(如转换为 JPG 或 PNG),并持久化到 OSS。转换后去掉 _IOS
后缀。
3.1 图片处理命令拼接
使用以下方法拼接 OSS 图片处理的命令参数:
/**
* 拼接处理图片格式转换命令参数
*
* @param bucketName OSS Bucket 名称
* @param styleType 图片处理命令(如 "image/format,jpg")
* @param targetKey 处理后持久化的目标文件 Key
* @return 拼接后的图片处理命令
*/
private static StringBuilder processStyle(String bucketName, String styleType, String targetKey) {
StringBuilder sbStyle = new StringBuilder();
Formatter styleFormatter = new Formatter(sbStyle);
styleFormatter.format("%s|sys/saveas,o_%s,b_%s", styleType,
BinaryUtil.toBase64String(targetKey.getBytes()),
BinaryUtil.toBase64String(bucketName.getBytes()));
log.info(sbStyle.toString());
return sbStyle;
}
3.2 图片格式转换与持久化
使用以下方法实现图片格式转换与持久化:
/**
* 图片格式转换
* 1. 先根据目标格式转换,成功后结束
* 2. 异常后尝试向 PNG 转换,失败后不再重试
*
* @param bucketName 原文件所在 Bucket
* @param key 原文件 Key
* @param targetKey 目标文件 Key
* @param format 目标格式(如 "jpg", "png")
*/
private void processImageFormat(String bucketName, String key, String targetKey, String format) {
try {
String styleType = String.format("image/format,%s", format);
StringBuilder sbStyle = processStyle(bucketName, styleType, targetKey);
ProcessObjectRequest request = new ProcessObjectRequest(bucketName, key, sbStyle.toString());
GenericResult processResult = ossClient.processObject(request);
String json = IOUtils.readStreamAsString(processResult.getResponse().getContent(), "UTF-8");
processResult.getResponse().getContent().close();
log.info("处理图片 {} 转换结果 {}", key, json);
} catch (Exception e) {
log.warn("图片格式转换失败 key {} format {}", key, format, e);
// PNG 格式化兜底,如果 PNG 也失败了就不再重试
if (!StrUtil.equalsIgnoreCase("png", format)) {
processImageFormat(bucketName, key, targetKey, "png");
}
}
}
4. 异步处理与生产者-消费者模型
为了加速图片处理,可以采用生产者-消费者模型,将图片处理任务放入队列中,由多个消费者并行处理。
4.1 使用 Redisson 实现分布式队列
import org.redisson.Redisson;
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class ImageProcessingQueue {
private RedissonClient redissonClient;
private RBlockingQueue<String> queue;
public ImageProcessingQueue() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
redissonClient = Redisson.create(config);
queue = redissonClient.getBlockingQueue("imageProcessingQueue");
}
public void addTask(String imageUrl) {
queue.add(imageUrl);
}
public String takeTask() throws InterruptedException {
return queue.take();
}
public void shutdown() {
redissonClient.shutdown();
}
}
4.2 消费者处理任务
public class ImageProcessingConsumer implements Runnable {
private ImageProcessingQueue queue;
public ImageProcessingConsumer(ImageProcessingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
try {
String imageUrl = queue.takeTask();
// 处理图片
processImage(imageUrl);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
private void processImage(String imageUrl) {
// 调用图片处理接口
// ...
}
}
5. 使用 RRateLimiter
进行限速
为了避免对 OSS 图片处理接口的请求过载,可以使用 Redisson 的 RRateLimiter
来实现限速功能。
5.1 初始化 RRateLimiter
import org.redisson.api.RRateLimiter;
import org.redisson.api.RateIntervalUnit;
import org.redisson.api.RateType;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class RateLimiterService {
@Autowired
private RedissonClient redissonClient;
private RRateLimiter rateLimiter;
@PostConstruct
public void init() {
rateLimiter = redissonClient.getRateLimiter("ossImageProcessingRateLimiter");
rateLimiter.trySetRate(RateType.OVERALL, 50, 1, RateIntervalUnit.SECONDS);
}
public boolean tryAcquire() {
return rateLimiter.tryAcquire();
}
}
5.2 在图片处理接口中应用限速
@PostMapping("/process")
public String processImage(@RequestBody ImageProcessRequest request) {
if (!rateLimiterService.tryAcquire()) {
throw new RuntimeException("请求速率过高,请稍后重试");
}
String processedImageUrl = imageProcessingService.processImage(request.getImageUrl(), request.getFileName());
return processedImageUrl;
}
参考文档
通过以上方案,您可以有效解决 iOS 原图上传 OSS 后无法通过链接访问显示的问题。如果您有任何疑问或建议,欢迎在评论区留言!