Android测试笔记-从adb到日志排查
Android 这块的笔记之前是散着记的:adb 一篇、logcat 一篇、monkey 一篇,真到用的时候还得来回翻。这篇按实际测试的顺序把它们串成一条线——连得上 → 装得上 → 点得到 → 抓得全 → 看得懂 → 跑得稳,当作地图用,具体细节还是回到对应的笔记里看。
一、先弄清楚测的是什么
四大组件
Android 应用的基本单位是组件,测试时遇到的“页面没起来”“后台被杀”“数据串了”,基本都能对应到下面四类组件上。
| 组件 | 作用 | 测试关注点 |
|---|---|---|
| Activity | 一个可以交互的界面 | 生命周期、跳转返回、横竖屏重建 |
| Service | 没有界面、后台长期运行 | 被杀后能否恢复、绑定与解绑、前台服务通知 |
| BroadcastReceiver | 接收系统或应用广播 | 静态/动态注册、开机自启、广播风暴 |
| ContentProvider | 跨应用共享数据 | 权限、增删改查后数据一致性 |
Activity 的生命周期
一个 Activity 大致有四种状态:
- 运行:处于栈顶、可见、可交互;
- 暂停:被透明或非全屏的 Activity 盖住,失去焦点但还活着,内存紧张时才可能被回收;
- 停止:被完全覆盖,不可见,但资源还在;
- 回收:内存不足时被系统结束,结束前会回调
onSaveInstanceState(),再次回到前台时走onRestoreInstanceState()。
从生命周期推出来的用例
- 切到后台再切回来,页面状态和数据是否还在;
- 横竖屏切换导致 Activity 重建,输入框内容、滚动位置是否保留;
- 开着页面时来电话、来通知、弹权限框,回来后是否正常;
- 从 A 跳到 B 再返回,返回栈是否符合预期;
- 分屏、小窗、多任务卡片切换时的表现;
- 长时间停留在后台被回收后,恢复时是否崩溃(这一条最容易漏)。
二、两种“挂了”:ANR 和 Crash
ANR
主线程被卡住,5 秒内没有响应输入事件,或者广播 10 秒内没执行完,系统就会弹“应用无响应”。常见原因:
- 主线程做耗时操作:数据库、网络、大文件读写、图片解码;
- 等待锁、
sleep()、join(),或者等一个永远不回来的回调; - Binder 调用卡住、服务端忙导致超时;
- 其他进程抢 CPU,本进程长期拿不到时间片;
- system_server 的 WatchDog 触发。
抓现场的办法:在 logcat 里搜 ANR in,后面会跟包名、原因和当时的 CPU 使用情况;完整的 trace 在设备 /data/anr/ 目录下(需要 root),或者直接抓一份 bugreport,里面也带着。
Crash
Java 层未捕获的异常会打印 FATAL EXCEPTION 并结束进程;native 层收到 SIGSEGV 这类信号,则会在 tombstone 里留下记录。不管哪种,先读堆栈这五样东西:
- 崩溃的包名;
- 进程 PID;
- 异常类型和异常信息;
- 出错的类、方法、行号;
- 调用栈的每一层(栈帧),用来还原调用路径。
常见异常整理成表,看到名字基本能猜到方向:
| 异常 | 含义 | 典型场景 |
|---|---|---|
| NullPointerException | 空指针 | 接口没返回数据就直接取字段、context 为空 |
| ClassCastException | 类型转换失败 | 强转前没判断真实类型 |
| IllegalArgumentException | 参数非法 | 传了不存在的 URI、Fragment 重复嵌套 |
| ArithmeticException | 算术异常 | 除数为 0 |
| ArrayIndexOutOfBoundsException | 数组越界 | 循环条件写成 <= length |
| IndexOutOfBoundsException | 下标越界 | List 渲染时数据源被改 |
| OutOfMemoryError | 内存溢出 | 大图不采样、集合只增不减 |
| IllegalStateException | 状态非法 | 响应已提交还继续写、getOutputStream() 重复调用 |
定位命令很简单,崩溃日志单独在 crash 缓冲区里:
adb logcat -b crash -d
想连现场一起看,就 adb logcat -v threadtime 全程录着,复现后搜 FATAL EXCEPTION。
三、把设备连上
adb 是怎么工作的
adb 分三部分:PC 上的 client(我们敲的命令)、PC 上的 server(默认监听 5037 端口)、设备上的 adbd。client 把命令交给 server,server 再转发给 adbd 执行,所以 server 没起来时任何 adb 命令都会先尝试把它拉起来。
adb start-server # 启动 server
adb kill-server # 停掉,端口冲突时常用
adb -P 5038 start-server # 换端口
adb version
看设备状态
adb devices
输出里的状态只有三种需要记住:
| 状态 | 含义 | 处理 |
|---|---|---|
device |
已连接,可以执行命令 | 正常 |
offline |
连接上了但没响应 | 重插线、adb kill-server 后重来 |
unauthorized |
设备没授权这台电脑 | 看手机上的“允许 USB 调试”弹窗 |
多台设备同时连着时,必须用 -s 指定序列号,-d 指唯一 USB 设备,-e 指唯一模拟器:
adb -s cf264b8f shell wm size
无线连接
Android 11 及以上不需要线:开发者选项里打开“无线调试”,选“使用配对码配对”,然后
adb pair 192.168.1.10:37123 # 输入手机上的六位配对码
adb connect 192.168.1.10:5555
老系统或者不方便配对的,先用 USB 连一次,让设备在 5555 端口监听,再拔线:
adb tcpip 5555
adb connect 192.168.1.10:5555
adb disconnect 192.168.1.10:5555
拿 root
adb root # 重启 adbd 为 root,输出 restarting adbd as root
adb unroot
部分厂商机型会提示 adbd cannot run as root in production builds,这种只能 adb shell 进去再 su,或者换工程机。
四、包:装、看、控
看装了哪些包
adb shell pm list packages
常用的过滤参数:
| 参数 | 作用 |
|---|---|
-3 |
只看第三方应用 |
-s |
只看系统应用 |
-d / -e |
只看已停用 / 已启用 |
-u |
包含已卸载但残留数据的 |
-f |
同时显示 apk 路径 |
-i |
显示安装来源 |
已经知道包名,想看 apk 装在哪:
adb shell pm path com.example.app
装包
adb install -r app-release.apk
-r 覆盖安装并保留数据,-t 允许安装 testOnly 包,-d 允许降级,-g 一次性授予所有运行时权限。装不上时看错误码,常见的几个:
| 错误码 | 原因 | 处理 |
|---|---|---|
INSTALL_FAILED_ALREADY_EXISTS |
已存在同名应用 | 加 -r,或先卸载 |
INSTALL_FAILED_VERSION_DOWNGRADE |
设备上是更高版本 | 加 -d,或先卸载 |
INSTALL_FAILED_UPDATE_INCOMPATIBLE |
签名不一致 | 先 adb uninstall 再装 |
INSTALL_FAILED_INSUFFICIENT_STORAGE |
空间不足 | 清理空间 |
INSTALL_FAILED_TEST_ONLY |
包是 testOnly | 加 -t,或重新打 release 包 |
INSTALL_FAILED_NO_MATCHING_ABIS |
CPU 架构不匹配 | 换对应 abi 的包 |
INSTALL_PARSE_FAILED_NOT_APK |
路径或后缀不对 | 确认传的是 apk 文件 |
INSTALL_PARSE_FAILED_NO_CERTIFICATES |
包没签名 | 重新签名 |
注: adb install 内部其实是三步——push 到 /data/local/tmp、调 pm install、删临时文件,所以手动分步执行也能达到同样效果。
卸载、清数据、启停
adb uninstall com.example.app # 卸载
adb uninstall -k com.example.app # 卸载但保留数据和缓存
adb shell pm clear com.example.app # 清除数据,相当于设置里点“清除数据”
adb shell am force-stop com.example.app # 强杀进程
adb shell pm disable-user --user 0 com.example.app # 停用
adb shell pm enable com.example.app
adb shell pm suspend com.example.app # 挂起,图标变灰、通知被拦
adb shell pm unsuspend com.example.app
注: pm disable 在部分系统上需要 root,普通权限用 pm disable-user --user 0 更稳。
看当前在前台的是谁
adb shell dumpsys activity activities | grep mResumedActivity
Windows 下把 grep 换成 findstr。输出里 com.example.app/.MainActivity 就是当前前台页面,很多“点了没反应”的问题,先确认这一行有没有变。
五、操作:input 和 am
input:模拟点击、滑动、按键
adb shell input tap 500 1200
adb shell input swipe 500 1500 500 500 300 # 后一个数字是滑动时长,单位 ms
adb shell input text "hello"
adb shell input keyevent 3 # HOME
input text 对空格和中文不友好,空格可以用 %s 代替,中文建议装一个 ADBKeyboard 再发广播输入。
常用键值:
| keycode | 含义 | keycode | 含义 |
|---|---|---|---|
| 3 | HOME | 26 | 电源 |
| 4 | 返回 | 82 | 菜单 |
| 24 / 25 | 音量加 / 减 | 85 | 播放暂停 |
| 187 | 最近任务 | 223 / 224 | 休眠 / 唤醒 |
| 66 | 回车 | 67 | 删除 |
am:启动页面、发广播、杀进程
adb shell am start -n com.example.app/.MainActivity
adb shell am start -n com.example.app/.DetailActivity --es "id" "1001"
adb shell am start -a android.intent.action.VIEW -d "https://example.com"
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED
adb shell am force-stop com.example.app
两个在测试里特别好用的参数:
-W:等页面启动完成再返回,输出里有TotalTime,可以拿来粗测冷启动耗时;-S:启动前先把目标应用杀掉,保证测的是冷启动。
adb shell am start -W -S -n com.example.app/.MainActivity
还有一些不常用但救急的:am monitor 盯着崩溃和 ANR,am crash 包名 主动制造一次崩溃,am kill-all 杀掉所有后台进程,am send-trim-memory 模拟系统回收内存。
六、稳定性:monkey
monkey 是设备上自带的伪随机事件发生器,主要用来在短时间内把各种点击、滑动、按键砸到应用上,看会不会崩。
基本用法
adb shell monkey -p com.example.app -v -v --throttle 300 10000
上面这条的意思是:只测 com.example.app,打两级日志,每次事件间隔 300ms,一共 10000 个事件。跑之前建议先记录一下起始时间,结束后用 logcat 对一遍。
参数速查
| 参数 | 作用 |
|---|---|
-p 包名 |
限定被测应用,多个包就写多个 -p |
-v |
日志级别,最多三级 |
-s 数字 |
固定随机种子,复现上次的顺序 |
--throttle 毫秒 |
事件间隔,模拟真人操作节奏 |
--pct-touch / --pct-motion |
触摸、滑动类事件占比 |
--pct-nav / --pct-majornav |
方向键、返回确认等导航事件占比 |
--pct-syskeys |
HOME、音量等系统键占比 |
--pct-appswitch |
切应用事件占比 |
--ignore-crashes |
崩了也继续跑完 |
--ignore-timeouts |
出 ANR 也继续跑完 |
--ignore-security-exceptions |
权限异常不中断 |
--monitor-native-crashes |
额外监控 native 崩溃 |
--kill-process-after-error |
出错后杀掉进程,保留现场 |
结果怎么看
正常跑完会打印 Events injected: N 和一句 // Monkey finished。中途停下就要在输出里搜这几个关键字:
// CRASH:—— 崩溃,后面跟包名和异常;// NOT RESPONDING:—— ANR;Monkey aborted due to error—— 因为异常提前退出了。
注: 有些机型直接跑 monkey 会报 SecurityException: Injecting to another application requires INJECT_EVENTS permission,不是脚本的锅,去开发者选项里把“USB 调试(安全设置)”打开就好。
七、日志:logcat
几个缓冲区
Android 的日志不是一个文件,而是按用途分成的环形缓冲区:main(应用日志)、system(系统进程)、crash(崩溃)、radio(通话相关)、events(事件)。看哪个加 -b:
adb logcat -b crash
adb logcat -b events
按级别和 tag 过滤
级别从低到高是 V、D、I、W、E、F、S,写 tag:级别 表示输出该级别及以上:
adb logcat ActivityManager:I MyApp:D *:S
*:S 是把其他 tag 全部静音,只留自己关心的,不然日志会被系统日志淹掉。想按级别看全部:
adb logcat *:E
按进程过滤
Android 7.0 之后支持直接按 pid 过滤,先查 pid 再过滤:
adb shell pidof com.example.app
adb logcat --pid 12345
没有 pidof 的老系统,用 adb shell ps | grep 包名 取第二列。
输出格式
-v 指定格式,排查时最常用 threadtime,带日期、PID、TID,能看出是哪个线程卡的:
adb logcat -v threadtime
| 格式 | 样式 |
|---|---|
| brief | D/Tag( 1234): message |
| time | 08-28 22:39:39.974 D/Tag( 1234): message |
| threadtime | 08-28 22:39:39.974 1234 1832 D Tag: message |
| long | 元信息单独一行,正文另起一行 |
落盘和滚存
adb logcat -v threadtime > D:\log\case123.txt
adb shell logcat -f /sdcard/log.txt -r 1024 -n 10
-r 是每个文件多少 KB 就切,-n 是保留几个文件,长跑压测时用这套比直接重定向靠谱。
只抓某个应用的日志
思路是先拿 pid,再用 pid 过滤,写成 bat 就是两条:
@echo off
adb shell "ps | grep com.example.app"
拿到 pid 后:
@echo off
for /f "tokens=2 delims= " %%i in ('getpid.bat') do set pid=%%i
adb logcat | find "%pid%"
崩溃现场
adb logcat -b crash -d
adb logcat -d | grep -A 30 "FATAL EXCEPTION"
想一次性把系统服务、dumpstate、logcat 全打包,用 bugreport:
adb bugreport D:\reports
生成的是一个 zip,里面最关键的是 bugreport-*.txt,还有 version.txt 和 FS/ 目录(设备文件系统的快照)。文件太大不好读,可以拿 ChkBugReport 转成 html 看。
八、系统状态:dumpsys、top 和渲染
dumpsys
dumpsys 是把某个系统服务的内部状态整个倒出来,加服务名只看一部分:
adb shell dumpsys activity activities
adb shell dumpsys package com.example.app
adb shell dumpsys meminfo com.example.app
adb shell dumpsys battery
adb shell dumpsys window displays
| 服务 | 能看什么 |
|---|---|
activity |
任务栈、前台 Activity、Service 状态 |
package |
版本、权限、安装路径、签名 |
meminfo |
内存占用明细,PSS、Private Dirty |
gfxinfo |
渲染帧统计 |
battery |
电量、温度、充电状态 |
window displays |
分辨率、密度、显示区域 |
dumpsys -l 可以列出所有可用服务,记不住名字时先查一下。
top
adb shell top -m 10 -n 1
adb shell top -d 2 | grep com.example.app
Android 8.0 之后输出分成了 Tasks、Mem、Swap 三段,再下面是进程列表。重点看 %CPU、RES、S(R 运行、S 睡眠、D 不可中断、Z 僵尸)和 TIME+。RES 减掉 SHR 才是这个进程真正独占的物理内存。
内存
adb shell dumpsys meminfo com.example.app
adb shell dumpsys procstats --hours 3
meminfo 看当下,重点两列:Pss Total(含共享内存按比例分摊)和 Private Dirty(进程独享、必须留在内存里的部分)。procstats 看一段时间内的变化,适合找“慢慢涨上去”的泄漏。
渲染
卡顿相关的几条命令,细节在那篇渲染检查里,这里只留索引:
| 想看什么 | 命令 |
|---|---|
| 整体帧统计、掉帧占比 | adb shell dumpsys gfxinfo 包名 |
| 最近 120 帧的逐帧耗时 | adb shell dumpsys gfxinfo 包名 framestats |
| 重置统计,只看某段操作 | adb shell dumpsys gfxinfo 包名 reset |
| 上屏延迟(SurfaceView 类) | adb shell dumpsys SurfaceFlinger --latency "图层名" |
设备信息速查
| 想看什么 | 命令 |
|---|---|
| 型号 | adb shell getprop ro.product.model |
| 系统版本 | adb shell getprop ro.build.version.release |
| SDK 版本 | adb shell getprop ro.build.version.sdk |
| 分辨率 | adb shell wm size |
| 屏幕密度 | adb shell wm density |
| 电量 | adb shell dumpsys battery |
| IP | adb shell ifconfig wlan0 |
| 内存 | adb shell cat /proc/meminfo |
| CPU | adb shell cat /proc/cpuinfo |
做兼容性时还可以临时改分辨率和密度,看布局会不会挤爆,测完记得 reset:
adb shell wm size 720x1280
adb shell wm density 320
adb shell wm size reset
adb shell wm density reset
九、UI 自动化
先选框架
Android 的 UI 自动化大致三条路:原生的 UiAutomator(Java、要打包成 apk/jar)、uiautomator2(Python,电脑上跑,能实时看设备画面)、以及 Appium 这类跨端方案。笔记里主要用了第二种,理由是写起来快、调试直观。
用例组织上,如果团队用 Python,pytest 比 unittest 省事:断言就一个 assert,参数化用 @pytest.mark.parametrize,失败重跑有 pytest-rerunfailures,报告有 pytest-html 和 allure。两者的区别简单列一下:
| 对比项 | unittest | pytest |
|---|---|---|
| 用例发现 | 继承 TestCase |
文件名 test_ 开头,函数名 test_ 开头 |
| 前置后置 | setUp/tearDown |
fixture,粒度更细 |
| 参数化 | 依赖 ddt | @pytest.mark.parametrize |
| 失败重跑 | 没有 | --reruns |
| 断言 | 一堆 assertXxx |
原生 assert |
pytest 几种前后置的执行顺序是固定的,写用例时别搞混:
setup_module > setup_class > setup_method > setup > 用例 > teardown > teardown_method > teardown_class > teardown_module
uiautomator2 常用操作
import uiautomator2 as u2
d = u2.connect() # 只有一台设备时可以省略序列号
d.app_start("com.example.app") # 启动
d.app_wait("com.example.app", front=True) # 等它到前台
d(text="登录").click()
d(resourceId="com.example.app:id/et_phone").set_text("13800000000")
d.swipe(500, 1500, 500, 500)
d.screenshot("shot.png")
d.app_stop("com.example.app")
| 操作 | 写法 |
|---|---|
| 连接设备 | u2.connect("序列号") |
| 启动 / 停止 | d.app_start(包名) / d.app_stop(包名) |
| 清除数据 | d.app_clear(包名) |
| 等待前台 | d.app_wait(包名, front=True) |
| 应用信息 | d.app_info(包名) |
| 当前界面 | d.app_current() |
| 按文本点击 | d(text="确定").click() |
| 按 id 输入 | d(resourceId="...").set_text("abc") |
定位元素的属性就那几样:resource-id、text、content-desc、className、checked、focused。拿不准属性时用 uiautomatorviewer(SDK 的 tools/bin 下)截个图看层级,或者直接 d.dump_hierarchy() 把当前界面的 xml 打出来。
稳定性
UI 自动化最烦的是“偶发找不到元素”,本质是没等页面稳定。三层等待按需用:
- 隐式等待:
d.wait_timeout = 10,全局生效; - 显式等待:
d(text="提交").wait(timeout=5),等具体某个元素; - 强制等待:
time.sleep(),只在实在没办法时兜底。
判断用例是不是“真挂了”,比判断“跑没跑通”更重要:失败时先把截图、当前界面 xml、logcat 一起存下来,否则复跑一次就找不到现场了。
十、串起来:一次问题排查的完整动作
假设收到一个 bug:“打开详情页偶尔白屏,然后闪退”。按下面的顺序走,基本不会漏:
-
确认设备和前台页面
adb devices -l adb shell dumpsys activity activities | grep mResumedActivity -
清日志,复现
adb logcat -c # 在设备上按步骤操作,直到问题出现 -
先看崩溃和 ANR
adb logcat -b crash -d adb logcat -d -v threadtime | grep -i "FATAL\|ANR in" -
按进程看上下文
adb shell pidof com.example.app adb logcat --pid 12345 -d -
看资源有没有异常
adb shell top -m 10 -n 1 adb shell dumpsys meminfo com.example.app -
怀疑卡顿就补渲染数据
adb shell dumpsys gfxinfo com.example.app -
留证据
adb bugreport D:\reports\case123 adb shell screenrecord /sdcard/repro.mp4 -
提 bug:机型、系统版本、包版本、复现步骤、频率,加上 logcat、bugreport、录屏。信息给全了,开发才不用反过来找你。
十一、速查与踩坑
日常最常用的十条
| 目的 | 命令 |
|---|---|
| 看设备 | adb devices -l |
| 装包 | adb install -r app.apk |
| 卸包 | adb uninstall 包名 |
| 清数据 | adb shell pm clear 包名 |
| 强杀 | adb shell am force-stop 包名 |
| 启动 | adb shell am start -W -n 包名/.Activity |
| 抓日志 | adb logcat -v threadtime > log.txt |
| 抓崩溃 | adb logcat -b crash -d |
| 看内存 | adb shell dumpsys meminfo 包名 |
| 截图 | adb exec-out screencap -p > sc.png |
常见报错怎么处理
| 现象 | 原因 | 处理 |
|---|---|---|
device unauthorized |
没授权调试 | 手机上看弹窗点允许,或撤销 USB 调试授权重来 |
device offline |
连接异常 | 重插线、adb kill-server |
error: device not found |
没识别到设备 | 换线换口,检查驱动 |
protocol fault / 起不来 server |
5037 被占用 | netstat -ano 找到 5037 对应的 PID 并结束 |
INSTALL_FAILED_UPDATE_INCOMPATIBLE |
签名不一致 | 先卸载再装 |
INJECT_EVENTS permission |
没开 USB 调试安全设置 | 开发者选项里打开 |
adbd cannot run as root |
正式版系统限制 | 用 su,或换工程机 |
| monkey 莫名提前结束 | 遇到崩溃/ANR/权限异常 | 加 --ignore-* 参数,或先看崩溃日志 |

浙公网安备 33010602011771号