简介:《Android 性能优化》文档面向移动端应用开发者、系统工程师与测试人员,围绕开机速度、响应速度、内存管理、系统稳定性、数据传输、流量控制、存储容量与模块功耗八个方向梳理优化思路,并针对 PAD 与车机两类硬件场景给出差异化取舍,适合需要系统排查性能瓶颈、建立验收标准的中高级读者参考。压缩包内含 1 个 docx 文件,约 10.32MB,正文以条目化清单组织,覆盖 14 类集成应用的响应时间测试方法、冷启动与热启动差异分析、内存泄漏与 ANR、Crash 检查、Monkey 与 CTS 兼容性验证,以及音视频编解码、背光、LCD、modem 等模块的功耗控制要点,并附 adb 常用命令与 adb shell 操作速查,便于在调试现场对照查阅。目前已有 687 人学习下载,可作为性能专项自查与问题定位的参考资料。
1. 从「开机 31 秒」倒推:一套能落地的 android 性能优化清单
某车机项目验收前一周,测试提了个单:冷启动到桌面可操作 31 秒,竞品同配置 22 秒。团队第一反应是去翻 kernel 配置、砍服务,折腾了三天,秒数只降了 1.2 秒。后来拉了一份带时间戳的开机打点才发现,时间大头根本不在 kernel,而在 PackageManagerService 扫描和 dex2oat 预编译上——android 性能优化最容易踩的坑,不是不知道该优化什么,而是不知道该先量什么。
这份资源把优化面拆成八块:开机速度、响应速度、内存管理、系统稳定性、数据传输、数据流量、存储容量、模块功耗,并且针对车机和平板两套形态给了不同的取舍——车机不烧电池,功耗优先级可以往后放;平板 BSP 是内嵌打包的,模块功耗基本只能在应用层做对比验证。配套的是一整套 adb 命令手册,从procrank到dumpsys meminfo,从 logcat 过滤器到 tombstone 反汇编。适合正在做整机性能调优、手上有一台能连 adb 的机器、需要拿数据说话的人。
2. 开机速度:从 kernel 日志到 preload-class 的删减顺序
2.1 先把开机链路的四个阶段拆开量
不做打点就直接砍配置,等于闭眼改代码。Android 从按下电源到桌面可操作,可以切成四段:bootloader 与 kernel 初始化、init 与 zygote 预加载、SystemServer 启动各服务、PMS 扫描预装应用、最后是 Launcher 冷启动。每段的耗时都能用系统自带的 event log 打点读出来,不需要额外埋点。
# 抓开机阶段的所有 boot_progress 事件,-b events 只读事件缓冲,干扰小 adb logcat -b events -v threadtime | grep -E "boot_progress" # 单独看当前开机已经跑了多少秒(第一个字段是秒,第二个是空闲进程累计秒) adb shell cat /proc/uptime输出大致长这样(数值是某台平板的实测,仅作格式参考):
boot_progress_start: 0 boot_progress_preload_start: 820 boot_progress_preload_end: 1650 boot_progress_system_run: 1720 boot_progress_pms_start: 2810 boot_progress_pms_system_scan_start: 2980 boot_progress_pms_data_scan_start: 5120 boot_progress_pms_scan_end: 6240 boot_progress_pms_ready: 6810 boot_progress_ams_ready: 7420 boot_progress_enable_screen: 9020两个相邻打点的差值就是该阶段耗时。上面这组数据里,preload 花了 830ms,SystemServer 从启动到 PMS 开始花了 1090ms,而 PMS 扫描(系统分区 2140ms + 数据分区 1120ms)合计占了 3.2 秒,接近总时长的三分之一。这时候再去砍 kernel 驱动,收益上限就是那 820ms,方向一开始就错了。
kernel 侧的时间用另一条路拿:
# 打开 initcall_debug 后,每个 initcall 的耗时都会打到 dmesg # 在 kernel cmdline 里加 initcall_debug,或者临时: adb shell "echo 1 > /proc/sys/kernel/printk" adb shell dmesg | grep -E "initcall.*returned" | sort -t' ' -k6 -rn | head -20initcall_debug会让每个驱动 probe 打一行initcall xxx returned 0 after NNN usecs,按 usecs 排序就是驱动初始化耗时榜。车机上常见的上榜者是触摸屏、摄像头、音频 codec 和以太网 PHY,把这几个的 probe 里多余的寄存器读写和延时去掉,几百毫秒是能抠出来的。
2.2 日志、驱动、服务三类删减项的取舍
开机阶段最便宜的优化是砍日志,但要砍对地方。kernel 的 printk 等级由 cmdline 里的loglevel控制,默认 7,调到 4 之后 info 级以下不再输出;init 进程的日志由init.rc里log_level控制;Android 用户态则是persist.log.tag属性。车机还有个特殊情况:串口 UART 日志。量产车上串口是接诊断设备的,调试阶段常开,单条日志的同步写代价比内存 buffer 高一个数量级,关掉串口输出经常能省 200~500ms。
驱动模块的裁剪遵循「没用到的才删」原则。判断有没有用到,不是看 defconfig 里有没有写,而是看开机后adb shell lsmod和/proc/devices里实际注册了谁。删之前先确认这个驱动对应的外设在这个 SKU 上是否真的不存在——同一套 BSP 打不同配置的 bin,删错一个触摸驱动,整机就变砖,只能重刷。
Android 服务的裁剪风险最高。用adb shell service list或adb shell dumpsys -l能列出所有注册的服务,但「列出来」和「能删」是两回事。系统里大量服务之间存在隐式依赖,删掉telephony.registry之后ConnectivityService在无 SIM 卡场景下会走到空指针分支,这类问题在日志里表现为开机后某个服务反复重启。稳妥的做法是每次只删一个,跑一遍完整开机加 Monkey,确认无异常再删下一个。
| 删减项 | 典型收益 | 主要风险 | 验证方式 |
|---|---|---|---|
| kernel printk 降级 | 100~300ms | 现场问题定位困难,需留可动态打开的口子 | 对比 dmesg 时间戳 |
| 串口日志关闭 | 200~500ms | 调试期不便 | 抓 boot_progress 差值 |
| 未用驱动模块 | 100~800ms | 删错外设驱动导致功能缺失 | 全外设点检 |
| 冗余系统服务 | 300ms~1s | 隐式依赖引发服务重启 | service list 稳定性观察 |
| preload-class 精简 | 200~600ms | 首次打开应用变慢 | 冷启动基线对比 |
| 预装 APK 合并 | 1~2s | 功能入口变更 | PMS 扫描段打点 |
2.3 preload-class 与预装 APK 的减法边界
frameworks/base/preloaded-classes这个文件里列的是 zygote 启动时预加载的 Java 类,目的是让每个应用 fork 出来后少走一次类加载。原文件动辄两三千个类,全量预加载在低端硬件上要花 600~900ms。但这里有个反直觉的结论:删得太狠,开机是快了,用户第一次点开设置会明显卡一下,因为原本在开机时付的成本被挪到了首次使用。
我一般的做法是先量命中率再删。思路是抓一次典型使用场景下的类加载日志,统计哪些预加载类在整个开机到首次操作完成期间从未被触发,这部分才是安全删除区。实践上保守删 20%~30% 就够,再往上收益递减而体验风险陡增。清单要跟着版本走,换了 codebase 或者框架层大改之后必须重新采一遍。
预装 APK 的扫描时间是另一块大头,从boot_progress_pms_system_scan_start到pms_scan_end就是全量扫描窗口。减数量是最直接的:把同一厂商的多个小 APK 合并成一个大 APK,扫描次数从 N 降到 1,每个 APK 的解析、签名校验、资源索引都能省掉。另一个手段是把 dex2oat 从首次开机挪走,用pm.dexopt.first-boot属性把首次编译策略从speed降到quicken或verify,首次开机快几秒,代价是首次运行某个应用时多一次编译。刷机后的首次启动优化主要就靠这个,量产机的首次开机时间和二次开机时间差异往往就在这。
提示:preload-class 和 dexopt 策略的调整都必须在同一硬件、同一固件版本上做 A/B,跨版本对比没有意义,因为 BSP 和框架的差异会把收益完全淹没。
3. 响应速度与内存管理:用同一套 adb 命令把两个问题分开
3.1 冷启动与热启动的基线怎么测
响应速度的第一原则是分层:应用层、框架层、硬件层。应用层能改,框架层看 codebase,硬件层只能认命。资源里点名的 14 个集成应用——桌面、电话、联系人、短信、相机、相册、云邮箱、音乐、视频、应用中心、安全精灵、主题中心、设置、文件浏览器——之所以要单独测,是因为它们都涉及大数据量的导入导出和频繁的跨进程数据交换,启动路径最长,优化空间也最大。
am start -W是最省事的量法,能直接吐出三个时间:
# -W 等待启动完成并输出耗时;-S 先 force-stop 再启动,保证是真正的冷启动 adb shell am force-stop com.demo.settings adb shell am start -W -S -n com.demo.settings/.Settings输出里的TotalTime是从 Activity 启动到首帧绘制完成的时长,WaitTime包含了系统调度和 Activity 栈切换的额外开销,ThisTime只看当前 Activity 自身。判断是应用慢还是系统慢,看TotalTime和WaitTime的差值——差值大说明卡在 AMS 调度或者任务栈切换上,跟应用代码关系不大。
冷热启动必须分开测,差别经常在两倍以上。冷启动时进程不存在,要走完 fork、Application 初始化、ContentProvider 装载全流程;热启动只是把已存在的 Activity 重新拉到前台。测试脚本里冷启动前必须force-stop,热启动则用input keyevent 3回桌面再点回来,中间不要杀进程。
高速摄像机是另一条路,240fps 下每帧 4.17ms,从手指接触屏幕到画面出现首帧,逐帧数。这条路的好处是不受系统打点精度影响,能抓到「点击响应延迟」这种am start -W量不到的部分;坏处是慢,14 个应用全测一遍要半天。我的习惯是先用am start -W跑全量筛出异常项,再用摄像机对异常项做一次精确复核。
# 批量采集 14 个集成应用的冷启动耗时,输出到 csv #!/bin/bash APPS=( "com.demo.homeshell/.Launcher" "com.demo.contacts/.ContactsActivity" "com.demo.settings/.Settings" # ... 其余 11 个按同样格式补齐 ) echo "app,cold_total,cold_wait" > cold_start.csv for entry in "${APPS[@]}"; do pkg="${entry%%/*}" adb shell am force-stop "$pkg" sleep 1 out=$(adb shell am start -W -S -n "$entry") total=$(echo "$out" | awk -F': ' '/TotalTime/{print $2}') wait=$(echo "$out" | awk -F': ' '/WaitTime/{print $2}') echo "$pkg,$total,$wait" >> cold_start.csv done脚本逻辑很直白:先按包名杀进程保证冷启动条件,再启动并解析输出。sleep 1是给系统回收进程留时间,去掉这个等待会得到偏小的数值。落成 csv 是为了后续做版本间对比,同一台机器刷两个版本各跑一遍,直接 diff 就能看出哪个应用退化了。
解锁和锁屏是另一类响应场景,容易被忽略。锁屏界面要加载壁纸、播放解锁音效、解除和重新加载功耗锁(wakelock),这套流程涉及 SurfaceFlinger 合成和音频通道初始化,在某些平台上能占到 400ms 以上。测法是录屏加逐帧,或者抓dumpsys window里的窗口切换时间戳。
3.2 VSS、RSS、PSS、USS 四个口径的差别
内存这块最容易吵起来的原因,是两个人说的「内存占用」根本不是同一个数。procrank输出的四个值各有含义,选错了口径,优化方向会完全跑偏。
| 口径 | 全称 | 是否含共享库 | 适用场景 |
|---|---|---|---|
| VSS | Virtual Set Size | 是 | 看虚拟地址空间,几乎无参考价值 |
| RSS | Resident Set Size | 是(全额计入每个进程) | 粗看物理页占用,多进程场景会重复计数 |
| PSS | Proportional Set Size | 是(按共享进程数均摊) | 系统级内存压力评估,最常用 |
| USS | Unique Set Size | 否 | 判断某进程真实独占内存,泄漏排查首选 |
判断泄漏看 USS,因为共享库的波动会干扰 PSS;评估整机内存压力看 PSS 总和;VSS 基本不用,一个进程申请了 1GB 虚拟地址但没实际写入,VSS 显示 1GB,物理内存一页都没占。
# 按 USS 降序排列所有进程,需要 root 或 userdebug 版本 adb shell procrank -u # 单进程详细内存构成,包含 Java Heap / Native Heap / Code / Stack / Graphics adb shell dumpsys meminfo com.demo.homeshell # 系统整体内存水位,MemAvailable 才是真正可用的估算值 adb shell cat /proc/meminfo | grep -E "MemTotal|MemFree|MemAvailable|Cached|SwapTotal"/proc/meminfo里有个经典的口径坑:设置里「运行中」页面显示的可用内存不是MemFree,而是MemFree + Cached - SECONDARY_SERVER_MEM,这是框架里为了给后台服务预留空间做的一个修正。直接拿MemFree去和设置页对比,永远对不上。从 Android 4.4 之后内核提供了MemAvailable,这个值综合了可回收的 page cache 和 slab,是更接近真实可用量的估算,做基线对比时统一用它。
3.3 泄漏、OOM、ANR 的排查脚本
内存泄漏的判定标准是「反复进出同一页面,USS 单调上升且不回落」。单次测量没有意义,必须做循环。
#!/bin/bash # 反复进出设置页 30 次,记录每次的 PSS,观察是否单调上升 PKG=com.demo.settings ACT=$PKG/.Settings > leak_trace.txt for i in $(seq 1 30); do adb shell am start -n "$ACT" > /dev/null sleep 2 # 不同 Android 版本 dumpsys meminfo 的字段位置不同,先跑一次确认列号 pss=$(adb shell dumpsys meminfo "$PKG" | awk '/TOTAL PSS:/{print $3}') echo "$i $pss" >> leak_trace.txt adb shell input keyevent 4 > /dev/null # 返回键退出,不杀进程 sleep 1 done跑完把leak_trace.txt画成折线,正常情况应该在前几次上升后趋于平稳,如果在 30 次内涨了 20MB 以上且没有回落趋势,基本可以判定泄漏。这时候再抓 heap dump 对比对象数量,用 MAT 或者 Android Studio 的 Memory Profiler 找支配树里异常膨胀的类。
代码层面最高频的两类泄漏,一类是静态单例持有了 Activity 的 Context,另一类是 Bitmap 没有按目标尺寸采样。前者改起来简单:单例里存context.getApplicationContext(),需要 Activity 的地方用弱引用或者回调时传入。
// 反例:单例把 Activity 钉死,Activity 销毁后整棵 View 树都回收不了 public class ImageLoader { private static ImageLoader sInstance; private Context mContext; public static ImageLoader get(Context ctx) { if (sInstance == null) { sInstance = new ImageLoader(); sInstance.mContext = ctx; // 传进来的是 Activity } return sInstance; } } // 正例:只留 Application Context,生命周期跟进程一致 public static ImageLoader get(Context ctx) { if (sInstance == null) { sInstance = new ImageLoader(); sInstance.mContext = ctx.getApplicationContext(); } return sInstance; }Bitmap 的问题在相册和相机预览里最明显。一张 4000×3000 的 JPEG 按 ARGB_8888 解码要占 48MB,低端机单进程堆上限可能只有 128MB,连开几张就 OOM。标准做法是先读inJustDecodeBounds拿原图尺寸,按目标 View 大小算inSampleSize,再解码;格式上如果没有透明度需求,用RGB_565能再省一半。
空指针、ANR、Crash 三者的关系要理清:空指针是代码缺陷,ANR 是主线程阻塞超时,Crash 是未捕获异常导致进程退出。ANR 的主线程堆栈在/data/anr/下,Crash 的 native 堆栈在/data/tombstones/下,Java 异常在 logcat 的AndroidRuntimetag 里,三条线要分开抓。
注意:
procrank、showmap、procmem这几个工具在 user 版本上可能不存在或没有输出,需要 userdebug/eng 版本或者先adb root。量产机上拿不到这些数据时,退回到dumpsys meminfo也能完成大部分判断。
4. 系统稳定性:ANR、tombstone 与 Monkey/CTS 的闭环
4.1 ANR 的三个触发条件与 trace 抓取
ANR 不是随机事件,触发条件写死在框架里,一共三类:输入事件分发超过 5 秒没有处理完,前台 BroadcastReceiver 的onReceive超过 10 秒(后台是 60 秒),前台 Service 的启动或绑定超过 20 秒(后台是 200 秒)。知道了阈值,就能反推是哪一类。
# 事件缓冲里记录每一次 ANR,包含进程名和 reason adb logcat -b events -v time | grep -E "am_anr|am_crash" # ANR 发生时会往 /data/anr/ 写 trace,文件名各版本不同(traces.txt 或 anr_时间戳) adb shell ls -l /data/anr/ adb pull /data/anr/ ./anr_logs/trace 文件里最关键的是"main"线程的堆栈。如果卡在MessageQueue.nativePollOnce,说明主线程其实是空闲的,问题出在别的地方——常见的是 Binder 调用对端阻塞,或者消息队列被大量消息堵住;如果卡在synchronized等待锁,就顺着waiting to lock找持锁线程;如果卡在 IO 或者数据库查询,那就是实打实的慢操作放主线程了。
车机场景下还有一类特殊的 ANR:冷启动阶段系统负载极高,Launcher 还没完成初始化就来了输入事件,5 秒内分发不出去。这种不是某个应用的问题,是开机流程和用户操作的时间窗重叠了。解决办法要么把 Launcher 的可交互时间提前,要么在开机后一段时间内对输入事件做降级处理。
4.2 tombstone 反汇编还原 native 崩溃调用栈
Java 层的 Crash 看 logcat 就够,native 层的崩溃必须靠 tombstone。进程收到 SIGSEGV 或 SIGABRT 时,debuggerd 会把寄存器状态、调用栈、内存映射全部写到/data/tombstones/tombstone_NN。
# 拉取所有 tombstone adb pull /data/tombstones/ ./tombstones/ # 从 logcat 里也能看到崩溃摘要,包含 pid、信号、fault addr adb logcat -b crash -v timetombstone 里的调用栈形如#00 pc 00012345 /system/lib/libxxx.so,这个十六进制地址是相对偏移,要靠带符号的 so 翻译成文件行号。
# 工具链在源码树的 prebuilts 下,路径按自己的 codebase 调整 TOOL=prebuilts/gcc/linux-x86/arm/arm-eabi-4.6/bin/arm-eabi-addr2line # 把 0x00012345 翻译成 文件名:行号 $TOOL -e out/target/product/demo/symbols/system/lib/libxxx.so 0x00012345 # 导出动态库的完整符号表,按地址排序,方便对照多个栈帧 arm-eabi-nm -l -C -n -S out/target/product/demo/symbols/system/lib/libdvm.so > dvm.symbols # 反汇编,看崩溃点前后的指令 arm-eabi-objdump -C -d out/target/product/demo/symbols/system/lib/libc.so > libc.s参数说明:-e指定带符号的 ELF 文件,必须是out/target/product/xxx/symbols/下的版本,用设备上直接 pull 出来的 stripped so 是翻译不出行号的;-C做 C++ 符号 demangle,否则看到的是一堆_ZN前缀;-n按地址排序,-S输出符号大小,崩溃地址落在哪个函数区间一眼能看出来。
如果 addr2line 返回??:0,八成是 so 版本不匹配。设备上的 so 是编译后 strip 过的,符号表在 symbols 目录里另存一份,刷机版本和 symbols 版本必须严格对应,差一个 commit 都可能对不上。
4.3 Monkey 与 CTS 的达标口径
Monkey 的价值不在于跑多久,而在于跑完之后怎么判。原始输出里CRASH、NOT RESPONDING、ANR各出现几次,是唯一可以量化的指标。
# 200000 次随机事件,throttle 300ms,忽略已知异常让测试跑完,固定 seed 保证可复现 adb shell monkey -p com.demo.homeshell \ --throttle 300 \ --ignore-crashes --ignore-timeouts --ignore-security-exceptions \ -s 20240115 -v -v 200000 > monkey.log-s固定 seed 是关键,不固定的随机流出了问题时无法复现,等于白跑。--throttle控制事件间隔,设太小会把压力测试变成极限压测,正常交互是 300~500ms。判标准则是:跑完后grep -c "CRASH"和grep -c "NOT RESPONDING"都应该是 0,只要有 1 次就说明存在必现或高概率复现的缺陷,必须定位到具体堆栈再关单。
CTS 的用法不一样,它不是压测,是兼容性红线。做性能优化、尤其删服务删驱动之后,跑一遍 CTS 能兜住大部分「删过头」的问题。实操上建议保留一份优化前的 CTS 通过清单,每次删除动作后重跑对应模块,把 fail 的用例和删除项对上号。这份对照关系是后面做同类项目的后悔药,下次知道哪个服务动不得。
Monkey 和 CTS 之间还有个空白地带:第三方应用的兼容性。CTS 只覆盖 Google 定义的用例集,厂商自己的应用商店、地图、视频类应用不在里面。工程上的做法是拿一份内部 Top 50 第三方应用清单,安装后逐个做启动加基本操作,观察是否有兼容性 Crash。这块投入产出比不高但漏了会很难看,尤其在车机上,用户装不了几个应用,装上的那个打不开就是致命问题。
5. 避坑与排查:五条花时间换来的记录
5.1 日志抓取类
现象:问题复现了三次,每次回头去抓日志,logcat里只剩最近十几秒,前面的全没了。原因:logcat 是环形的,默认 main 缓冲区只有 256KB 左右,系统日志刷得快的时候十几秒就绕一圈覆盖掉。解决:复现前把缓冲区调大或者直接落盘。adb logcat -G 16M可以把缓冲区调到 16MB,adb logcat -f /data/local/tmp/full.log -r 10240 -n 10会按 10MB 一个文件滚动保存 10 个。更稳的做法是分缓冲抓,-b main,system,crash,events各走各的,进程崩溃前的am_crash事件基本不会丢。
现象:adb shell cat /proc/kmsg > kmsg.txt执行之后,命令一直不返回,文件大小停在 0。原因:/proc/kmsg是内核日志的字符设备接口,行为是阻塞读——没有新日志就一直等,永远不会 EOF。同理adb shell logcat不加-d也是一直等。解决:加超时或者换成非阻塞的方式。抓当前快照用adb shell dmesg,它读的是内核 ring buffer 的副本,读完就返回。需要持续跟踪的话用timeout 30 adb shell cat /proc/kmsg > kmsg.txt,超时自动断开。抓上一次崩溃的内核日志用/proc/last_kmsg,这个也是读一次就结束,但只在部分内核配置下存在。
5.2 删减配置类
现象:为了开机提速删掉了两个看起来无关紧要的系统服务,开机确实快了 600ms,但 CTS 跑出 7 个 fail,其中几个和删掉的服务完全不搭边。原因:系统服务之间存在隐式依赖,框架里大量代码是直接ServiceManager.getService()拿服务然后不判空的,删掉之后在特定路径上才炸,静态扫描发现不了。解决:把删减动作做成原子提交,一次只删一个服务,删完立刻跑该服务相关模块的 CTS 加上一轮基础 Monkey。维护一份删除清单,记录每个删除项的收益和对应 CTS 模块的通过状态,这个表比任何文档都有用。发现 fail 就回滚那一项,不要试图去改 CTS 用例。
现象:preload-class 砍掉四成,开机 preload 阶段从 830ms 降到 410ms,但用户反馈「第一次打开设置要等三秒」。原因:预加载的成本没有消失,只是从开机时挪到了首次使用时。zygote 里没预加载的类,第一次被用到时要在应用进程里现场加载、校验、链接,单个类的代价不大,但一个页面涉及几百个类就是几百毫秒。解决:按命中率删,不按比例删。采集一次完整开屏加典型操作路径的类加载序列,把整个周期内没被触发的类标出来再删。删除比例控制在 20%~30%,超过这个区间用户感知明显。高版本上改用 baseline profile 和 cloud profile 更划算,本质是把「开机全量预热」换成「按实际使用预热」。
5.3 数值口径类
现象:cat /proc/meminfo算出来可用内存 1.2GB,设置里「运行中」显示只剩 700MB,两个人拿着两个数对不上,会开了两小时。原因:设置页显示的可用量是MemFree + Cached - SECONDARY_SERVER_MEM,框架给后台服务预留了一块固定空间并从可用量里扣掉,/proc/meminfo里没有这一项。解决:统一口径。做整机内存水位基线用MemAvailable,这个值内核已经综合了可回收的 page cache 和 slab,最接近真实可用量;要和设置页对齐就按框架的公式自己算一遍。所有对比数据在同一份表格里必须用同一个口径,跨口径比较得出的结论一律作废。
提示:做优化前后的数据对比,硬件、固件版本、温度、后台应用列表、屏幕亮度这五个条件必须锁定,任何一项变了,5% 以内的收益都会被噪声吃掉。
6. 把抓取动作固化成一个批处理:让每次回归都有据可查
性能优化做到后期,最大的敌人是「记不清上次是什么数」。我的做法是把一次完整采集写成一个脚本,每次改完固件跑一遍,输出按日期归档,做版本对比时直接 diff 目录。
@echo off REM collect_perf.bat —— 一次抓齐内存、CPU、崩溃、存储、流量的原始数据 set OUT=perf_%date:~0,4%%date:~5,2%%date:~8,2% mkdir %OUT% adb wait-for-device adb root adb shell input keyevent 224 REM 内存与 CPU:三个口径都留,后面按需取 adb shell dumpsys meminfo > %OUT%\meminfo.txt adb shell dumpsys cpuinfo > %OUT%\cpuinfo.txt adb shell procrank -u > %OUT%\procrank.txt adb shell cat /proc/meminfo > %OUT%\proc_meminfo.txt REM 崩溃现场:三个目录一个都不能少 adb pull /data/anr %OUT%\anr adb pull /data/tombstones %OUT%\tombstones adb pull /data/system/dropbox %OUT%\dropbox REM 存储与流量:df 看分区水位,netstats 看各 uid 的收发字节 adb shell df -h > %OUT%\df.txt adb shell dumpsys netstats > %OUT%\netstats.txt REM 内核日志用 dmesg,不要用 cat /proc/kmsg,那个不会返回 adb shell dmesg > %OUT%\dmesg.txt adb exec-out screencap -p > %OUT%\screen.png pause脚本里最容易写错的两行,一个是cat /proc/kmsg,前面踩过,会挂住;另一个是adb pull的目标目录不存在时 Windows 下会静默失败,所以mkdir必须放在最前面。adb root不是所有版本都能成功,user 版本上会返回adbd cannot run as root,这时候procrank那行会输出空文件,脚本继续跑不报错,事后看数据才发现少了一块——所以每个输出文件生成后都该看一眼大小。
传输和存储的量化不用太复杂。WiFi 的连接和 AP 切换耗时抓dumpsys wifi里的状态机时间戳,蓝牙配对抓dumpsys bluetooth_manager,SD 卡读写用dd跑固定块大小取平均。流量这块重点是防偷跑,dumpsys netstats按 uid 聚合收发的字节数,把待机 8 小时前后的差值拉出来,超过阈值的 uid 逐个查是不是有后台保活或者心跳过于频繁。存储容量则是定时清理冗余数据,du -sh /data/*按目录排序,结合df -h看分区水位,把增长最快的目录列出来定清理策略。
从那以后我每次改动固件、哪怕只是删了一行日志,都强制走一遍这个脚本再对比,不看数据不说话。性能优化里最难的不是找到优化点,是证明你改的那一下真的有用,而且没有把别的地方搞坏。希望帮到你。
本文还有配套的精品资源,点击获取