YYImage Bug:一行之差,正常 JPEG 如何在 iOS 被读成条纹

深入理解代替单纯记忆

先看问题:原图正常,App 大图却扭曲

用户发布的是一张横拍 JPEG。把原图下载下来或在浏览器中打开,内容完全正常;但 iOS App 的大图页会显示为横向条纹和拉伸。

问题样本的原始像素是 4000 × 3000 ,EXIF Orientation 是 6(Right) 。这表示:像素数据本身是横向存储的,展示前还需要向右旋转 90°。

因此,问题不在 CDN、下载过程或图片上传结果,而在客户端对这张"需要旋转"的 JPEG 进行解码和方向归一化时。

真正的处理过程:src、tmp、dest 分别是什么

YYImage是如何做图像的旋转的?

下面用一张由"绿植、杯子、苹果、手机"等局部组成的缩小照片举例。

每个格子代表照片的一块区域:旋转后,照片内容已在 tmp 中转正;dest 的照片内容相同,但它是 vImage 为最终显示格式新分配的另一块内存。YYImage 最终还要将这块内存数据转为可使用的CGImage格式

为什么需要 tmp? 旋转不能在原图内存上边读边写:先写入的新位置可能覆盖尚未读取的原始像素。因此需要 tmp 作为新的旋转画布。

为什么还需要 dest? tmp 使用旋转函数方便处理的 ARGB 格式;如果最终显示需要不同的字节序或 alpha 格式,vImage 会新建 dest 做格式转换。若格式相同,代码会直接返回 tmpImage,不会创建 dest。

错误怎样变成条纹

  • dest 是 vImage 生成的 vImage_Buffer。vImage 为了让下一行从更适合 CPU / SIMD 读取的内存地址开始,可能把每行实际占用的字节数补齐到 16、32 或其他对齐边界

    • 下图中的灰色部分就是这段对齐填充字节:它占用内存,但不是图像像素,正常情况下不会显示出来。
  • 错误发生在dest转为 CGImage 的时候( 对应YYImageCoder.m中YYCGImageCreateAffineTransformCopy方法)

  • 创建 CGImage 时错误地使用了 destBytesPerRow,而不是 dest.rowBytes。这会让 CGImage 按错误的行距解释同一块 dest.data:第一行通常正常,从第二行开始会从错误偏移读取像素,最终绘制成条纹/拉伸

下图展示了出错的过程:

scss 复制代码
// 此处 dest 由 vImageBuffer_Init 在 helper 内部重新分配
YYCGImageDecodeToBitmapBufferWith32BitFormat(tmpImage, &dest, destBitmapInfo);

// 错:dest.data 的实际行距应是 dest.rowBytes
destImage = CGImageCreate(destWidth, destHeight, 8, 32,
                          destBytesPerRow, ...);

为什么YYImage 使用错误的destBytesPerRow? destBytesPerRow 是 YYImage 按宽度推算出来的,这是它认为的最终 dest 每行应有的字节数,并非实际的字节数

怎么修:使用 vImage 实际给出的行距

修复只改两处:Provider 的总字节数和 CGImage 的每行字节数都以 dest.rowBytes 为准。

ini 复制代码
// dest.rowBytes 是 vImage 实际输出缓冲区的行距
destProvider = CGDataProviderCreateWithData(dest.data, dest.data,
                                             dest.height * dest.rowBytes,
                                             YYCGDataProviderReleaseDataCallback);
destImage = CGImageCreate(destWidth, destHeight, 8, 32,
                          dest.rowBytes, // 与 dest.data 的真实内存布局一致
                          YYCGColorSpaceGetDeviceRGB(), destBitmapInfo,
                          destProvider, NULL, false,
                          kCGRenderingIntentDefault);

这是组件层的根因修复:不需要业务侧关闭 EXIF,也不需要把每张图先重绘、再重新编码。所有经过该旋转函数的路径都会使用正确的内存布局。

什么场景更容易复现

特征 为什么值得重点排查
JPEG 的 EXIF Orientation 不是 Up 需要执行方向旋转,才会进入这段代码;Right(6)和 Left(8)尤其典型。
Android / 三星手机横拍图片 本次问题样本属于这一类。不是三星专属,但这类来源的横拍 JPEG 更容易带有需要旋转的 EXIF 元数据。
原图、浏览器正常,只有 iOS App 异常 说明文件内容大概率正常,应优先检查 App 是否由 YYImage 执行了方向归一化。
同类图偶现而非必现 实际行距是否与预计行距不同受图像尺寸、像素格式、系统和内存对齐影响;这正是问题看起来罕见的原因。

补充:YYImage Issue #131 也在方向修正后出现乱码,最终同样会走到这个旋转函数;该 issue 的业务层做法是先手动转正以绕开问题,而本修复直接更正了底层行距。

相关推荐
fthux2 小时前
Tick Rest,一个休息提醒工具的自我修养
macos·开源·swift
茶底世界之下7 小时前
阴天花摊不必变暖:用 Awaking 留住花色与湿街
ios·app·产品
Fatbobman(东坡肘子)21 小时前
快速消失的硬盘容量 -- 肘子的 Swift 周报 #160
开发语言·人工智能·redis·ios·swiftui·iphone·swift
chaoahyl1 天前
WaterDiary 喝水记录 App 实战(2)· 实现喝水首页与本地保存
swift
茶底世界之下1 天前
斜光下的旧自行车:用 Awaking 留一版复古胶片色
ios·app·产品
2501_916008891 天前
iPhone 设备信息查看,用 KeyMob 看型号、序列号、电池健康和 UDID
android·ios·小程序·https·uni-app·iphone·webview
茶底世界之下2 天前
iPhone Duo 适配实战,Awaking 踩过的 8 个坑
ios·swiftui·swift
跨境数据猎手2 天前
HTTP 响应体编码与压缩:识别、解码
网络协议·http·ios
也非非也2 天前
iPhone 中国区 AI 落地:Apple 与阿里千问合作的技术观察
大数据·人工智能·ios·agent·iphone·智子