点和像素
在 iOS 开发中,我们布局一个 UIView 和 CoreGraphics 绘制内容的时候,使用的单位是点(Point, 缩写 pt),而屏幕上的显示单位是像素(Pixel, 缩写 px)。
点和像素的换算规则取决于屏幕的精细程度(PPI:屏幕上每英寸可以显示的像素点的数量),比如:
- iPhone 3GS 中一点等于一像素
- iPhone 4 中一点等于两像素
- iPhone 6 Plus 中一点等于三像素
这个比例值我们可以通过 CGFloat screenScale = [UIScreen mainScreen].scale
获取 。
什么是像素对齐
通常我们指定一个 UIView 的 frame 会采用整数,由于点是像素的整数倍,所以这里像素是对齐的。
但是有的时候我们的 frame 是通过计算出来的,就会出现小数,这个小数乘以 screenScale 得出的数值可能不是整数,这就是像素不对齐。
还有一种特殊的像素不对齐,是使用 CoreGraphics 绘制内容时特有的,后面会详细说明。
UIView 的像素对齐
UILabel 的试验
我们设置一个 UILabel :
1 | UILabel *label = [[UILabel alloc] init]; |
通过 iPhone 13 Pro Max 的模拟器打开,在模拟器菜单中打开 Debug - Color Misaligned Images 选项,你会发现这个 UILabel 的背景变成黄色:
这是因为系统发现了 500.001 是一个像素不对齐的高度,将它标记它为黄色。
同样的,(100.1, 100, 300, 500),(100, 100.1, 300, 500) 都会有问题。
修正为 (100, 100, 300, 500) 就没问题了。
还有 (100, 100, 300, 500.33333) 也是没问题的,因为 500.33333 * 3 = 1501 是整数。
神奇的是,(100, 100, 300.1, 500) 是没问题的,猜测是 UILabel 的自动宽度导致(没有验证)。
不对齐会怎么样
有同学可能会问,对齐不对齐会有什么问题呢?好像也没什么影响,看起来上面的 UILable 也显示很正常。
我们采用「像素眼」来看看:(100, 100, 300, 500) vs (100, 100, 300, 500.1)
很明显,右边指定的高度高,渲染的时候需要多填充一个像素,而填充内容是一些半透明的像素,主观来说就是「右边糊了」。
再看:(100, 100, 300, 500) vs (100.1, 100, 300, 500)
这对比就更加夸张了,各种细节不一样,这是 x 处于像素点的中间位置,结果引发了更多抗锯齿来补充过渡颜色。
为什么会糊
让我们再进一步调查一下为什么会糊?
之前有个普遍猜想,当我们设置 x 为 100.1 或者 100.9 的时候,系统会自动帮我们取整数像素单位来显示,这对吗?
对的,但是跟我们想的可能有点不一样。
我们来对比一下 (100.1, 100, 300, 500) vs (100.9, 100, 300, 500):
我们可以很显著的发现,最左边那条竖线的颜色不一致,这是为什么呢?
先用苹果官方的话来解释一下:
简单理解一下就是,如果你非得从 0.5 像素的位置开始画线,系统也会帮你画,只是会触发抗锯齿(antialiasing),也就是会帮你把半个像素补齐到一个像素。
补齐规则是什么呢?
苹果没说,我根据试验结论猜测一下:根据该像素的占用面积的比例乘上原始颜色渲染出一个新的像素,比如苹果这个示例中就是 0.5 * 黑色,得出一个灰色。
结合上述论证与猜想,可以推测出上面文字 100.1 vs 100.9 竖线颜色不一的问题了。
根据我们前面的推测,100.1 占了 90% 像素面积,于是就是 90% 颜色深度,而 100.9 占了 10% 像素面积,也就是 10% 颜色深度。
所以,肉眼可见 100.1 比 100.9 深了许多。
其他视图
如果去尝试其他视图的情况,可以发现 UIImageView,UIButton 只要设置了图片或文字,都会有上述情况。
一个 UIView 只设置了背景颜色,那么像素不对齐也不会有什么问题。但是,用 CoreGraphics 画上一条蓝线,就会出现问题了。
所以得出结论:UIView 在有内容的情况下,像素不对齐就会触发抗锯齿,就会导致模糊现象。如果没有内容,只是设置了背景色则不会有问题。
CoreGraphics 的像素对齐
CoreGraphics 的像素对齐问题,又是什么情况呢?
假设我们用 CoreGraphics 在 (3, 0 ) 到 (3, 5) 画一条一像素宽的线时,这条线会落在哪里?
让我们魔改一下苹果的原图,会更好理解:
很明显,一倍屏的情况下,是在第三个点(point)上左右平分半个像素。
半个像素,是不是想起了什么?没错,触发了抗锯齿,于是你的一像素黑线变成了二像素灰线。
怎么解决呢?有两个方法:
- 向左或向右移动半个像素,在不同屏幕下通用公式是: ±(1 / screenScale / 2)
- 把线的宽度补齐到两个像素,这样你会获得一条两个像素的黑线
推广一下:使用 CoreGraphics 绘制奇数像素宽/高度线的时候,像素不对齐,会触发抗锯齿,导致模糊。
注意,偶数宽/高度的情况下,因为绘制内容会均匀地落在坐标两边的像素点中,不需要也不能去做奇数时的处理方案。
再来魔改一下苹果的图,方便大家理解偶数的情况:
最终还有一个点容易搞错,CoreGraphics 绘制问题不要运用到 UIView 体系中。
因为 CoreGraphics 画线的时候在宽度上是没有方向性的,所以在 x 点画一条线的时候,是以 x 为中线,左右平均分配线宽。
而 UIView 在布局的时候是有坐标系的,当我们指定一个 (3, 0, 1, 10) 的矩形框的时候(一倍屏),可以准确地在 3 像素开始向右边画出一个一像素宽的矩形(也就是一像素线)。
总结
所以,再次提炼一下「像素对齐」:当我们绘制视图时,应该让内容填满像素格。否则会触发了抗锯齿,可能导致内容模糊。
参考文档
iOS Drawing Concepts
iOS 绘制1像素的线
iOS优化:解决iOS中像素不对齐问题
iOS Color Misaligned Images优化
Aligned UIViews