为了适应不同尺寸、不同DPR(device pixel rate)的屏幕,微信小程序通过rpx这个长度单位来实现自适应布局,理论上只要屏幕高宽比变化不大,小程序可以在任意屏幕上无损渲染。
基本概念
在微信小程序中,利用style属性、wxss样式文件,为组件设置样式,可以使用rpx、vw/vh、px几种单位设计组件的布局。要理解这几种单位的区别,需要了解物理像素和设备无关像素(DIP):
- 物理像素:物理像素和设备相关,其大小在不同设备上可能不一样,像素密度越大,物理像素宽度越小。一般以px为单位,但是在CSS中px指的是DIP单位,浏览器内核中一般以in_pixel后缀来标识物理像素。
- DIP:设备不相关的一个单位,是一个绝对大小值。
- DSF:device scale factor,即一个DIP单位所占物理像素的比例。类似的概念如dpr等于:1个DIP/1个物理像素宽度比,例如iphone6的物理像素宽度是750px,dpr=2,那么一个DIP等于两个物理像素。
然后介绍一下微信小程序用到的几个单位:
rpx
参考微信官方文档介绍:
rpx不是绝对值,它是将屏幕固定等分为750份,每一份就是1rpx,所以屏幕越宽1rpx越大,微信小程序框架解析样式时,会将rpx转换成CSS的px,即:
A (rpx)* window.screen.width / 750 = B(px)
vw/vh
vw/vh和rpx类似是一个相对单位,但是以viewport的高度或宽度为比例v即viewport,浏览器内核原生支持,关键代码:
// Viewport size that should be used for viewport units (i.e. 'vh'/'vw').
// May include the size of browser controls. See implementation for further
// documentation.
FloatSize LocalFrameView::ViewportSizeForViewportUnits() const {
......
zoom = GetFrame().PageZoomFactor();
......
layout_size.SetWidth(layout_view_item.ViewWidth(kIncludeScrollbars) / zoom);
layout_size.SetHeight(layout_view_item.ViewHeight(kIncludeScrollbars) / zoom);
......
return layout_size;
}
获取到viewport size,后续vw/vh都根据这个值按比例转换为像素;这里除以了一个zoom,它就是DSF的值,所以viewport大小也是DIP单位。
px
https://www.chromium.org/developers/design-documents/blink-coordinate-spaces/#types-of-pixels
CSS pixels: CSS defines its own pixel type that is also independent of physical pixels. When there is no Browser-Zoom, Pinch-Zoom, or CSS transforms applied, CSS pixels and DIPs are equivalent. However, zoom can make CSS pixels bigger or smaller relative to DIPs.
在不缩放的情况下,1px等于1DIP。后面会介绍到zoom发生变化时,1px(CSS)也会发生变化。
UseZoomForDSF
Linux平台上,这个开关是默认打开的(–enable-use-zoom-for-dsf),它的意思是使用跟页面缩放一样的技术来实现对 DSF 的处理,内核在Layout阶段就会进行坐标的缩放。相比于按照1x比例Paint,然后按照DSF缩放Draw,使用zoom的好处是画面可以任意缩放而不会产生失真。
https://www.chromium.org/developers/design-documents/blink-coordinate-spaces/#types-of-pixels
It changes the size of a CSS pixel relative to a device independent pixel and so it will cause page layout to change. Throughout Blink, Browser-zoom is referred to as “Page Zoom” or “Zoom” more generally.
也就是说DSF被当成了zoom,而zoom会影响一个CSS 1px的大小,同时会影响布局,文字字号、图片解码大小等等,渲染出来的效果类似于矢量图形。
实验
一. 修改屏幕属性
- 修改窗口物理像素尺寸,从原来的1920*720,调整为3840*1440
- 将–force-device-scale-factor选项设置为2,设置一个强制的DSF。
经过上述修改,通过Display获取的屏幕物理像素大小就是3840*1440,而在打开USE_AURA宏开关的情况下,通过Display::Bounds获取的是DPI单位尺寸:3840/2 * 1440/2 = 1920*720
对比了一下只修改屏幕尺寸,和同时修改屏幕尺寸和DSF的效果对比:
只修改屏幕尺寸,rpx/vw的布局是对的,但是CSS px也是基于大尺寸(3840宽度)布局的,所以看上去特别小
同时修改屏幕大小和DSF,CSS px的布局基于宽度1920,所以看上去要大一些
后面可以看到为什么这两处修改会产生这些影响:
对rpx的影响
rpx依赖window.screen.width,从下面window.screen.width获取的调用栈来看,window.screen.width是DIP单位宽度:
screen::AdjustSurfaceSize // wayland window设置的以物理像素为单位的大小
Display::SetScaleAndBounds
Display::bounds // 通过Display::Bounds获取的是DPI单位尺寸
RenderWidget::GetScreenInfo
GetChromeClient().GetScreenInfo
Screen::width // 最终window.screen.width拿到的就是DIP单位
根据rpx转换px的公式:
A (rpx)* window.screen.width / 750 = B(px)
根据微信官方文档,rpx的换算是以物理像素宽度(iphone6 750px)为基准,而window.screen.width是DIP单位,所以换算出的px是CSS的px单位,不是物理像素。
对vw/vh的影响
viewport size的大小最终也是从Display获取,但是Display::bounds已经转换成了DIP,LocalFrameView::ViewportSizeForViewportUnits又会对尺寸除以zoom,所以传递过来的必须是物理像素单位。
对Surface大小的影响
经过上述修改,小程序组件的布局会基于3840 * 1440的物理像素尺寸,但是由于DSF设置为2,原来的rpx/px/vw/vh单位实际上会放大一倍,这样理论上组件的布局没有发生变化,但是整体效果会放大一倍,这可以通过Chromium提交到Wayland Compositor的绘制结果截图看出。
Chromim使用OpenGL/EGL渲染,最终渲染目标是一个类型为EGLSurface的窗口类型,通过eglCreateWindowSurface和传入的EGLNativeWindowType window参数创建,在Wayland渲染框架中,EGLNativeWindowType实际类型是wl_egl_window,它可以通过wl_egl_window_resize命令修改大小,其影响EGLSurface的大小,最终影响提交到父级Compositor的Surface大小。
Resize的时机,是在绘制每一帧的CompositorFrame前,检查当前viewport大小和下一帧CompositorFrame记录的viewport大小是否相同,不同就Resize一下,修改WindowSurface的大小。
从每一帧的绘制截图来看,图片的大小是3840*1440,说明图片在不失真的情况下放大了一倍。