乐于分享
好东西不私藏

手机厂商必看危险命令wm size干废安卓手机的原理源码剖析

手机厂商必看危险命令wm size干废安卓手机的原理源码剖析

背景

首先介绍一下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(00this);
    }
// ...
    performLayout(...);   // 对所有窗口执行布局
    assignWindowLayers(...);  // 重新分配窗口层级
}

reconfigureDisplayLocked() 是整个窗口系统重新布局的入口。它做了:

  1. 重新计算 Configuration — computeScreenConfiguration() 会根据新的 mBaseDisplayWidth/Height 计算出新的 screenWidthDpscreenHeightDpsmallestScreenWidthDp 等配置项。
  2. 广播配置变更 — 如果配置确已变化,WMS 会将新的 Configuration 通过 ActivityTaskManagerService.updateConfiguration() 发送给所有应用进程,触发它们的 onConfigurationChanged() 回调。
  3. 窗口重新布局 — 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群,积极讨论各种行业难点痛点疑难问题,答疑服务等。

目前所有专题课程如下:
1、经典fw的入门到精通实战八件套专题
详细课表:
Android Framework开发rom实战合集课表/车载车机手机高级系统开发工程必会技能
重大消息:Hal+perfetto-systrace+SurfaceFlinger合集新专题发布
课程优惠,手把手指导你深入学习FW技术,请联系马哥微信: