ADB获取手机分辨率全攻略:从wm size到dumpsys window的实战解析
1. 从一次UI适配的“灵异事件”说起
最近在搞一个自动化测试脚本,需要根据不同的手机分辨率来动态调整点击坐标。这听起来是个再基础不过的需求,对吧?我一开始也是这么想的,直接上adb shell wm size,大部分手机都乖乖地返回了物理分辨率,比如1080x2400。脚本跑得飞起,直到我遇到了几台“刺头”手机。命令执行了,返回结果了,但内容却是Physical size: 1080x2400后面跟着一个诡异的Override size: 720x1280,或者干脆只返回一个看起来像是虚拟的、更小的分辨率。脚本用这个错误的分辨率去计算点击位置,结果自然是点得牛头不对马嘴,自动化测试直接变成了“自动乱点测试”。
这问题就很有意思了。wm size这个命令,在安卓开发者文档里,它被明确用于获取或设置物理显示的尺寸。那为什么有些手机会返回一个“覆盖(Override)”尺寸,或者一个明显不对的尺寸呢?这背后其实涉及到了安卓系统显示机制的一个关键概念:显示叠加层(Display Overlay)和窗口管理器(WindowManager)的配置。有些厂商,特别是国内一些深度定制的UI(像MIUI、ColorOS等),为了兼容性或者实现某种省电、游戏模式下的分辨率切换功能,会在系统层面设置一个虚拟的、逻辑上的分辨率给应用层。wm size命令在某些版本或特定模式下,获取到的可能就是这一个逻辑分辨率,而非屏幕真实的物理像素。
所以,当你发现adb shell wm size获取的分辨率不对劲,或者脚本在A手机上正常、在B手机上失效时,别急着怀疑人生,这很可能不是你的代码问题,而是你获取分辨率的方法“单一”且“不可靠”。今天,我们就来彻底盘一盘用ADB获取手机分辨率的多种方法,并重点解决那些“获取不到”或“获取不准”的疑难杂症。无论你是做自动化测试、UI适配、爬虫脚本还是玩机搞机,这套组合拳都能让你稳稳地拿到真实的屏幕数据。
2. 核心原理:安卓的“屏幕”到底有几层?
在深入具体命令之前,我们必须先理解为什么会有“获取不到”或“获取不准”的情况。安卓的显示系统并非铁板一块,对于应用和ADB命令来说,“屏幕尺寸”可能意味着不同的东西。
2.1 物理分辨率 vs. 逻辑分辨率
这是最核心的差异点。
- 物理分辨率:这是屏幕硬件实际拥有的像素数量,例如
1080x2400。这是屏幕的绝对物理属性。 - 逻辑分辨率(或应用分辨率):这是系统通过软件(如显示叠加层、窗口管理器策略)呈现给应用程序的“虚拟屏幕”尺寸。系统可能会为了兼容老应用、提升性能(如游戏模式)或实现特定UI效果,将一个
1080x2400的物理屏幕,“伪装”成一个720x1280的逻辑屏幕给应用使用。应用在这个逻辑坐标系下进行布局和绘制,系统再负责将其缩放映射到物理屏幕上。
wm size命令的“不可靠”,正是因为它可能返回的是这个逻辑分辨率,尤其是在厂商修改了系统行为的情况下。
2.2 关键系统服务:dumpsys与WindowManager
dumpsys是一个强大的ADB shell命令,可以转储(dump)几乎所有系统服务(Service)的当前状态信息。其中,window服务(更准确说是WindowManagerService)掌管着所有窗口的显示、层级、尺寸等信息。通过dumpsys window,我们可以窥见系统底层关于屏幕和窗口的真实数据,这通常比wm size更底层、更可靠。
2.3 显示ID与多屏
现代手机可能有多个逻辑显示区域,比如主屏、副屏(某些折叠屏)、投屏等。每个显示区域都有一个唯一的displayId(通常主屏是0)。在获取分辨率时,如果不指定displayId,命令可能默认操作的是主屏,但在某些复杂场景下需要明确指定。
理解了这些背景,我们就能明白,单纯依赖wm size就像只通过猫眼去看房间全貌,可能会被视角限制。我们需要更多的“观测窗口”。
3. 方法一:基础但不可靠的wm size
我们先从最常用的命令开始,并详细分析其局限性和变体。
3.1 标准用法与输出
adb shell wm size典型输出有两种:
- 正常情况:
Physical size: 1080x2400 - 异常情况(存在覆盖):
此时,系统当前生效的可能是Physical size: 1080x2400 Override size: 720x1280720x1280这个覆盖尺寸。很多自动化脚本如果只做简单的字符串分割取第一行或最后一个尺寸,就会在这里栽跟头。
3.2 为什么它会“失灵”?
- 厂商定制:如之前所述,MIUI的游戏加速模式、ColorOS的某些省电场景,可能会动态修改这个覆盖值。
- ADB/WM工具版本:不同安卓版本、不同厂商ROM中的
wm工具行为可能有细微差别。 - 权限问题:虽然
shell用户通常可以执行,但在某些极度阉割的ROM或特殊模式下(如访客模式),命令可能被限制。
3.3 实操中的解析技巧你不能假设输出是固定的格式。一个健壮的解析脚本应该这样处理:
import re import subprocess def get_resolution_via_wm(): try: result = subprocess.check_output(['adb', 'shell', 'wm', 'size'], text=True).strip() # 使用正则匹配所有 “数字x数字” 的模式 matches = re.findall(r'(\d+)x(\d+)', result) if not matches: return None # 通常,最后一个匹配项可能是当前生效的“覆盖尺寸”,第一个是物理尺寸。 # 但更稳妥的策略是:如果有“Override”字样,则取覆盖尺寸;否则取物理尺寸。 if 'Override' in result and len(matches) > 1: # 通常覆盖尺寸在第二行 width, height = matches[-1] # 或者 matches[1] else: width, height = matches[0] return int(width), int(height) except subprocess.CalledProcessError as e: print(f"执行 wm size 失败: {e}") return None这段代码的核心是正则匹配和逻辑判断,而不是简单的字符串切割。它先找出所有可能的分辨率格式,再根据输出文本的语义(是否包含“Override”)来决定选取哪一个,容错性更高。
4. 方法二:深入底层的dumpsys window displays
当wm size不给力时,dumpsys window是我们的首选备用方案。它的输出信息量巨大,我们需要从中提取关键字段。
4.1 命令与原始输出
adb shell dumpsys window displays或者更精简一点,使用grep过滤:
adb shell dumpsys window | grep -E "Display|init|cur|app"一条典型的输出可能如下(经过简化):
WINDOW MANAGER DISPLAY CONTENTS (dumpsys window displays) Display: mDisplayId=0 init=1080x2400 420dpi cur=1080x2400 app=1080x2400 rng=1080x2160-2400x2400这里的几个关键参数:
init:初始的物理分辨率,通常就是屏幕的硬件分辨率。cur:当前系统显示分辨率。在绝大多数未修改的情况下,cur和init相同。如果被修改(如通过wm size 720x1280命令),这里会显示修改后的值。app:应用程序可见区域(Application visible bounds)的尺寸。这是最关键的字段之一。它表示扣除状态栏、导航栏等系统装饰区域后,真正可供应用使用的屏幕空间。对于需要精确点击应用内元素的自动化测试来说,app值比物理分辨率更有用。dpi:屏幕密度,用于像素与物理尺寸(如英寸)的换算。rng:该显示支持的分辨率范围。
4.2 精准解析app区域分辨率对于UI自动化,我们往往更关心app的值。解析脚本需要处理多行、多显示器的复杂情况。
def get_resolution_via_dumpsys(): try: result = subprocess.check_output(['adb', 'shell', 'dumpsys', 'window', 'displays'], text=True) # 方案1:直接查找 “app=” 模式 import re app_pattern = r'app=(\d+)x(\d+)' match = re.search(app_pattern, result) if match: return int(match.group(1)), int(match.group(2)) # 方案2:更健壮地,先定位主显示屏(mDisplayId=0)区块 display_sections = result.split('Display:') for section in display_sections: if 'mDisplayId=0' in section: # 主屏 for line in section.split('\n'): if 'app=' in line: match = re.search(app_pattern, line) if match: return int(match.group(1)), int(match.group(2)) return None except Exception as e: print(f"解析 dumpsys 失败: {e}") return None为什么dumpsys window更可靠?因为它直接查询的是WindowManagerService的内部状态,这个服务管理着所有窗口的合成与显示,其记录的app区域是系统级行为,极少被上层应用或普通设置修改,能真实反映应用窗口的可用画布大小。
4.3 处理多显示屏与折叠屏对于折叠屏设备(如三星 Galaxy Z Fold 系列、华为 Mate X 系列),dumpsys window displays可能会输出多个Display块,对应内屏、外屏等。每个显示都有自己的mDisplayId、init、cur、app值。你的脚本需要能区分当前活跃的显示是哪一个。可以通过adb shell dumpsys window | grep mCurrentFocus或adb shell dumpsys window | grep mTopDisplay等来辅助判断当前焦点窗口所在的显示屏ID。
5. 方法三:直接查询系统属性
安卓系统存储了许多关于硬件和构建信息的属性,我们可以通过getprop命令来读取。虽然分辨率不直接以宽x高的形式存储,但有两个相关属性可供参考。
5.1 查询ro.sf.lcd_density和ro.sf.real_lcd_density
adb shell getprop ro.sf.lcd_density adb shell getprop ro.sf.real_lcd_densitylcd_density是逻辑密度(Logical Density),用于系统UI缩放。real_lcd_density在某些设备上会记录物理密度。但请注意,这只能得到DPI(每英寸像素数),无法直接得到分辨率。需要结合屏幕物理尺寸(英寸)才能换算,而物理尺寸通常很难通过ADB精确获取。因此,这个方法通常不用于直接获取分辨率,但可以作为辅助信息,验证其他方法获取的分辨率与DPI是否匹配一个合理的组合(例如,1080x2400 配合 420dpi 是合理的,但配合 160dpi 就非常奇怪)。
5.2 通过dumpsys display获取更原始的显示信息
adb shell dumpsys display | grep -A5 -B5 "mOverrideDisplayInfo"这个命令会输出DisplayManagerService的信息,其中可能包含mOverrideDisplayInfo,里面记录了覆盖的显示信息。输出非常冗长,但你可以搜索width=和height=字段。解析起来比dumpsys window更复杂,但在某些极端情况下,它可能提供另一条数据路径。
6. 实战:构建一个健壮的多策略获取函数
在实际项目中,我们不能把鸡蛋放在一个篮子里。一个工业级的分辨率获取函数,应该实现多策略回退(fallback)机制。
6.1 设计思路
- 优先策略:使用
dumpsys window displays解析app区域尺寸。因为它最贴近应用实际可用区域,且受厂商干扰较小。 - 备用策略一:解析
dumpsys window displays中的init或cur作为物理或当前系统分辨率。 - 备用策略二:解析
wm size,并智能处理Override情况。 - 备用策略三:尝试
dumpsys display。 - 验证与纠错:对获取到的宽高进行合理性验证(例如,是否为正整数,是否在常见范围如480~4000之间)。如果可能,将不同方法获取的结果进行交叉比对,如果差异巨大,则记录警告日志。
6.2 代码实现示例
import re import subprocess import logging logging.basicConfig(level=logging.INFO) def get_phone_resolution_robust(): """ 健壮地获取手机分辨率,优先返回应用可见区域(app),其次物理分辨率。 返回 (width, height) 元组,失败返回 (None, None) """ strategies = [ _get_via_dumpsys_window_app, _get_via_dumpsys_window_init, _get_via_wm_size, _get_via_dumpsys_display, ] for strategy in strategies: width, height = strategy() if _validate_resolution(width, height): logging.info(f"通过 {strategy.__name__} 获取到分辨率: {width}x{height}") return width, height else: logging.warning(f"策略 {strategy.__name__} 未获取到有效分辨率") logging.error("所有策略均失败,无法获取分辨率") return None, None def _validate_resolution(width, height): """简单验证分辨率合理性""" if not isinstance(width, int) or not isinstance(height, int): return False if width <= 0 or height <= 0: return False # 简单范围校验,现代手机分辨率通常在此区间 if not (480 <= width <= 4000 and 800 <= height <= 4000): logging.warning(f"获取到的分辨率 {width}x{height} 超出常见范围,可能异常") # 即使超出范围,也先返回,但打警告。有时折叠屏等设备可能确实有特殊分辨率。 return True def _get_via_dumpsys_window_app(): """策略1: 从 dumpsys window displays 获取 app 区域""" try: output = subprocess.check_output(['adb', 'shell', 'dumpsys', 'window', 'displays'], text=True, stderr=subprocess.DEVNULL) # 优化正则,更精确匹配 app=WxH 模式 pattern = r'app[=:](\d+)x(\d+)' match = re.search(pattern, output) if match: return int(match.group(1)), int(match.group(2)) except Exception: pass return None, None def _get_via_dumpsys_window_init(): """策略2: 从 dumpsys window displays 获取 init 物理分辨率""" try: output = subprocess.check_output(['adb', 'shell', 'dumpsys', 'window', 'displays'], text=True, stderr=subprocess.DEVNULL) # 匹配 init=WxH, cur=WxH 作为备选 for field in ['init', 'cur']: pattern = rf'{field}[=:](\d+)x(\d+)' match = re.search(pattern, output) if match: return int(match.group(1)), int(match.group(2)) except Exception: pass return None, None def _get_via_wm_size(): """策略3: 解析 wm size 命令,处理 Override""" try: output = subprocess.check_output(['adb', 'shell', 'wm', 'size'], text=True, stderr=subprocess.DEVNULL).strip() matches = re.findall(r'(\d+)x(\d+)', output) if matches: # 逻辑:如果有Override行,且匹配到两个尺寸,优先取最后一个(通常是override) lines = output.split('\n') if len(lines) > 1 and 'Override' in lines[1] and len(matches) > 1: return int(matches[-1][0]), int(matches[-1][1]) else: # 否则取第一个匹配(物理尺寸) return int(matches[0][0]), int(matches[0][1]) except Exception: pass return None, None def _get_via_dumpsys_display(): """策略4: 从 dumpsys display 中尝试挖掘""" try: output = subprocess.check_output(['adb', 'shell', 'dumpsys', 'display'], text=True, stderr=subprocess.DEVNULL) # 寻找类似 width=1080 height=2400 的字段 pattern = r'width[=:](\d+).*?height[=:](\d+)' match = re.search(pattern, output, re.DOTALL) # DOTALL 让 . 匹配换行 if match: return int(match.group(1)), int(match.group(2)) except Exception: pass return None, None # 使用示例 if __name__ == "__main__": w, h = get_phone_resolution_robust() if w and h: print(f"最终获取到的分辨率: {w}x{h}") else: print("获取失败")这个函数集成了多种获取方式,并按可靠性排序尝试。_validate_resolution函数提供了基本的数据清洗。在实际使用中,你还可以根据日志,统计不同策略在不同设备上的成功率,从而动态调整策略优先级。
7. 疑难杂症与深度排坑
即使有了多策略回退,在实际环境中你仍可能遇到各种奇怪的问题。下面是一些典型场景和解决方案。
7.1 ADB连接正常,但命令返回空或权限错误
- 现象:
adb devices列表中有设备,但执行adb shell wm size或adb shell dumpsys window返回空、报Permission denied或Access denied。 - 排查:
- 用户权限:确保手机已开启“USB调试(安全设置)”或“允许通过USB调试修改权限”。有些设备需要额外开启这个开关,ADB shell 才能拥有足够的权限执行
dumpsys等命令。 - ADB版本:使用
adb version检查你的ADB工具版本。过旧的ADB可能无法兼容新版本安卓的安全协议或命令。建议使用Android SDK Platform-Tools中的最新版本。 - 设备状态:确认设备屏幕已解锁。部分设备在锁屏状态下会限制某些shell命令。
- 多用户/多空间:如果设备开启了“多用户”或“隐私空间”,ADB连接可能默认进入的是主用户空间。尝试切换到有权限的用户:
adb shell am switch-user <user_id>(通常主用户id是0)。
- 用户权限:确保手机已开启“USB调试(安全设置)”或“允许通过USB调试修改权限”。有些设备需要额外开启这个开关,ADB shell 才能拥有足够的权限执行
7.2 获取到的分辨率明显错误(如 0x0 或极小值)
- 现象:命令有输出,但解析出来的宽高是 0x0 或像 100x100 这样明显不合理的值。
- 排查:
- 命令输出格式解析错误:这是最常见的原因。用
adb shell wm size和adb shell dumpsys window displays直接查看原始输出,检查你的正则表达式或字符串解析逻辑是否能正确匹配。不同厂商、不同安卓版本的输出格式可能有细微差别(例如空格、冒号、等号的使用)。 - 设备处于特殊模式:例如,手机正连接着电脑处于“文件传输”模式,或者处于“安全模式”。尝试切换到“仅充电”模式,并重启ADB服务 (
adb kill-server && adb start-server)。 - 显示服务未就绪:在设备刚启动或屏幕刚点亮时,显示服务可能还未完全初始化。可以在命令前加一个短暂的延时,或者重试几次。
- 命令输出格式解析错误:这是最常见的原因。用
7.3 针对特定品牌/ROM的特别处理
- 华为/荣耀:部分旧款机型或特定系统版本,可能需要开启“仅充电”模式下的“HDB”调试(在开发者选项里找),或者关闭“监控ADB安装应用”选项。华为手机开启USB调试时弹出的华为账号验证,必须登录验证后才能使用完整的ADB功能。
- 小米/Redmi (MIUI):注意“开发者选项”中的“USB调试(安全设置)”必须打开。MIUI的游戏加速模式会修改分辨率,获取到的可能是游戏模式下的逻辑分辨率。如果需要物理分辨率,可以尝试退出游戏模式后再获取。
- OPPO/一加 (ColorOS):类似MIUI,也可能有性能模式或游戏空间会干预分辨率。同时,确保“USB调试”和“禁止权限监控”等选项已开启。
- 云手机/模拟器:如MuMu模拟器、夜神模拟器等,它们通常有固定的ADB端口(如MuMu的7555,夜神的62001)。你需要使用
adb connect 127.0.0.1:7555来连接。获取到的分辨率就是模拟器窗口设置的分辨率。如果连接被拒绝,检查模拟器设置中ADB调试是否开启,以及防火墙是否阻挡了对应端口。
7.4 自动化脚本中的最佳实践
- 增加重试与超时机制:网络波动或设备响应慢可能导致命令失败。封装你的ADB命令执行函数,加入重试逻辑(如最多3次)和超时设置。
import subprocess import time def adb_shell_with_retry(cmd, max_retries=3, timeout=10): for i in range(max_retries): try: result = subprocess.check_output(['adb', 'shell'] + cmd.split(), stderr=subprocess.PIPE, text=True, timeout=timeout) return result.strip() except subprocess.TimeoutExpired: logging.warning(f"命令 {cmd} 超时,重试 {i+1}/{max_retries}") time.sleep(1) except subprocess.CalledProcessError as e: logging.error(f"命令 {cmd} 执行失败: {e.stderr}") break # 如果是权限错误,重试可能无用 return None - 缓存结果:分辨率在单次会话中通常不会改变。对于需要多次调用分辨率的脚本,可以在首次成功获取后将其缓存起来,避免频繁执行ADB命令,提高效率。
- 环境检查:在脚本开始执行关键任务前,先做一次简单的环境检查:
adb devices是否有授权设备?adb shell echo test是否能正常执行?这可以提前发现连接问题。
8. 扩展应用:分辨率数据的实际使用场景
获取分辨率不是目的,利用它解决问题才是。这里分享两个我实际项目中深度依赖分辨率数据的场景。
8.1 自动化测试中的坐标换算这是最直接的应用。假设你的测试脚本需要点击屏幕正中央。如果你只知道物理分辨率是1080x2400,直接计算(540, 1200)并点击,在带有刘海屏、挖孔屏或虚拟导航栏的设备上,很可能点到了状态栏或导航栏上。 正确的做法是使用从dumpsys window displays获取的app区域分辨率。假设app=1080x2274,这意味着屏幕顶部状态栏占用了2400-2274=126像素的高度(可能包含刘海区域),底部导航栏可能占用了另外一些高度(如果存在)。那么应用区域的正中央坐标就是(1080/2, 2274/2) = (540, 1137)。在这个坐标系下点击,才能精准命中应用内的元素。 更进一步,你还需要结合adb shell dumpsys window | grep mCurrentFocus获取当前活动窗口的Surface信息,有时窗口可能还不是全屏的(比如有弹窗),这就需要更精细的坐标计算。
8.2 多设备兼容性测试与截图分析在做UI兼容性测试时,我们需要在不同分辨率的设备上运行同一套测试用例。获取到的分辨率可以作为关键元数据,与测试截图、日志一起保存。当某个用例在特定分辨率下失败时,可以快速定位是否是分辨率相关的布局问题(如小屏设备元素重叠、大屏设备布局错乱)。 我们可以写一个简单的脚本,在测试开始前获取设备分辨率、DPI、型号等信息,并生成一份测试环境报告。
def collect_device_info(): info = {} # 获取分辨率 info['resolution_app'] = _get_via_dumpsys_window_app() info['resolution_physical'] = _get_via_dumpsys_window_init() # 获取DPI try: dpi_output = subprocess.check_output(['adb', 'shell', 'getprop', 'ro.sf.lcd_density'], text=True).strip() info['dpi'] = int(dpi_output) if dpi_output.isdigit() else None except: info['dpi'] = None # 获取设备型号 try: info['model'] = subprocess.check_output(['adb', 'shell', 'getprop', 'ro.product.model'], text=True).strip() except: info['model'] = 'Unknown' return info这份报告对于后续的问题复现和数据分析至关重要。
8.3 与dumpsys window其他信息联用dumpsys window是一个宝库。除了分辨率,你还可以获取:
- 屏幕方向:
adb shell dumpsys window | grep mRotation。这对于横屏游戏或应用的测试很重要。 - 窗口层级与焦点:
adb shell dumpsys window windows。可以查看所有窗口的层级关系、包名、是否可见等,对于判断当前哪个应用在前台、是否有弹窗遮挡等场景非常有用。 - 输入事件:结合
getevent或input命令,可以在已知的坐标体系下模拟精确的触控、滑动操作。
掌握多种分辨率获取方法并理解其背后的原理,远不止于解决一个“获取不到”的报错。它让你对安卓设备的显示系统有了更深的掌控力,是编写稳定、可靠的移动端自动化脚本和进行深度系统调试的基石。下次再遇到分辨率问题时,希望这套组合拳能帮你快速定位,游刃有余。