PHAsset 获取图片高宽的 Bug

最近遇到一个在 App 中打开相册 Crash 的问题,但是我们无法复现,于是找用户要到了她要上传的图片的原图,原图是一张微博等平台上常见的超长图片。现在不少专业的博主为了排版方便及全平台统一,都会使用一整张长图来发内容,而不是用图文混排的方式。

图片导入到相册后发现的确会 Crash,于是单步跟进,发现这张图片高度居然是 18446744073709519761!很明显不正常,因为在电脑上查看图片高度为 33681,如果 App 认为它高度是那么多,就会照那个大小去申请内存,不 Crash 才怪。那么这个错误的高度与真实高度有什么联系呢?想到 NSUInteger 的 (-1) = 18446744073709551615(64位系统),中间差 31854,同时图片的实际高 33681 ,而:

33681 + 31854 = 65535

于是就有了思路,这就是一个越界的问题。

先看 PHAsset 关于图片高宽的接口的声明:

@interface PHAsset : PHObject
@property (nonatomic, assign, readonly) NSUInteger pixelWidth;
@property (nonatomic, assign, readonly) NSUInteger pixelHeight;
@end

返回的就是一个 NSUInteger,然而它的底层 C 实现很可能是返回 signed short,如:

short ph_get_width(void *asset);  
short ph_get_height(void *asset);  

signed short 最大可表示的值是 32767,当返回一个 33681时,隐式转换后的值是 -31854,然后又被扩展到 64 位的 NSUInteger,前面填 1 补齐,就成了一个很大的数了。

(33681)
10000011 10010001  
(18446744073709519761)
11111111 11111111 11111111 11111111 11111111 11111111 10000011 10010001  

修复方法也很简单:

@implementation PHAsset (Bugfix)

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self jr_swizzleMethod:@selector(pixelWidth)
                    withMethod:@selector(bugfix_pixelWidth)
                         error:NULL];
        [self jr_swizzleMethod:@selector(pixelHeight)
                    withMethod:@selector(bugfix_pixelHeight)
                         error:NULL];
    });
}

- (NSUInteger)bugfix_pixelWidth
{
    NSUInteger width = [self bugfix_pixelWidth];
    if (width > 0xffff) {
        width &= 0xffff;
    }
    return width;
}

- (NSUInteger)bugfix_pixelHeight
{
    NSUInteger height = [self bugfix_pixelHeight];
    if (height > 0xffff) {
        height &= 0xffff;
    }
    return height;
}

@end

相当于越界时直接丢弃前面的 1,但是图片高度依然不可以超过 65535。

此问题已经提交 Apple Bug Report,但是他们回复需要诊断日志,我就懒得搞啦。

Apple Developer Relations  
February 23 2017, 5:24 AM  
Engineering has requested a sysdiagnose in order to further investigate this issue.

Important: Note the date and time the issue occurred and include this information in your bug report.

Note: It's important to trigger the sysdiagnose process as soon as possible after the problem occurs, even if the logs can’t be synced off the device until later.  

最新进展(2017-9-27)

苹果终于回复了!iOS 11 中修复了这个问题:

Hello Ricky,

This is a follow-up regarding regarding Bug ID# 30463403.  

Please verify this issue with the iOS 11 GM and update your bug report at https://bugreport.apple.com/ with your results.

iOS 11 GM (15A372)  
https://developer.apple.com/download/  
Posted Date: Sep 19th, 2017

If the issue persists, please attach a new sysdiagnose captured in the latest build and attach it to the bug report. Thank you.