调安卓应用的时候,很多人的操作习惯是左手搭在电脑键盘上,右手握着鼠标,眼睛盯着 Android 模拟器 AVD 的窗口。结果一敲回车没反应,按 Backspace 退格退不了,想用方向键在列表里挪一格,模拟器纹丝不动。更别扭的是想模拟个返回、电源、音量,还得把鼠标挪到模拟器右侧那条竖排物理按键上一个个点。这篇文章就聊透两件事:电脑键盘的按键怎么被 Android 模拟器 AVD 正确接收,以及模拟器上的物理按键(侧边栏那几个、以及通过 HID 设备接进来的真实按键)怎么被准确触发。内容包括 AVD 配置里那几个真正管键盘的参数、宿主机键位到 Android 按键的映射关系、用 adb 把"按键"这件事脚本化的完整路子,以及我这些年踩过的坑。不管你刚装好模拟器第一次跑 Demo,还是已经在写自动化脚本批量点按钮,下面的内容都能直接抄。
1. 先把问题说清楚:敲键盘为什么没反应
1.1 三种"输入"其实是三条完全不同的链路
很多人把"在模拟器里打字"当成一件理所当然的事,但它在底层其实分成了三条互不相干的链路,任何一条断了,表现都是"键盘没反应",而你得修的地方却完全不同。
第一条是宿主机键盘事件到模拟器窗口这一层。电脑上按下的键先被操作系统捕获,再交给模拟器进程,模拟器进程判定这个键应该映射成 Android 的哪个KEYCODE_xxx。这一层管的是"模拟器认不认这块键盘"。
第二条是Android 系统内部从按键到界面这一层。事件进了 Android 之后,要经过输入法框架(IME)和窗口焦点分发,才能落到某个控件上。这一层管的是"这个按键应该交给谁"。如果你的焦点在一个 EditText 上,按键会先被输入法截走,看起来像"按键没生效",其实是生效了但走的不是你以为的那条路。
第三条是物理按键与系统按键这一层,比如 Power、Volume、Back。这一类按键在 Android 里是"系统键",由系统的 PhoneWindowManager 之类的组件优先拦截,压根不会分发给普通应用。你按对了键位但应用没接收到,不代表按键没生效。
排查口诀:先在模拟器里打开一个纯文本输入框敲字母,能出字说明第一层通;再按返回看界面退不退,能退说明第二层和系统键链路通。两件事分开验证,比一上来就翻配置文件高效得多。
我见过不少人的"键盘失灵"其实是第一层就没通:AVD 在创建时默认把硬件键盘关掉了,模拟器窗口收到的键位事件被直接丢弃,所以你敲什么都没反应,但鼠标点击完全正常,这个特征非常好辨认。
1.2 从现象反推:三步锁定问题层
与其漫无目的地翻配置,不如用现象反推。下面这张表是我自己排查时用的顺序,基本三步之内能定位到具体哪一层出了问题。
| 现象 | 最可能的原因层 | 优先验证动作 |
|---|---|---|
| 敲字母、数字完全无反应,鼠标正常 | 宿主机到模拟器 | 检查 AVD 的hw.keyboard是否为yes |
| 能敲英文,但中文打不出来、候选框不弹 | Android 输入法框架 | 检查模拟器内是否装了中文输入法并设为当前 IME |
| 字母能打,退格键、方向键、Esc 无反应 | 键位映射 | 换键盘布局测试,看是否被系统快捷键截走 |
| 返回键能退,但应用内监听不到返回 | 系统键分发 | 改用onBackPressed/手势回调或 adb 事件验证 |
| 侧边栏点 Power 有效,adb 发 KEYCODE_POWER 无效 | 屏幕状态机 | 检查屏幕是否已亮、是否被锁屏挡住 |
| 键盘偶尔灵偶尔不灵 | 焦点/输入法抢占 | 检查当前焦点窗口与 IME 状态 |
这张表里的第二行是最容易被忽略的:很多人以为"模拟器不支持中文",实际上模拟器对中文的支持完全取决于你装没装输入法。AVD 的系统镜像里默认只有一个英文输入法,你拿它当然打不出中文。这是另一个话题,后面会单独说。
第三行的情况在 macOS 上特别多。Mac 的 Command 键被系统拿去做全局快捷键了,模拟器的一部分组合键又恰好和系统快捷键撞车,于是你按下去是系统响应,应用毫无动静。换台 Windows 机器一试就正常,很多人因此误判成"模拟器在 Mac 上有 Bug"。
2. AVD 侧的配置:让模拟器真正认下硬件键盘
2.1 config.ini 里真正管键盘的那几个参数
AVD 的所有硬件特性都写在一个叫config.ini的纯文本文件里,位置固定在用户目录下的 AVD 目录中。这个文件才是模拟器行为的最终依据,图形界面上那些勾选项,本质只是这个文件的另一个编辑器。
路径分布是这样的:
- Linux / macOS:
~/.android/avd/<你的AVD名字>.avd/config.ini - Windows:
C:\Users\<你的用户名>\.android\avd\<你的AVD名字>.avd\config.ini - 如果你设过
ANDROID_AVD_HOME环境变量,那就去那个变量指向的目录下找
和键盘、物理按键最相关的参数其实就那么几个,我整理成了下面这张表,后面会逐条说清楚它们的实际效果。
| 参数 | 常用取值 | 实际作用 | 我的建议 |
|---|---|---|---|
hw.keyboard | yes/no | 是否存在硬件键盘设备 | 一律设yes,这是本文的前提 |
hw.mainKeys | yes/no | 是否有物理主按键(Home/Back/Menu) | 建议no,把导航交给系统手势或软件导航栏 |
hw.dPad | yes/no | 是否有物理方向键 | 测试遥控器/车机类应用时设yes |
hw.trackBall | yes/no | 是否有轨迹球 | 基本用不上,保持no |
hw.rotaryInput? | — | 部分新版本支持的旋钮输入 | 按镜像版本决定,不确定就别写 |
hw.lcd.width/hw.lcd.height | 如1080/2400 | 屏幕分辨率 | 改分辨率比改 DPI 更直观 |
hw.lcd.density | 如420 | 屏幕密度 | 影响布局,配合分辨率一起调 |
hw.gpu.enabled | yes/no | 是否启用 GPU 加速 | 建议开启,否则动画卡顿影响手感 |
hw.keyboard是这篇文章的主角。它设成no的时候,Android 系统会认为这台设备没有物理键盘,于是所有来自宿主机键盘的输入事件在框架层就被过滤掉了——注意,是"认为没有",不是"驱动坏了"。这也解释了一个很典型的现象:模拟器里软键盘弹出来,你用电脑键盘敲字,输入框毫无反应,但鼠标点软键盘上的字母就能输进去。因为软键盘走的是触摸事件,从头到尾不经过硬件键盘那条链路。
改法很直接,用文本编辑器打开config.ini,找到hw.keyboard这一行,改成yes;如果没有这一行,直接在文件末尾另起一行补上hw.keyboard=yes即可。改完必须完整关闭并重启模拟器,热重载不生效,因为硬件信息是在虚拟机启动时一次性挂载的。
2.2 修改配置的两种姿势与生效验证
改配置有两条路:图形界面和直接编辑文件。图形界面在 AVD 管理器里点编辑,找到 "Show Advanced Settings" 或者类似的高级选项,在 Hardware 那一栏里把 Keyboard 的存在与否勾上。这条路适合不想碰文件的人,但有个小缺点:不同版本的界面文案会有微调,找起来费劲。
直接改文件这条路我更推荐,因为快且可控。下面是完整操作流程,以 Linux/macOS 为例:
# 1. 先确认你的 AVD 名字 emulator -list-avds # 2. 看一眼当前配置里键盘相关项 grep -E "hw.keyboard|hw.mainKeys|hw.dPad" ~/.android/avd/<AVD名字>.avd/config.ini # 3. 备份一份再改(养成习惯,改坏了能一分钟回滚) cp ~/.android/avd/<AVD名字>.avd/config.ini ~/.android/avd/<AVD名字>.avd/config.ini.bak # 4. 用 sed 直接改,没有这一行就追加 sed -i '/hw.keyboard/d' ~/.android/avd/<AVD名字>.avd/config.ini echo "hw.keyboard=yes" >> ~/.android/avd/<AVD名字>.avd/config.ini # 5. 从命令行冷启动,避开快照恢复带来的旧状态 emulator -avd <AVD名字> -no-snapshot-load这里有个细节值得说:-no-snapshot-load这个参数不是可有可无的。模拟器默认会从上次的快照快速恢复,而快照里保存的是"上一台虚拟机"的硬件状态,你改了 config.ini 但直接快照恢复,很可能还是旧配置。我最早折腾的时候就栽在这上面,改了三四遍文件以为没生效,后来加了冷启动参数一次就好了。
验证是否真的生效,最靠谱的办法是进系统看设备列表,而不是凭手感:
# 查看 Android 系统识别到的输入设备,能看到键盘设备就说明挂载成功了 adb shell dumpsys input | grep -i -A2 "keyboard" # 更直观的方式:抓一下按键事件流,按键盘上任意键,看有没有输出 adb shell getevent -l第二种方式最直观。getevent -l会把所有输入事件打出来,你按一下电脑键盘的 A 键,如果终端里刷出一行形如/dev/input/eventX: EV_KEY KEY_A DOWN的内容,说明从宿主机到 Android 内核这条链路是通的。如果一片安静,那说明事件压根没进内核,问题一定在第一层,继续回去查hw.keyboard。
顺带说一句,getevent的输出会非常多,触摸屏的坐标事件也在里面刷。想只看按键,可以用adb shell getevent -l /dev/input/eventX指定具体设备,设备号从adb shell getevent -lp的列表里找,列表里会标注设备名和能力位,找名字里带 keyboard 的那个就行。
2.3 输入法层面的坑:软键盘与硬键盘的争夺
把hw.keyboard打开之后,下一个高频问题来了:键盘能打英文字母了,但一进输入框,软键盘弹出来,物理键盘敲字没反应;或者反过来,物理键盘好用,软键盘死活不弹。这属于第二层的问题,跟 AVD 配置没关系了,是输入法框架在决定"要不要用硬件键盘"。
Android 里有个概念叫硬件键盘的 IME 策略。简单说,当一个物理键盘存在时,系统会倾向隐藏软键盘,因为用户显然有更好的输入方式。这个行为在某些定制界面上表现得很怪,比如你点开输入框软键盘不弹,光标在那儿闪但打不了中文。
排查思路很直接:先确认当前激活的输入法是谁。
# 列出系统里所有可用的输入法 adb shell ime list -s # 查看当前激活的是哪个 adb shell settings get secure default_input_method # 强制切换某个输入法(把包名换成列表里实际的) adb shell ime set com.example.ime/.SomeImeService关于中文输入,说个实在话:模拟器自带镜像里通常只有一个英文输入法,你想要中文,就得自己装一个。装完之后记得在设置里切过去,或者用上面的命令直接切。这一步做完,电脑键盘敲拼音出候选框是正常的,不需要额外配置。
还有一个反直觉的现象:有的人打开了硬件键盘,结果软键盘反而不弹了,想用鼠标点虚拟按键都没机会。这是因为系统认为你有物理键盘,就把软键盘收起来了。解决办法是在系统的语言与输入法设置里,找到物理键盘相关选项,把"显示虚拟键盘"打开。不同版本的入口位置不一样,一般在设置里的系统 → 语言和输入法 → 物理键盘下面。这个开关不在 AVD 配置里,也不在输入法设置里,藏得比较深,第一次找会花点时间。
实操心得:如果你做的是需要频繁切换中英文的测试,建议在模拟器里装两个输入法,一个英文一个中文,用 adb 脚本
ime set来回切,比手动点设置快十倍。写自动化脚本的时候这一步几乎必做,因为输入法状态是不确定的,一旦跑在中文输入法上,input text发英文也会被吃成候选词。
3. 宿主机键位到 Android 按键的映射关系
3.1 常用映射速查表
硬件键盘挂上之后,模拟器会按照一套内置规则把宿主机键位翻译成 Android 的按键码。大部分映射是符合直觉的:字母键就是字母键,回车是KEYCODE_ENTER,方向键是四个方向。但也有几个特殊映射,不查一下真的想不到。
| 电脑键盘 | Android 侧效果 | 说明 |
|---|---|---|
| 字母 / 数字键 | 对应字符输入 | 最常见,直接可用 |
| 方向键 ↑↓←→ | D-pad 上下左右 | 列表导航、游戏方向键都靠它 |
| Esc | 返回(BACK) | 我个人的高频键,比找侧边栏快 |
| Home 键 | 回到桌面(HOME) | 部分窗口管理器会截走 |
| 回车 | 确认(ENTER) | 输入框内是换行,列表里是选中 |
| Backspace | 删除(DEL) | 输入框内表现为退格 |
| Tab | 焦点切换 | 表单类测试非常好用 |
| Page Up / Page Down | 上下滚动 | 长列表翻页 |
| Ctrl + 方向键 | 屏幕横竖旋转(部分版本) | 版本差异较大,以本机 Help 清单为准 |
| Alt + Enter | 全屏切换 | Windows / Linux 平台 |
| Ctrl + Shift + 某些键 | 截图、快照等辅助功能 | 建议打开模拟器 Help 菜单确认 |
关于最后一行的组合键,我要提醒一句:模拟器的快捷键在不同版本、不同平台上调整过不止一次。网上流传的很多"快捷键大全"其实是某个旧版本的东西,照着按未必有用。最稳妥的查法是打开模拟器窗口的菜单,找 Keyboard Shortcuts(有的版本叫 Help → Shortcuts),那里列的是你这台机器上这一版的真实映射,以它为准。
注意:如果你看到某个组合键在官方清单里有、自己按了却没反应,优先怀疑被宿主机抢了。Windows 的输入法切换、macOS 的 Mission Control、Linux 桌面环境的窗口管理器,都会抢占一部分组合键,模拟器根本没收到。
3.2 组合键、长按与"模拟不了"的边界
这里必须讲清楚一个边界,否则你会在这上面浪费大量时间:通过宿主键盘直接按出的组合键,和真正的多指触摸不是一回事。
键盘上的 Ctrl+A、Shift+方向这类组合,走的是键盘事件里的修饰键(modifier)机制,Android 侧接收到的是带 metaState 的按键事件,应用能正常识别成"复制全选""按住 Shift 做范围选择"。这个没问题。
但是像"两根手指同时按屏幕的两个位置"这种多点触控,键盘是模拟不出来的。有些游戏和地图应用需要双指缩放,你在电脑上按 Ctrl+滚轮,模拟器未必映射成缩放;按 Ctrl+加号减号也未必。真要测双指手势,还是老老实实用鼠标操作——模拟器支持按住 Ctrl 或 Alt 再用鼠标点击来产生"两只手指",具体哪个修饰键取决于版本,一般在模拟器设置里能看到提示。
长按也是同理。键盘按住一个键不放,模拟器确实会连发重复事件,但这和"触摸屏长按 800 毫秒"是两码事。前者是按键重复(key repeat),后者是触摸手势。如果你的应用逻辑里区分了这两种,测试时必须用鼠标按住或者 adb 发手势,不能靠键盘按住来凑。
至于"完全模拟不了的",主要是这几类:电源键长按呼出关机菜单(侧边栏能点,键盘映射不了)、三指截屏、物理音量键的硬件级耳放触发、以及部分厂商定制的组合键。前者用侧边栏或 adb,后者在原生镜像里本来就没有。
3.3 跨平台差异:Windows、macOS、Linux 各踩一遍
同一套 AVD 配置,换个宿主机操作系统,键盘手感可能完全不一样,这点我深有体会。三平台的主要差异点集中在修饰键和全局快捷键冲突上。
Windows 上最典型的问题是输入法。中文系统默认开着微软拼音,按下 Shift 会切换中英文状态,模拟器收到的可能就是一个奇怪的修饰状态,你以为是模拟器的问题,其实是宿主机输入法在吞键。测试类工作我一般建议临时把宿主机切到英文输入法状态,跑完再切回来。
macOS 上最典型的是 Command 键。Command 是系统级修饰键,很多全局快捷键都绑在它上面,模拟器的大部分辅助快捷键在 Mac 上会被换成 Ctrl 或 Cmd 的组合,而这个组合恰好又容易和系统功能撞车。如果你发现某个组合键在 Mac 上无论如何都不生效,先去系统设置里看快捷键有没有占用它,九成是这个原因。
Linux 上最典型的是键盘布局。部分发行版默认布局不是美式 QWERTY,而是带变体的布局,于是你在模拟器里敲出来的字符和键盘上印的对不上,比如按 Y 出来 Z。这个问题跟模拟器一点关系都没有,是布局层面的错位。查一下当前布局:
# 查看当前键盘布局 localectl status # 临时切到美式布局验证 setxkbmap usWindows 也有类似的布局问题,只是藏得更深。如果你用的是非美式布局(比如德语、法语键盘),config.ini里还应该显式声明键盘布局,否则模拟器按美式来处理,特殊符号全乱。相关参数在 config.ini 里通常是hw.keyboard.lid(盖子,笔记本用)和布局相关的条目,但更稳的做法是从模拟器启动参数里指定,具体参数名以你本机emulator -help的输出为准,别照抄网上的旧参数名。
我在跨平台切换时养成一个习惯:新机器上第一次跑模拟器,先花两分钟做三件事——敲字母看能不能出字、按 Esc 看能不能返回、按方向键看列表动不动。三个都过,再开始正式调试,省得后面把环境问题误判成代码问题。
4. 用 adb 把"按键"这件事脚本化
4.1 keyevent 常用键值表
前面讲的都是"手动按",一旦进入自动化或者要精确复现某个操作序列,就得切换到 adb 这条线。adb shell input keyevent是最基础也最常用的命令,它直接往 Android 的输入系统注入一个按键事件,绕过了宿主机和模拟器窗口的映射层,所以即使hw.keyboard是no,这条命令照样有效。这个特性非常有用:它意味着你可以把按键操作和模拟器配置解耦。
下面这张表是我日常用得最多的一批键值,按使用频率排的。
| 键值常量 | 数值 | 典型用途 |
|---|---|---|
KEYCODE_HOME | 3 | 回桌面 |
KEYCODE_BACK | 4 | 返回(新版本会映射成返回手势) |
KEYCODE_DPAD_UP/DOWN/LEFT/RIGHT | 19/20/21/22 | 焦点移动 |
KEYCODE_DPAD_CENTER | 23 | 选中当前焦点 |
KEYCODE_VOLUME_UP/VOLUME_DOWN | 24 / 25 | 音量调节 |
KEYCODE_POWER | 26 | 电源键(点亮/熄灭屏幕) |
KEYCODE_CAMERA | 27 | 相机快门 |
KEYCODE_A到KEYCODE_Z | 29 到 54 | 字母输入 |
KEYCODE_0到KEYCODE_9 | 7 到 16 | 数字输入 |
KEYCODE_TAB | 61 | 焦点切换 |
KEYCODE_SPACE | 62 | 空格 |
KEYCODE_ENTER | 66 | 确认 / 换行 |
KEYCODE_DEL | 67 | 退格删除 |
KEYCODE_MENU | 82 | 菜单 |
KEYCODE_NOTIFICATION | 83 | 下拉通知栏 |
KEYCODE_SEARCH | 84 | 搜索 |
KEYCODE_MEDIA_PLAY_PAUSE | 85 | 媒体播放控制 |
KEYCODE_APP_SWITCH | 187 | 多任务界面 |
用法有两种,写常量或者写数值都行,实测效果完全一样:
# 写法一:常量名,可读性好,推荐 adb shell input keyevent KEYCODE_BACK # 写法二:数值,脚本里短一些 adb shell input keyevent 4 # 一次发多个键,按顺序执行 adb shell input keyevent KEYCODE_HOME KEYCODE_HOME # 长按(部分版本支持 --longpress 参数) adb shell input keyevent --longpress KEYCODE_POWER--longpress这个参数值得说一句。它在不同 Android 版本上的支持程度不统一,老版本会直接报参数错误。如果报错了,用下面的替代方案:连着发两次同一个键,两次之间间隔很短,系统会将其识别为双击;或者干脆改用触摸手势的长按(后面会讲)。我一般不去赌--longpress的兼容性,写脚本时优先用更稳的方式。
4.2 input text 的转义与中文方案
input text是往当前焦点输入框里塞文本的命令,看着简单,坑却不少。第一次用的人基本都会遇到三个问题:空格不见了、特殊字符报错、中文变问号。
空格的问题最常见。input text "hello world"里的空格会被 shell 提前拆开,实际传进去的是 "hello","world" 被当成了额外的命令参数,于是只有 hello 进去了。正确写法是用%s表示空格:
# 错误:空格会被 shell 吃掉 adb shell input text "hello world" # 正确:用 %s 代替空格 adb shell input text "hello%sworld"特殊字符是第二个坑。&、<、>、(、)、|这些在 shell 里有特殊含义,直接传会被解释成重定向或子命令,轻则报错重则执行了奇怪的命令。稳妥做法是全程加引号,并且对 shell 元字符做转义:
# 输入包含特殊符号的字符串 adb shell input text 'a\&b\(c\)'第三个坑就是中文。input text走的是按键注入通道,它对非 ASCII 字符的支持非常差,实际表现是中文全部丢失或者变成问号。这一点是命令本身的限制,不是配置问题,改什么参数都没用。
那中文怎么自动化输入?社区里的成熟做法是装一个专门用于注入文本的输入法:这种输入法不显示界面,而是通过广播接收待输入的文本,再作为输入法自身把文本提交给焦点控件。流程大致是这样:
- 在模拟器里安装这个输入法,并在设置里把它设为当前输入法
- 脚本里通过广播把待输入的字符串发给它
- 它接收到之后,作为 IME 直接提交文本,中文、emoji、特殊符号都没问题
这个方案的好处是彻底绕开按键通道,直接用输入法的提交能力。缺点是得先装东西,而且每次跑之前要确认当前 IME 是它,否则广播没人接。我的脚本里通常会先ime set一次做保险,跑完再切回原来的输入法,避免影响后续手工测试。
提示:如果你的场景只需要输入英文和数字,别折腾输入法,老老实实用
input text加%s就够了,稳定性最好。中文自动化再上输入法方案,投入产出比才对。
还有一个折中办法:把文本先写进模拟器里的一个临时文件,再用am broadcast或者剪贴板方案粘贴。这个路子适合批量长文本,但实现比输入法方案麻烦,我只在特殊场景用过。
4.3 长按、滑动与连续按键的脚本化
单纯发按键只能覆盖一部分场景,真实测试里更多是"按住不放""拖一段距离""连续快速点"。这些命令都在input下面,我把常用的几个整理在一起。
# 单击某个坐标 adb shell input tap 540 1200 # 滑动(从一点拖到另一点,最后是持续时间毫秒) adb shell input swipe 540 1800 540 600 300 # 长按:起止点相同,持续时间拉长,这就是最可靠的长按实现 adb shell input swipe 540 1200 540 1200 1000 # 连续点击 5 次(用循环实现,两次之间留 100ms) for i in $(seq 1 5); do adb shell input tap 540 1200 sleep 0.1 done # 滚动轨迹球(部分场景下等同于方向滚动) adb shell input roll 0 3长按那个写法我要重点推荐。input swipe的起止点相同时,Android 侧收到的就是按下、保持、抬起三个事件,中间的持续时间完全可控,落到应用里就是标准的onLongClick或ACTION_DOWN加延时加ACTION_UP。这比赌--longpress参数兼容性可靠太多了,我在各种版本的镜像上都验证过,行为一致。
组合键的脚本化稍微麻烦一点。input keyevent一次只处理一个按键序列,如果你的应用需要"Shift 按住 + 按方向键"这种真正的组合,命令行层面没有直接支持的参数。可行的路子有两条:一条是进到 evdev 层直接写事件(下一节讲),另一条是在应用层想办法,比如用测试框架的按键 API 代替 adb。日常测试里,需要组合键的场景其实不多,大部分是"音量上 + 电源"这种系统级组合,用侧边栏点两下更快。
4.4 getevent 与 sendevent:直接操作输入子系统
前面所有命令都工作在 Android 框架层。如果框架层的命令满足不了你,比如要模拟一个系统根本不认识的按键、要产生原始的多点触控序列,那就得下到 Linux 输入子系统这一层。这条路线威力大,但也最容易把设备搞乱,用之前务必心里有数。
第一步是找设备。getevent -lp会列出所有输入设备及其能力位:
adb shell getevent -lp输出里你会看到一堆/dev/input/eventX,每个下面有设备名、支持的按键类型、坐标范围。找名字带 keyboard 的那个,记下设备节点。然后抓一次真实按键的事件流,看清事件码:
# 监听指定设备的事件,按一下电脑键盘的某个键观察输出 adb shell getevent -l /dev/input/event2 # 典型输出(EV_KEY 表示按键事件,DOWN/UP 表示按下抬起) # /dev/input/event2: EV_KEY KEY_VOLUMEUP DOWN # /dev/input/event2: EV_SYN SYN_REPORT 00000000 # /dev/input/event2: EV_KEY KEY_VOLUMEUP UP看懂输出之后就能反向注入了。sendevent的格式是"设备节点 事件类型 事件码 值",注意全部要用十六进制或十进制数值,不能用 KEY_VOLUMEUP 这种常量名,这是最容易出错的地方。模拟一次音量上键的完整序列:
# 按下(EV_KEY=1,KEY_VOLUMEUP=115,值1表示按下) adb shell sendevent /dev/input/event2 1 115 1 # 同步事件(EV_SYN=0,SYN_REPORT=0,值0) adb shell sendevent /dev/input/event2 0 0 0 # 抬起(值0) adb shell sendevent /dev/input/event2 1 115 0 adb shell sendevent /dev/input/event2 0 0 0这套写法有个非常重要的注意点:sendevent 不经过输入法,也不经过框架的分发逻辑,它产生的就是内核层面的事件。好处是能触发一些极其底层的响应,坏处是如果你少发了同步事件(SYN_REPORT),事件会卡在缓冲区里,表现就是"发了没反应",然后过一会儿突然连续生效好几下。我最早写这个脚本的时候漏了同步,排查了半天,最后对着getevent的输出逐行比对才发现问题。
注意:
sendevent需要 root 权限的镜像,普通 Google APIs 镜像里/dev/input/eventX的写权限是受限的。要用这条路线,选带 root 的镜像,或者用adb root重启 adbd。另外别拿生产设备试手,注入错误的事件序列有可能让输入子系统进入奇怪状态,重启才能恢复。
5. 常见问题与排查实录
5.1 现象、原因、动作速查表
下面这张表是我这几年攒下来的,基本覆盖了九成以上的"按键不灵"现场。遇到问题先扫一眼,能省掉大量盲目试错。
| 现象 | 根因 | 处理动作 |
|---|---|---|
| 键盘完全无反应,鼠标正常 | hw.keyboard=no | 改成yes,冷启动模拟器 |
| 改了配置还是没反应 | 从快照恢复,硬件未重新挂载 | 加-no-snapshot-load冷启动 |
| 字母能打,退格/方向键无效 | 焦点在输入法或被系统截获 | 换输入框测试,切英文输入法 |
| 中文打不出来 | 镜像里没有中文输入法 | 装输入法并ime set切换 |
| 软键盘不弹了 | 系统认为有物理键盘 | 设置里打开"显示虚拟键盘" |
| 组合键无反应 | 被宿主机全局快捷键抢占 | 改模拟器快捷键或关掉宿主机占用 |
input text空格丢失 | shell 分词 | 用%s代替空格 |
input text中文丢失 | 命令不支持非 ASCII | 换用可注入文本的输入法方案 |
--longpress报错 | 版本不支持该参数 | 改用同点input swipe长按 |
sendevent无反应 | 缺 SYN_REPORT 或多设备节点错 | 补同步事件,确认设备节点 |
| Mac 上部分键位错乱 | Command 键被系统占用 | 换 Ctrl 组合或改系统快捷键 |
| Linux 上字符对不上 | 键盘布局非 QWERTY | localectl查看,setxkbmap us切换 |
这张表里第一行和第二行的组合是最常见的组合拳:很多人改了 config.ini,但模拟器是从快照恢复的,于是死活不生效,然后开始怀疑文件路径写错了、权限不对、版本 Bug,一路排查到天荒地老。实际加一个启动参数就好了。
5.2 几个印象深刻的现场记录
说两个我印象最深的具体案例,都是那种"看文档看不出来,踩过一次就永远忘不掉"的类型。
第一个案例是"模拟器开机后前三十秒键盘正常,之后就失灵"。这个现象非常迷惑,因为它不是完全不工作。排查的过程是这样:先用getevent抓事件流,发现失灵之后按键事件确实还在往内核送,说明宿主机到模拟器这层没问题;再看dumpsys input里的焦点窗口,发现焦点落到了一个后台的系统窗口上;最后定位到是某个开机自启动的应用弹了一个全屏的透明浮层,抢走了焦点。按键事件确实被系统收到了,只是分发给了一个看不见的窗口。这种情况的教训是:"没反应"有时候不是没收到,是收到了给错了人。以后遇到间歇性失灵,先看焦点,再看事件。
第二个案例是"用 adb 发 POWER 键,屏幕黑了但再也点不亮"。这个更麻烦,因为按侧边栏的电源键是好的,adb 发的就不行。后来搞明白了:侧边栏的电源按钮发的是一个更完整的电源状态切换请求,而input keyevent KEYCODE_POWER在某些版本上只做了单向下发。解决办法是发两次——第一次灭,第二次亮;中间加个延时确保状态机切换完成。写成脚本就是:
adb shell input keyevent KEYCODE_POWER sleep 1 adb shell input keyevent KEYCODE_POWER这个"发两次"的思路在很多地方都通用。Android 的很多系统键本质是状态切换而不是绝对设置,你只发一次,状态对不对取决于当前处于什么状态,一旦前置状态不确定,结果就不确定。写自动化脚本时,宁可在前面先"归零"一次,也不要假设当前状态。
5.3 提升手感的几个小技巧
最后分享几个纯经验性的东西,不属于"排错",但能让日常用模拟器舒服很多。
第一,把高频操作绑到自己顺手的键上。比如我习惯用 Esc 当返回,如果某个版本里 Esc 被改掉了,我会去模拟器设置里把它调回来。这件事看起来很小,但一天按几百次,手感差异非常明显。
第二,做输入密集型测试时,把模拟器的软键盘关掉。软键盘会占据大半个屏幕,遮挡界面,而且电脑键盘已经能打字了,它纯属碍事。关掉之后屏幕利用率立刻上一个档次。开关位置在模拟器的 Extended controls 或者设置里,找不到就搜"show virtual keyboard"。
第三,用命令行启动模拟器而不是点图标。命令行的好处是可以带参数、可以写成快捷脚本、可以在后台跑。我常用的启动脚本长这样:
#!/usr/bin/env bash # 冷启动 + 指定 GPU 模式 + 放大窗口,日常调试够用了 emulator -avd <AVD名字> \ -no-snapshot-load \ -gpu auto \ -netdelay none -netspeed full \ -scale 0.8其中-scale控制窗口缩放比例,屏幕大的机器上 0.8 左右最舒服,既不糊又能一屏放下。-netdelay none -netspeed full是关掉网络模拟,避免本地调试时被莫名其妙的延迟干扰。这两个参数用不上可以去掉,但-no-snapshot-load建议一直留着,能避开大量"配置改了不生效"的玄学问题。
第四,如果你的应用需要频繁在横竖屏之间切换,别每次都去点旋转按钮,用 adb 一条命令搞定,还能顺便锁死方向:
# 关闭自动旋转 adb shell settings put system accelerometer_rotation 0 # 强制横屏(1 是竖屏,0 是横屏) adb shell settings put system user_rotation 1这套命令的好处是状态确定。点按钮旋转是"切到另一个状态",发命令是"设成指定状态",后者在脚本里可重复执行,不会因为当前是什么方向而结果不同。这个思路和前面说的"先归零再操作"是一脉相承的。
关于物理按键那一侧,我还想补一句:现在越来越多的测试场景不再靠模拟器侧边栏,而是把真实的 HID 设备(比如带按键的耳机、遥控器、扫码枪)接到宿主机上,让模拟器把宿主机识别到的输入设备透传进去。这条路能测到很多侧边栏按钮测不出的东西,比如某些设备特有的按键码、按键长按的时序特性。配置思路上和普通键盘完全一致,核心还是hw.keyboard=yes加上系统层面能正确枚举设备,剩下就是确认你的应用监听的 keyCode 和实际下发的一致,用getevent对一遍就知道。