背景
首先介绍一下wm size命令, 是 Android 开发者最常用的调试命令之一,可以强制修改设备的屏幕分辨率。执行 adb shell wm size 1080x1920 后,设备屏幕立刻以 1080x1920 的分辨率渲染,且重启后仍然生效。
一个wm size命令干废了我的手机。
但是大家注意了wm size这个命令一定不要在你的个人正常使用的手机上使用,马哥这边就踩了一个大坑,拿着自己平时用的手机使用wm size修改了屏幕size,重启后导致手势解锁画面无法展示正常,无法解锁。

可以发现解锁画面连9宫格都无法展示出来,导致根本无法解锁。
导致整个手机就基本报废,虽然可能可以使用恢复出厂设置修改好,但是数据肯定是没有了,往往贵的都是数据,数据无价,所以大家切记切记不要随意用自己的手机进行随意的wm size设置屏幕尺寸。
1、Shell 脚本入口
文件:frameworks/base/cmds/wm/wm
#!/system/bin/sh
cmd window "$@"
wm 本身只是一个 2 行的 shell 脚本,它将所有参数原样转发给 cmd window。
cmd 是 Android 的通用服务调用二进制(源码位于 frameworks/native/cmds/cmd/),它的核心逻辑在 cmd.cpp 的 cmdMain() 函数中:
// frameworks/native/cmds/cmd/cmd.cpp (简化)
sp<IServiceManager> sm = defaultServiceManager();
// ...
String16 serviceName = argv[0]; // "window"
Vector<String16> args; // ["size", "1080x1920"]
// ...
sp<IBinder> service = sm->checkService(serviceName);
status_t error = IBinder::shellCommand(service, in, out, err, args, cb, result);
IBinder::shellCommand() 将所有参数打包进 Parcel,通过 SHELL_COMMAND_TRANSACTION 这个 Binder 事务码发送给 "window" 服务。
2、Binder 分发与 WMS.onShellCommand
"window" 服务在系统启动时注册于 SystemServer.java:
// frameworks/base/services/java/com/android/server/SystemServer.java
ServiceManager.addService(Context.WINDOW_SERVICE, wm,
/* allowIsolated= */false,
DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PROTO);
当 SHELL_COMMAND_TRANSACTION 到达服务端,Binder.java 的 onTransact 会回调到 WindowManagerService.onShellCommand():
// frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java:1186
@Override
publicvoidonShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
String[] args, ShellCallback callback, ResultReceiver result){
new WindowManagerShellCommand(this).exec(this, in, out, err, args, callback, result);
}
这里创建了 WindowManagerShellCommand 对象并调用其 exec() 方法。exec() 继承自 BasicShellCommandHandler,它会提取第一个位置参数作为命令名(即 "size"),然后调用 onCommand("size")。
4、命令路由与参数解析
文件:frameworks/base/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
4.1 onCommand — 命令分发
// 第 68-77 行
publicintonCommand(String cmd){
if (cmd == null) {
return handleDefaultCommands(cmd);
}
switch (cmd) {
case"size":
return runDisplaySize(pw);
// ... 其他命令 ...
}
}
"size" 字符串匹配到 runDisplaySize() 方法。
4.2 runDisplaySize — 参数解析
// 第 168-203 行
privateintrunDisplaySize(PrintWriter pw)throws RemoteException {
String size = getNextArg(); // 获取 "1080x1920"
int w, h;
finalint displayId = getDisplayId(size);
if (size == null) {
printInitialDisplaySize(pw, displayId); // 无参数 → 打印当前尺寸
return0;
} elseif ("-d".equals(size)) {
printInitialDisplaySize(pw, displayId); // -d 参数 → 打印当前尺寸
return0;
} elseif ("reset".equals(size)) {
w = h = -1; // reset → 恢复原始尺寸
} else {
int div = size.indexOf('x'); // 找到 'x' 分隔符位置
if (div <= 0 || div >= (size.length()-1)) {
getErrPrintWriter().println("Error: bad size " + size);
return -1;
}
String wstr = size.substring(0, div); // "1080"
String hstr = size.substring(div+1); // "1920"
try {
w = parseDimension(wstr, displayId); // 解析宽度
h = parseDimension(hstr, displayId); // 解析高度
} catch (NumberFormatException e) {
getErrPrintWriter().println("Error: bad number " + e);
return -1;
}
}
if (w >= 0 && h >= 0) {
mInterface.setForcedDisplaySize(displayId, w, h); // 设置强制尺寸
} else {
mInterface.clearForcedDisplaySize(displayId); // 清除强制尺寸
}
return0;
}
解析逻辑要点:
以 'x'为分隔符拆分宽高parseDimension()支持后缀:px(像素)、dp(密度无关像素)、无后缀(默认像素)支持 -d DISPLAY_ID指定目标显示器(默认 displayId=0 即主屏)
4.3 parseDimension — 单位换算
// 第 339-354 行
privateintparseDimension(String s, int displayId)throws NumberFormatException {
if (s.endsWith("px")) {
return Integer.parseInt(s.substring(0, s.length() - 2));
}
if (s.endsWith("dp")) {
int density = mInterface.getBaseDisplayDensity(displayId);
return Integer.parseInt(s.substring(0, s.length() - 2))
* density / DisplayMetrics.DENSITY_DEFAULT;
}
return Integer.parseInt(s); // 无后缀,直接作为像素值
}
5、WMS.setForcedDisplaySize — 权限校验
文件:frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
// 第 5657-5675 行
@Override
publicvoidsetForcedDisplaySize(int displayId, int width, int height){
// 1. 权限校验:必须持有 WRITE_SECURE_SETTINGS
if (mContext.checkCallingOrSelfPermission(WRITE_SECURE_SETTINGS)
!= PackageManager.PERMISSION_GRANTED) {
thrownew SecurityException("Must hold permission " + WRITE_SECURE_SETTINGS);
}
// 2. 清除 calling identity(以 system 身份执行)
finallong ident = Binder.clearCallingIdentity();
try {
synchronized (mGlobalLock) {
final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
if (displayContent != null) {
displayContent.setForcedSize(width, height); // 委托给 DisplayContent
}
}
} finally {
Binder.restoreCallingIdentity(ident);
}
}
WRITE_SECURE_SETTINGS 是 signature|privileged 级别的权限,仅系统进程和 shell 用户持有。这就是为什么 adb shell 可以执行此命令。
6、DisplayContent.setForcedSize — 尺寸生效核心
文件:frameworks/base/services/core/java/com/android/server/wm/DisplayContent.java
// 第 3460-3487 行
voidsetForcedSize(int width, int height){
// 1. 不能超过 maxUiWidth(通常用于限制屏幕宽度的属性)
if (mMaxUiWidth > 0 && width > mMaxUiWidth) {
finalfloat ratio = mMaxUiWidth / (float) width;
height = (int) (height * ratio);
width = mMaxUiWidth;
}
// 2. 判断是否真的做了强制修改
mIsSizeForced = mInitialDisplayWidth != width || mInitialDisplayHeight != height;
if (mIsSizeForced) {
// 3. 边界约束:最小 200px,最大为原始尺寸的 2 倍
finalint minSize = 200;
finalint maxScale = 2;
width = Math.min(Math.max(width, minSize), mInitialDisplayWidth * maxScale);
height = Math.min(Math.max(height, minSize), mInitialDisplayHeight * maxScale);
}
Slog.i(TAG_WM, "Using new display size: " + width + "x" + height);
// 4. 更新基础显示指标(内存中的宽高数据)
updateBaseDisplayMetrics(width, height, mBaseDisplayDensity,
mBaseDisplayPhysicalXDpi, mBaseDisplayPhysicalYDpi);
// 5. 触发全局重配置,对所有窗口生效
reconfigureDisplayLocked();
// 6. 持久化存储
if (!mIsSizeForced) {
width = height = 0; // 恢复原始尺寸时,写入 0 表示清除
}
mWmService.mDisplayWindowSettings.setForcedSize(this, width, height);
}
这个方法完成了三件关键事情,下面逐一展开。
6.1 updateBaseDisplayMetrics — 内存中生效
// 第 3388-3417 行
voidupdateBaseDisplayMetrics(int baseWidth, int baseHeight,
int baseDensity, float baseXDpi, float baseYDpi){
mBaseDisplayWidth = baseWidth; // ← 新的宽度
mBaseDisplayHeight = baseHeight; // ← 新的高度
mBaseDisplayDensity = baseDensity;
mBaseDisplayPhysicalXDpi = baseXDpi;
mBaseDisplayPhysicalYDpi = baseYDpi;
// 如果尺寸是强制的,需要重新计算刘海屏和圆角裁剪区域
if (mIsSizeForced) {
mBaseDisplayCutout = loadDisplayCutout(baseWidth, baseHeight);
mBaseRoundedCorners = loadRoundedCorners(baseWidth, baseHeight);
}
// 应用 maxUiWidth 约束(与 setForcedSize 中的逻辑呼应)
if (mMaxUiWidth > 0 && mBaseDisplayWidth > mMaxUiWidth) {
finalfloat ratio = mMaxUiWidth / (float) mBaseDisplayWidth;
mBaseDisplayHeight = (int) (mBaseDisplayHeight * ratio);
mBaseDisplayWidth = mMaxUiWidth;
mBaseDisplayPhysicalXDpi *= ratio;
mBaseDisplayPhysicalYDpi *= ratio;
if (!mIsDensityForced) {
mBaseDisplayDensity = (int) (mBaseDisplayDensity * ratio);
}
}
}
这些 mBaseDisplay* 字段是 DisplayContent 的核心状态。后续 DisplayInfo(每个应用获取到的屏幕信息)就是从这些字段计算而来。
6.2 reconfigureDisplayLocked — 全局生效
// 第 1955-1975 行(简化)
voidreconfigureDisplayLocked(){
if (!isReady()) return;
configureDisplayPolicy(); // 重新配置显示策略
setLayoutNeeded(); // 标记需要重新布局
boolean configChanged = updateOrientation(); // 更新屏幕方向
final Configuration currentDisplayConfig = getConfiguration();
computeScreenConfiguration(mTmpConfiguration); // 重新计算 Configuration
configChanged |= currentDisplayConfig.diff(mTmpConfiguration) != 0;
if (configChanged) {
mWaitingForConfig = true;
// 冻结屏幕、发送新 Configuration 到所有应用进程
mWmService.startFreezingDisplay(0, 0, this);
}
// ...
performLayout(...); // 对所有窗口执行布局
assignWindowLayers(...); // 重新分配窗口层级
}
reconfigureDisplayLocked() 是整个窗口系统重新布局的入口。它做了:
重新计算 Configuration — computeScreenConfiguration()会根据新的mBaseDisplayWidth/Height计算出新的screenWidthDp、screenHeightDp、smallestScreenWidthDp等配置项。广播配置变更 — 如果配置确已变化,WMS 会将新的 Configuration通过ActivityTaskManagerService.updateConfiguration()发送给所有应用进程,触发它们的onConfigurationChanged()回调。窗口重新布局 — performLayout()会对所有可见窗口按照新尺寸重新计算位置和大小。
7、持久化存储设置的分辨率
文件:frameworks/base/services/core/java/com/android/server/wm/DisplayWindowSettings.java
// 第 65-78 行
voidsetForcedSize(DisplayContent displayContent, int width, int height){
// ===== 路径一:Settings.Global(仅默认显示屏) =====
if (displayContent.isDefaultDisplay) {
final String sizeString = (width == 0 || height == 0)
? "" : (width + "," + height); // "1080,1920"
Settings.Global.putString(mService.mContext.getContentResolver(),
Settings.Global.DISPLAY_SIZE_FORCED, sizeString);
}
// ===== 路径二:per-display override XML(所有显示屏) =====
final DisplayInfo displayInfo = displayContent.getDisplayInfo();
final SettingsProvider.SettingsEntry overrideSettings =
mSettingsProvider.getOverrideSettings(displayInfo);
overrideSettings.mForcedWidth = width;
overrideSettings.mForcedHeight = height;
mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
}
8、重启后恢复生效
8.1 BOOT 时序
文件:WindowManagerService.java 第 5076-5094 行
publicvoiddisplayReady(){
synchronized (mGlobalLock) {
if (mMaxUiWidth > 0) {
mRoot.forAllDisplays(dc -> dc.setMaxUiWidth(mMaxUiWidth));
}
// ★ 步骤1:读取 Settings.Global 中的强制属性(仅默认显示屏)
applyForcedPropertiesForDefaultDisplay();
mAnimator.ready();
mDisplayReady = true;
// ★ 步骤2:对所有显示屏应用 display_settings.xml 中的配置
mRoot.forAllDisplays(DisplayContent::reconfigureDisplayLocked);
}
mAtmService.updateConfiguration(null);
}
8.2 applyForcedPropertiesForDefaultDisplay — 从 Settings.Global 读取
// 第 5697-5744 行
privatebooleanapplyForcedPropertiesForDefaultDisplay(){
boolean changed = false;
final DisplayContent displayContent = getDefaultDisplayContentLocked();
// 1. 从 Settings.Global 读取
String sizeStr = Settings.Global.getString(mContext.getContentResolver(),
Settings.Global.DISPLAY_SIZE_FORCED);
// 2. Fallback:如果 Settings 中没有,尝试系统属性
if (sizeStr == null || sizeStr.length() == 0) {
sizeStr = SystemProperties.get("ro.config.size_override", null);
}
// 3. 解析 "1080,1920" 格式
if (sizeStr != null && sizeStr.length() > 0) {
finalint pos = sizeStr.indexOf(',');
if (pos > 0 && sizeStr.lastIndexOf(',') == pos) {
int width = Integer.parseInt(sizeStr.substring(0, pos));
int height = Integer.parseInt(sizeStr.substring(pos + 1));
if (displayContent.mBaseDisplayWidth != width
|| displayContent.mBaseDisplayHeight != height) {
displayContent.updateBaseDisplayMetrics(width, height,
displayContent.mBaseDisplayDensity,
displayContent.mBaseDisplayPhysicalXDpi,
displayContent.mBaseDisplayPhysicalYDpi);
changed = true;
}
}
}
// 4. 同样应用强制 density 和 scaling mode ...
return changed;
}
9、总结干废手机原因及手机厂商应该重视考虑有备案解锁等方案
1、根本原因事故国内各个手机厂商针对wm size命令根本没有进行任何的适配测试,而且锁屏解锁界面没有用安卓原生的,本身安卓aosp是好的,任意尺寸都可以额解锁显示,
2、首次重启后,手机画面无法解锁,adb连接又必须要进行解锁才可以,所以进入死循环。任何保存的数据settings的xml都是在data下面保存,也都是需要解锁才可以操作。
3、建议手机厂商们修改这个手势解锁等情况下,都应该考虑好wm size各个场景,或者要和用户强烈说明利害关系,甚至交互上可以要求用户要移除锁屏密码等情况下才可以wm size操作。
更多fw相关课程,wms,ams,framework程优惠购买成为vip学员进入vip群,积极讨论各种行业难点痛点疑难问题,答疑服务等。
夜雨聆风