Android测试笔记-从adb到日志排查

Android 这块的笔记之前是散着记的:adb 一篇、logcat 一篇、monkey 一篇,真到用的时候还得来回翻。这篇按实际测试的顺序把它们串成一条线——连得上 → 装得上 → 点得到 → 抓得全 → 看得懂 → 跑得稳,当作地图用,具体细节还是回到对应的笔记里看。

一、先弄清楚测的是什么

四大组件

Android 应用的基本单位是组件,测试时遇到的“页面没起来”“后台被杀”“数据串了”,基本都能对应到下面四类组件上。

组件 作用 测试关注点
Activity 一个可以交互的界面 生命周期、跳转返回、横竖屏重建
Service 没有界面、后台长期运行 被杀后能否恢复、绑定与解绑、前台服务通知
BroadcastReceiver 接收系统或应用广播 静态/动态注册、开机自启、广播风暴
ContentProvider 跨应用共享数据 权限、增删改查后数据一致性
Activity 的生命周期

一个 Activity 大致有四种状态:

  1. 运行:处于栈顶、可见、可交互;
  2. 暂停:被透明或非全屏的 Activity 盖住,失去焦点但还活着,内存紧张时才可能被回收;
  3. 停止:被完全覆盖,不可见,但资源还在;
  4. 回收:内存不足时被系统结束,结束前会回调 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 里留下记录。不管哪种,先读堆栈这五样东西:

  1. 崩溃的包名;
  2. 进程 PID;
  3. 异常类型和异常信息;
  4. 出错的类、方法、行号;
  5. 调用栈的每一层(栈帧),用来还原调用路径。

常见异常整理成表,看到名字基本能猜到方向:

异常 含义 典型场景
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:“打开详情页偶尔白屏,然后闪退”。按下面的顺序走,基本不会漏:

  1. 确认设备和前台页面

    adb devices -l
    adb shell dumpsys activity activities | grep mResumedActivity
    
  2. 清日志,复现

    adb logcat -c
    # 在设备上按步骤操作,直到问题出现
    
  3. 先看崩溃和 ANR

    adb logcat -b crash -d
    adb logcat -d -v threadtime | grep -i "FATAL\|ANR in"
    
  4. 按进程看上下文

    adb shell pidof com.example.app
    adb logcat --pid 12345 -d
    
  5. 看资源有没有异常

    adb shell top -m 10 -n 1
    adb shell dumpsys meminfo com.example.app
    
  6. 怀疑卡顿就补渲染数据

    adb shell dumpsys gfxinfo com.example.app
    
  7. 留证据

    adb bugreport D:\reports\case123
    adb shell screenrecord /sdcard/repro.mp4
    
  8. 提 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-* 参数,或先看崩溃日志
posted @ 2026-10-21 01:07  LauTianen  阅读(6)  评论(0)    收藏  举报