从初代 iPhone 到 iPhone 3GS,屏幕的分辨率一直是 320x480 像素,然而 随着 iPhone 4 的推出,屏幕的分辨率也提升到了 640x960,这一下子将屏幕的清晰度拔高到了一个前所未有的高度。对于我们开发的 app 来说,同时支持 iPhone 4 和前代 iPhone 是一个必选项,但是因为前后两种分辨率的巨大差异,使用一套 icon 图片跑在两种机型上是不切实际的。一种可行的办法是准备两套尺寸不同的图片,在代码里通过判断机型或屏幕分辨率来选择需要使用的图片。然而这种方法不仅代码写起来冗长,而且在后续的维护中也容易出错。因此,集合两种分辨率的倍数关系,苹果提出了一种以 point 而不是 pixel 的方式来指代屏幕分辨率,在 iPhone 3GS 上和 iPhone 4 上获取到的屏幕尺寸都是 320x480,相应的图片也推出了一种新的命名方法来区分:在 iPhone 4 等后续机型上,使用 @2x 结尾的图片名。比如说这张图片:

分别命名为 origin_cat.pngcat@2x.png,使用如下代码检查图片的尺寸和 scale 值:

if let originCat = UIImage(named: "origin_cat") {
    print(originCat.size)
    print(originCat.scale)
}
        
if let cat = UIImage(named: "cat") {
    print(cat.size)
    print(cat.scale)
}

编译运行,输出如下:

(300.0, 300.0)
1.0
(150.0, 150.0)
2.0

可以看出:获取到的图片尺寸乘以 scale 就可以得到图片的实际像素分辨率。因此在编写代码的时候,我们可以直接通过图片名(不包含 @2x)获取图片,系统会自动判断机型获取对应的图片。

然而随着手机的更新换代,iPhone 也终于迎来了大屏幕手机,相应的分辨率也是进一步提升,UIView 的属性里也多了一个 nativeScale 属性,然而这个属性的注释里并没有说清楚它到底是用来干嘛的。这里引用 OpenThread 的代码(我用 Swift 改写了一下):

let bounds = UIScreen.main.bounds
let screenMode = UIScreen.main.coordinateSpace.description
let scale = UIScreen.main.scale
let nativeScale = UIScreen.main.nativeScale
print("bounds: \(bounds)")
print("screen mode: \(screenMode)")
print("scale: \(scale)")
print("native scale: \(nativeScale)")

编译运行,在 iPhone 6/6s/7 标准显示模式下输出如下:

bounds: (0.0, 0.0, 375.0, 667.0)
screen mode: <UIScreen: 0x100d01f90; bounds = {0, 0, 375, 667}; mode = <UIScreenMode: 0x1740297a0; size = 750.000000 x 1334.000000>>
scale: 2.0
native scale: 2.0

在 iPhone 6/6s/7 放大显示模式下输出如下:

bounds: (0.0, 0.0, 320.0, 568.0)
screen mode: <UIScreen: 0x100d02010; bounds = {0, 0, 320, 568}; mode = <UIScreenMode: 0x1740235b0; size = 640.000000 x 1136.000000>>
scale: 2.0
native scale: 2.34375

在 iPhone 6/6s/7 plus 标准模式输出如下:

bounds: (0.0, 0.0, 414.0, 736.0)
screen mode: <UIScreen: 0x1342a4780; bounds = {0, 0, 414, 736}; mode = <UIScreenMode: 0x134286a0; size = 1242.000000 x 2208.000000>>
scale: 3.0
native scale: 2.608696

在 iPhone 6/6s/7 plus 放大模式输出如下:

bounds: (0.0, 0.0, 375.0, 667.0)
screen mode: <UIScreen: 0x200d0ff00; bounds = {0, 0, 375, 667}; mode = <UIScreenMode: 0x224029700; size = 1125.000000 x 2001.000000>>
scale: 3.0
native scale: 2.880000

查看 Apple 官网可以知道:iPhone 6/6s/7 的分辨率为 750x1334, iPhone 6/6s/7 plus 的分辨率为 1920x1080

对于 iPhone 6/6s/7 来说,标准显示模式下 scalenativeScale 相同,放大模式下,观察屏幕实际像素分辨率和获取到尺寸可知,750 / 320 = 2.34375,也就是 nativeScale 的值;对于 iPhone 6/6s/7 plus 来说,标准显示下 nativeScale2.608696,放大显示下 nativeScale2.880000,由 iPhone 6/6s/7 的结果推测可知:由于 plus 机型的实际分辨率为 1080x1920,在标准显示下,1080 / 414 = 2.608696,在放大显示下,1080 / 375 = 2.880000。

说了半天,好像看起来这个 nativeScale 属性在我们写程序中并没有太大的作用,从 PaintCode这幅图中也可以看出来系统在渲染的时候自动帮我们做了适配的这个工作:

实际上,查阅这个答案可以知道这样做是因为:使用 nativeScale 可以更精确的根据屏幕的实际分辨率进行渲染,进而避免可能的额外消耗。比如说,plus 机型的实际分辨率为 1080x1920,而它的虚拟分辨率为 1242x2208,如果按照虚拟分辨率渲染,则在渲染阶段和绘制时的 downsampling 阶段都会有额外的不必要的渲染操作。

除了 scalenativeScale 之外,我们还可以看到在 UIViewCALayer 中分别有 contentsScaleFactorcontentsScale 这两个属性,似乎看起来跟上面的 scale/nativeScale 存在某种关联,鉴于 UIViewCALayer 的关系,猜测 contentsScaleFactor 只是 contentsScale 的包装,用下面的代码来看看它们具体的值是什么样:

let contentsScale = view.contentScaleFactor
print("contents scale: \(contentsScale)")
let layerContentsScale = view.layer.contentsScale
print("layer contents scale: \(layerContentsScale)")

编译运行,在 iPhone 7 上输出如下:

contents scale: 1.0
layer contents scale: 1.0

很奇怪,为什么不是 2.0?搜索了一下,发现这个问题跟我有一样的疑惑。原来,当你在 UIView 子类里实现 draw(_ rect:) 方法想要自己绘制 UI 时,系统会自动将这个值设置为正确的值,而当你只使用 UI 控件而不是自己绘图就不需要考虑这个值了。

参考