在做Android app开发的时候,为了验证不同屏幕分辨率和dpi下界面的布局情况。你可以使用android emulator来实现,也可以找不同屏幕配置的手机来验证。当然,你可以找台Android原生系统的手机来验证如Nexus 4/Nexus 5系列的手机来验证。Android系统中有一个wm命令,可以设置显示窗口的尺寸(重新设置屏幕的罗辑分辩率)和屏幕的dpi。
$ adb shell wm size 540x960
显示窗口的尺寸可以比屏幕的物理分辨率大,也可以比它小。重置用如下命令:
$ adb shell wm size reset
设置完之后,你会发现SurfaceFlinger中的Layer的尺寸也发生的变化。
以Nexus4为例,原来Live Wallpaper: PhaseBeam Layer的尺寸为768×1280:
$ adb shell dumpsys SurfaceFlinger
...
+ Layer 0xb7bbf3b0 (com.android.phasebeam.PhaseBeamWallpaper)
Region transparentRegion (this=0xb7bbf510, count=1)
[ 0, 0, 0, 0]
Region visibleRegion (this=0xb7bbf3b8, count=1)
[ 0, 50, 768, 1184]
layerStack= 0, z= 21000, pos=(0,0), size=( 768,1280), crop=( 0, 50, 768,1184), isOpaque=1, invalidate=0, alpha=0xff, flags=0x00000002, tr=[1.00, 0.00][0.00, 1.00]
client=0xb7bad728
format= 2, activeBuffer=[ 768x1280: 768, 3], queued-frames=0, mRefreshPending=0
mTexName=10 mCurrentTexture=0
mCurrentCrop=[0,0,0,0] mCurrentTransform=0
mAbandoned=0
-BufferQueue mMaxAcquiredBufferCount=1, mDequeueBufferCannotBlock=0, default-size=[768x1280], default-format=2, transform-hint=00, FIFO(0)={}
>[00:0xb7bbef08] state=ACQUIRED, 0xb7b18260 [ 768x1280: 768, 3]
[01:0xb7baf328] state=FREE , 0xb7badaa8 [ 768x1280: 768, 3]
[02:0xb7b1a5d0] state=FREE , 0xb7bad9a0 [ 768x1280: 768, 3]
...
将显示窗口的尺寸设为1080×1920之后:
$ adb shell dumpsys SurfaceFlinger
...
+ Layer 0xb7d596b8 (com.android.phasebeam.PhaseBeamWallpaper)
Region transparentRegion (this=0xb7d59818, count=1)
[ 0, 0, 0, 0]
Region visibleRegion (this=0xb7d596c0, count=1)
[ 0, 50, 1080, 1824]
layerStack= 0, z= 21000, pos=(0,0), size=(1080,1920), crop=( 0, 50,1080,1824), isOpaque=1, invalidate=0, alpha=0xff, flags=0x00000002, tr=[1.00, 0.00][0.00, 1.00]
client=0xb7dbf270
format= 2, activeBuffer=[1080x1920:1152, 3], queued-frames=0, mRefreshPending=0
mTexName=10 mCurrentTexture=2
mCurrentCrop=[0,0,0,0] mCurrentTransform=0
mAbandoned=0
-BufferQueue mMaxAcquiredBufferCount=1, mDequeueBufferCannotBlock=0, default-size=[1080x1920], default-format=2, transform-hint=00, FIFO(0)={}
[00:0xb7dd1fb0] state=FREE , 0xb7dd0048 [1080x1920:1152, 3]
[01:0xb7d5c2c8] state=FREE , 0xb7d39ca8 [1080x1920:1152, 3]
>[02:0xb7d1d578] state=ACQUIRED, 0xb7d5cb18 [1080x1920:1152, 3]
...
可以想象得到,系统把这个layer对应的窗口当成了一个1080p屏幕来布局和绘制了。
$ adb shell wm density 320
常用的dpi有160(mdpi), 240(hdpi), 320(xhdpi), 480(xxhdpi)。重置可用如下命令:
$ adb shell wm density reset
首先看一下wm命令如何通知WindowManagerService更新配置:
代码:frameworks/base/cmds/wm/src/com/android/commands/wm/Wm.java
public class Wm extends BaseCommand { ... public void onRun() throws Exception { mWm = IWindowManager.Stub.asInterface(ServiceManager.checkService( Context.WINDOW_SERVICE)); if (mWm == null) { System.err.println(NO_SYSTEM_ERROR_CODE); throw new AndroidException("Can't connect to window manager; is the system running?"); } String op = nextArgRequired(); if (op.equals("size")) { runDisplaySize(); } else if (op.equals("density")) { runDisplayDensity(); } else if (op.equals("overscan")) { runDisplayOverscan(); } else { showError("Error: unknown command '" + op + "'"); return; } } ... private void runDisplaySize() throws Exception { String size = nextArg(); int w, h; if (size == null) { Point initialSize = new Point(); Point baseSize = new Point(); try { mWm.getInitialDisplaySize(Display.DEFAULT_DISPLAY, initialSize); mWm.getBaseDisplaySize(Display.DEFAULT_DISPLAY, baseSize); System.out.println("Physical size: " + initialSize.x + "x" + initialSize.y); if (!initialSize.equals(baseSize)) { System.out.println("Override size: " + baseSize.x + "x" + baseSize.y); } } catch (RemoteException e) { } return; } else if ("reset".equals(size)) { w = h = -1; } else { int div = size.indexOf('x'); if (div <= 0 || div >= (size.length()-1)) { System.err.println("Error: bad size " + size); return; } String wstr = size.substring(0, div); String hstr = size.substring(div+1); try { w = Integer.parseInt(wstr); h = Integer.parseInt(hstr); } catch (NumberFormatException e) { System.err.println("Error: bad number " + e); return; } } try { if (w >= 0 && h >= 0) { // TODO(multidisplay): For now Configuration only applies to main screen. mWm.setForcedDisplaySize(Display.DEFAULT_DISPLAY, w, h); } else { mWm.clearForcedDisplaySize(Display.DEFAULT_DISPLAY); } } catch (RemoteException e) { } } ... }
先获取WindowManagerService再调用它的setForcedDisplaySize()方法。
代码:frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
public class WindowManagerService extends IWindowManager.Stub implements Watchdog.Monitor, WindowManagerPolicy.WindowManagerFuncs { ... @Override public void setForcedDisplaySize(int displayId, int width, int height) { if (mContext.checkCallingOrSelfPermission( android.Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Must hold permission " + android.Manifest.permission.WRITE_SECURE_SETTINGS); } if (displayId != Display.DEFAULT_DISPLAY) { throw new IllegalArgumentException("Can only set the default display"); } final long ident = Binder.clearCallingIdentity(); try { synchronized(mWindowMap) { // Set some sort of reasonable bounds on the size of the display that we // will try to emulate. final int MIN_WIDTH = 200; final int MIN_HEIGHT = 200; final int MAX_SCALE = 2; final DisplayContent displayContent = getDisplayContentLocked(displayId); if (displayContent != null) { width = Math.min(Math.max(width, MIN_WIDTH), displayContent.mInitialDisplayWidth * MAX_SCALE); height = Math.min(Math.max(height, MIN_HEIGHT), displayContent.mInitialDisplayHeight * MAX_SCALE); setForcedDisplaySizeLocked(displayContent, width, height); Settings.Global.putString(mContext.getContentResolver(), Settings.Global.DISPLAY_SIZE_FORCED, width + "," + height); } } } finally { Binder.restoreCallingIdentity(ident); } } ... // displayContent must not be null private void setForcedDisplaySizeLocked(DisplayContent displayContent, int width, int height) { Slog.i(TAG, "Using new display size: " + width + "x" + height); synchronized(displayContent.mDisplaySizeLock) { displayContent.mBaseDisplayWidth = width; displayContent.mBaseDisplayHeight = height; } reconfigureDisplayLocked(displayContent); } ... // displayContent must not be null private void reconfigureDisplayLocked(DisplayContent displayContent) { // TODO: Multidisplay: for now only use with default display. configureDisplayPolicyLocked(displayContent); displayContent.layoutNeeded = true; boolean configChanged = updateOrientationFromAppTokensLocked(false); mTempConfiguration.setToDefaults(); mTempConfiguration.fontScale = mCurConfiguration.fontScale; if (computeScreenConfigurationLocked(mTempConfiguration)) { if (mCurConfiguration.diff(mTempConfiguration) != 0) { configChanged = true; } } if (configChanged) { mWaitingForConfig = true; startFreezingDisplayLocked(false, 0, 0); mH.sendEmptyMessage(H.SEND_NEW_CONFIGURATION); } performLayoutAndPlaceSurfacesLocked(); } ... }
setForcedDisplaySize()->setForcedDisplaySizeLocked()->reconfigureDisplayLocked()->SEND_NEW_CONFIGURATION
这里可以看到可以设置的屏幕窗口最小为(200,200),最大小显示屏分辨率的两倍。
而WindowManagerService又是如何通知DisplayManagerService更新配置:
WindowsManagerService$H.handleMessage(SEND_NEW_CONFIGURATION)
->WindowManagerService.sendNewConfiguration()
->ActivityManagerService.updateConfiguration() x2
->WindowManagerService.setNewConfiguration()
->WindowManagerService.performLayoutAndPlaceSurfacesLocked()
->WindowManagerService.performLayoutAndPlaceSurfaceLockedLoop()
->WindowManagerService.performLayoutAndPlaceSurfaceLockedInner()
->DisplayManagerService$LocalService.performTraversalInTransactionFromWindowManager()
->DisplayManagerService.access$4200()
->DisplayManagerService.performTraversalInTransactionFromWindowManagerInternal()
->DisplayManagerService.performTraversalInTransactionLocked()
->DisplayManagerService.configureDisplayInTransactionLocked()
->LogicalDisplay.configureDisplayInTransactionLocked()
到这里就会去设置layer stack的尺寸和在屏幕上的位置:
final class LogicalDisplay { ... /** * Applies the layer stack and transformation to the given display device * so that it shows the contents of this logical display. * * We know that the given display device is only ever showing the contents of * a single logical display, so this method is expected to blow away all of its * transformation properties to make it happen regardless of what the * display device was previously showing. * * The caller must have an open Surface transaction. * * The display device may not be the primary display device, in the case * where the display is being mirrored. * * @param device The display device to modify. * @param isBlanked True if the device is being blanked. */ public void configureDisplayInTransactionLocked(DisplayDevice device, boolean isBlanked) { final DisplayInfo displayInfo = getDisplayInfoLocked(); final DisplayDeviceInfo displayDeviceInfo = device.getDisplayDeviceInfoLocked(); // Set the layer stack. device.setLayerStackInTransactionLocked(isBlanked ? BLANK_LAYER_STACK : mLayerStack); // Set the refresh rate device.requestRefreshRateLocked(mRequestedRefreshRate); // Set the viewport. // This is the area of the logical display that we intend to show on the // display device. For now, it is always the full size of the logical display. mTempLayerStackRect.set(0, 0, displayInfo.logicalWidth, displayInfo.logicalHeight); // Set the orientation. // The orientation specifies how the physical coordinate system of the display // is rotated when the contents of the logical display are rendered. int orientation = Surface.ROTATION_0; if ((displayDeviceInfo.flags & DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT) != 0) { orientation = displayInfo.rotation; } // Apply the physical rotation of the display device itself. orientation = (orientation + displayDeviceInfo.rotation) % 4; // Set the frame. // The frame specifies the rotated physical coordinates into which the viewport // is mapped. We need to take care to preserve the aspect ratio of the viewport. // Currently we maximize the area to fill the display, but we could try to be // more clever and match resolutions. boolean rotated = (orientation == Surface.ROTATION_90 || orientation == Surface.ROTATION_270); int physWidth = rotated ? displayDeviceInfo.height : displayDeviceInfo.width; int physHeight = rotated ? displayDeviceInfo.width : displayDeviceInfo.height; // Determine whether the width or height is more constrained to be scaled. // physWidth / displayInfo.logicalWidth => letter box // or physHeight / displayInfo.logicalHeight => pillar box // // We avoid a division (and possible floating point imprecision) here by // multiplying the fractions by the product of their denominators before // comparing them. int displayRectWidth, displayRectHeight; if (physWidth * displayInfo.logicalHeight < physHeight * displayInfo.logicalWidth) { // Letter box. displayRectWidth = physWidth; displayRectHeight = displayInfo.logicalHeight * physWidth / displayInfo.logicalWidth; } else { // Pillar box. displayRectWidth = displayInfo.logicalWidth * physHeight / displayInfo.logicalHeight; displayRectHeight = physHeight; } int displayRectTop = (physHeight - displayRectHeight) / 2; int displayRectLeft = (physWidth - displayRectWidth) / 2; mTempDisplayRect.set(displayRectLeft, displayRectTop, displayRectLeft + displayRectWidth, displayRectTop + displayRectHeight); device.setProjectionInTransactionLocked(orientation, mTempLayerStackRect, mTempDisplayRect); } ... }
->DisplayDevice.setProjectionInTransactionLocked()
通知SurfaceFlinger:
abstract class DisplayDevice {
...
/**
* Sets the display projection while in a transaction.
*
* @param orientation defines the display's orientation
* @param layerStackRect defines which area of the window manager coordinate
* space will be used
* @param displayRect defines where on the display will layerStackRect be
* mapped to. displayRect is specified post-orientation, that is
* it uses the orientation seen by the end-user
*/
public final void setProjectionInTransactionLocked(int orientation,
Rect layerStackRect, Rect displayRect) {
if (mCurrentOrientation != orientation
|| mCurrentLayerStackRect == null
|| !mCurrentLayerStackRect.equals(layerStackRect)
|| mCurrentDisplayRect == null
|| !mCurrentDisplayRect.equals(displayRect)) {
mCurrentOrientation = orientation;
if (mCurrentLayerStackRect == null) {
mCurrentLayerStackRect = new Rect();
}
mCurrentLayerStackRect.set(layerStackRect);
if (mCurrentDisplayRect == null) {
mCurrentDisplayRect = new Rect();
}
mCurrentDisplayRect.set(displayRect);
SurfaceControl.setDisplayProjection(mDisplayToken,
orientation, layerStackRect, displayRect);
}
}
...
}
最后,SurfaceFlinger就会跟据这些信息对Layer进行合成,显示在屏幕上。
代码:frameworks/native/services/surfaceflinger/DisplayDevice.cpp
void DisplayDevice::setProjection(int orientation, const Rect& newViewport, const Rect& newFrame) { Rect viewport(newViewport); Rect frame(newFrame); const int w = mDisplayWidth; const int h = mDisplayHeight; Transform R; DisplayDevice::orientationToTransfrom(orientation, w, h, &R); if (!frame.isValid()) { // the destination frame can be invalid if it has never been set, // in that case we assume the whole display frame. frame = Rect(w, h); } if (viewport.isEmpty()) { // viewport can be invalid if it has never been set, in that case // we assume the whole display size. // it's also invalid to have an empty viewport, so we handle that // case in the same way. viewport = Rect(w, h); if (R.getOrientation() & Transform::ROT_90) { // viewport is always specified in the logical orientation // of the display (ie: post-rotation). swap(viewport.right, viewport.bottom); } } dirtyRegion.set(getBounds()); Transform TL, TP, S; float src_width = viewport.width(); float src_height = viewport.height(); float dst_width = frame.width(); float dst_height = frame.height(); if (src_width != dst_width || src_height != dst_height) { float sx = dst_width / src_width; float sy = dst_height / src_height; S.set(sx, 0, 0, sy); } float src_x = viewport.left; float src_y = viewport.top; float dst_x = frame.left; float dst_y = frame.top; TL.set(-src_x, -src_y); TP.set(dst_x, dst_y); // The viewport and frame are both in the logical orientation. // Apply the logical translation, scale to physical size, apply the // physical translation and finally rotate to the physical orientation. mGlobalTransform = R * TP * S * TL; const uint8_t type = mGlobalTransform.getType(); mNeedsFiltering = (!mGlobalTransform.preserveRects() || (type >= Transform::SCALE)); mScissor = mGlobalTransform.transform(viewport); if (mScissor.isEmpty()) { mScissor = getBounds(); } mOrientation = orientation; mViewport = viewport; mFrame = frame; }
看来还是google原生系统处理得比较好,系在统重启之后,还没有找到有问题的布局(bootanimation会显示不正常)。