简介:《android性能优化》是一份面向 Android 应用开发、系统调优与测试人员的专题文档,重点讨论 PAD 和车机场景下的性能瓶颈与优化路径,帮助读者从开机、交互到后台资源管理建立系统化排查思路。压缩包共 1 个 docx 文件,大小约 10.32MB,内容按开机速度、响应速度、内存管理、系统稳定性、数据传输、数据流量、存储容量和模块功耗八大方向组织,并涉及桌面、相机等 14 类集成 APK 的冷热启动测试、锁屏解锁耗时统计、Benchmark 跑分、Monkey 与 CTS 兼容性验证,以及 WIFI、蓝牙、SD 卡读写和 adb 调试命令等细节。已有 686 人学习下载,适合需要形成性能测试清单、定位 ANR/Crash 与内存泄漏问题,或为车机、PAD 项目制定专项优化方案的读者参考。
1. 从两台设备的差异说起:性能优化先划边界,再谈工具
多数人一提 Android 性能优化,脑子里冒出来的是几条命令、几个虚拟机参数。真接手量产项目才会发现,难的不是工具,是边界:一台插电工作的车机,和一台靠电池跑的平板,优化清单本来就不该是同一份。车机不靠电池供电,功耗优先级自然往后排,但对启动速度和长时间运行的稳定性要求,比普通消费电子高一截;平板的 BSP 是以 Bin 内嵌打包的形式交付,模块级功耗基本无解,只能拿不同 BSP 版本做对比确认是否达标;再加上部分型号不支持电话短信,移动数据流量这一块直接可以划掉,只剩 WIFI 流量要管。想清楚哪些能做、哪些做不了,剩下的工作才谈得上排期。这里要展开的,是一套覆盖开机速度、响应速度、内存管理、系统稳定性、数据传输、数据流量、存储容量、模块功耗八个模块的完整优化方案,适合正在做毕设、课设,或需要交付一份系统级性能方案的人直接参考。
2. 优化范围与指标口径:八大模块的优先级划分与基线采集
性能优化最容易翻车的地方,不是技术难度,是前面没把"优化到什么程度算完成"讲清楚。八个模块一起铺开,每个都做一点,最后哪个都拿不出数据。我一般的做法是先按设备形态裁剪范围,再给每个模块定一个能被复现的量化口径,最后把采集动作固化成脚本。这样后面无论谁接手,拿到的都是同一套尺子量出来的数。
2.1 车机与 PAD 的取舍:先划掉做不了的事
车机和平板虽然共用一套 Android 基线,但优化侧重完全不同,这一点必须在方案第一页就写死。车机由车辆供电,续航不是问题,所以模块功耗只做"不劣化"的守线,不作为主攻方向;反过来,车机的启动时间和长时间运行的稳定性权重最高,因为它一上车就要可用,中途卡死或重启的代价远高于手机。平板这边,BSP 以 Bin 内嵌方式打包,意味着我们拿不到 BSP 内部模块的细粒度控制权,模块功耗只能退化成"版本间对比",即刷不同 BSP 版本、在相同场景下记录电流曲线,判断新版本是否比旧版本更差。至于不支持电话短信的型号,移动数据流量这一整块直接不做,只保留 WIFI 流量治理。
这样裁剪完,八个模块的优先级就出来了:开机速度、响应速度、内存管理、系统稳定性是四个主攻方向,数据传输、数据流量、存储容量是三块常规治理,模块功耗在车机上守线、在平板上做版本对比。方案里把这层写清楚,评审时就不会有人追问"为什么功耗优化只做这么点"。
2.2 八大模块的量化口径与验收线
口径不统一,优化结果就没法对比。比如"开机快了 3 秒"这句话,如果不说明是从按下电源键算起,还是从 kernel 打印第一行算起,换个人测就是另一个数。八个模块我通常按下面这套口径落地,每一项都要求"同一台机器、同一版本、同一场景、连续 5 次取中位数"。
| 模块 | 量化指标 | 采集方式 | 参考验收线 |
|---|---|---|---|
| 开机速度 | 上电到桌面可点击(秒) | logcat events 缓冲区时间戳 | 相比基线版本降低 15% 以上 |
| 响应速度 | 14 个集成应用冷/热启动耗时(毫秒) | am start -W 的 TotalTime | 冷启动中位数不超过 1200ms |
| 内存管理 | 常驻进程 PSS 总和、单进程 PSS 增长趋势 | procrank -u / dumpsys meminfo | 8 小时压测 PSS 无单调上涨 |
| 系统稳定性 | 每千次操作 ANR 数、Crash 数 | Monkey 结果 + dropbox 统计 | ANR 率低于 0.1%,无必现 Crash |
| 数据传输 | WIFI 重连耗时(秒)、SD 卡顺序读写(MB/s) | 脚本计时 + dd | 重连不超过 5s,写入不低于基线 |
| 数据流量 | 待机 24 小时后台流量(MB) | /proc/net/dev 差值 | 待机零业务流量 |
| 存储容量 | /data 可用空间、冗余文件占比 | du -sh 排序 | 连续使用 7 天可用空间不塌陷 |
| 模块功耗 | 典型场景电流(mA) | 外部电流表 + batteryinfo | 不高于对比 BSP 版本 |
这张表的价值在于,每个数字后面都跟着采集方式,换个同事来测也能测出同一个结果。毕设或课设里如果只写"提升了性能",答辩时基本会被追问到答不上来;把口径写在方案里,等于提前把问题堵死。
2.3 基线采集:把 adb 命令体系固化成一套脚本
采集动作一定要脚本化。手工敲命令,一是慢,二是每次敲的顺序不同,抓下来的状态就不一致,后面做对比全是玄学。Android 自带的 adb 体系足够覆盖绝大部分采集需求,关键在于组合方式:先用 wait-for-device 保证设备就绪,再按"构建信息 → 进程内存 → CPU → 日志 → 崩溃现场"的顺序抓,最后统一落到一个带时间戳的目录里。
# baseline.sh:一次抓全,避免多次插拔设备导致状态漂移 set -e STAMP=$(date +%Y%m%d_%H%M%S) OUT=baseline/$STAMP mkdir -p $OUT adb wait-for-device # 阻塞直到设备可用,避免抓到空数据 adb shell getprop ro.build.display.id > $OUT/build.txt # 记录构建号,对比时先对齐版本 adb shell procrank -u > $OUT/procrank.txt # 各进程 VSS/RSS/PSS/USS,含未映射部分 adb shell dumpsys meminfo > $OUT/meminfo_all.txt # 全局内存明细 adb shell dumpsys cpuinfo > $OUT/cpuinfo.txt # 各进程 CPU 占用占比 adb shell top -n 1 -m 10 > $OUT/top10.txt # 只刷一次,取占用最高的 10 个进程 adb shell logcat -v time -d '*:W' > $OUT/logcat_w.txt # 只留 warning 及以上,避免文件爆炸 adb pull /data/anr/ $OUT/anr/ 2>/dev/null || true # ANR 现场,可能不存在 adb pull /data/tombstones/ $OUT/tombstones/ 2>/dev/null || true adb pull /data/system/dropbox/ $OUT/dropbox/ 2>/dev/null || true echo "collected -> $OUT"脚本里几个参数值得单独说。procrank -u会按进程独自占用的物理内存降序排列,-u表示把未映射(unmapped)部分也算进去,排查泄漏时这个视图比ps有用得多。logcat -d是取完即走,不加-d会一直挂着刷屏,脚本就卡住了。*:W是过滤级别,只保留 warning 及以上,日常采集如果全量抓,单次文件能到几十兆,对比时根本翻不动。top -n 1只刷新一次就返回,适合脚本;要看趋势就换vmstat -n 3,每 3 秒一行,观察进程、内存、IO 和 CPU 的变化。
关于内存,新手最容易在 VSS、RSS、PSS、USS 四个值上犯迷糊。VSS 是虚拟耗用内存,包含共享库,数字最大但参考价值最低;RSS 是实际使用的物理内存,同样包含共享库,多个进程共享同一个库时会被重复计算;PSS 按比例分摊共享库,是横向比较进程内存时最该看的指标;USS 是进程独自占用的物理内存,不包含共享库,判断单进程是否泄漏时最准。方案里统一约定:横向比较看 PSS,纵向看增长看 USS,procrank输出的就是这几个值。
3. 开机速度与响应速度:从 kernel 到桌面图标的启动链路裁剪
开机慢这件事,最怕上来就改代码。正确的顺序是先量出时间花在哪一段,再决定砍什么。Android 的启动链路可以切成四段:bootloader、kernel、init 与 zygote/system_server、桌面可交互。前半段靠日志时间戳就能定位,后半段要靠 events 缓冲区里的 boot_progress 打点。量完之后你会发现,真正吃时间的往往不是驱动本身,而是一堆"调试期留下的东西"在量产版本里还开着。
3.1 四段启动链路拆解与日志裁剪
量化起点用 events 缓冲区,它记录的是系统级事件,比普通 logcat 干净得多。常用的几个打点是boot_progress_start(进入 Android 用户空间)、boot_progress_system_run(system_server 启动)、boot_progress_pms_start和boot_progress_pms_ready(包管理扫描开始与完成)、boot_progress_enable_screen(可交互)。把相邻两个时间戳一减,就知道时间花在包扫描上还是花在服务启动上。
# 抓一次开机过程的阶段打点,按时间排序后人工比对区间 adb logcat -b events -d | grep -E "boot_progress|sf_stop_bootanim"裁剪日志是第一件要做的事,也是性价比最高的一件。kernel 侧通过命令行参数把打印等级压下去,同时在 defconfig 里关掉一批只为调试存在的选项;init 侧去掉不必要的on boot动作和调试服务;Android 框架侧关掉量产不需要的服务和日志开关。注意这里的取舍逻辑:删的是"不影响功能、只影响观测"的东西,任何涉及状态同步或异常上报的日志都要保留,否则现场出问题时就成了黑匣子。
# 量产 defconfig 裁剪示例:保留 PRINTK 与 DYNAMIC_DEBUG,关掉重调试项 CONFIG_PRINTK=y CONFIG_DYNAMIC_DEBUG=y # CONFIG_DEBUG_KMEMLEAK is not set # 内存泄漏检测,仅调试版本开启 # CONFIG_DEBUG_SPINLOCK is not set # 自旋锁调试,量产关掉 # CONFIG_DEBUG_HIGHMEM is not set # CONFIG_SERIAL_MSM_DEBUG is not set # 串口调试输出,量产必须关配套的内核启动参数里加loglevel=4,只输出 warning 及以上。这一条单独看收益不大,但它影响的是整条启动链路上所有的打印调用,越靠前的阶段收益越明显。init.rc 里被删掉的服务通常包括各种调试桥、测试桩和只在工厂模式用的守护进程,删之前建议先在开发机上注释掉跑一轮 Monkey,确认没有隐式依赖再动手。
至于驱动模块,原则很简单:lsmod或者 defconfig 里能对上的、确实没接外设的,全部去掉。注意别把编译进内核的选项误当成模块删掉,这两类在 defconfig 里的写法不同(=y与=m),改错了会直接起不来,这类改动务必留后悔药——改一个提交一次,每次都能单独回滚。
3.2 preload-class 与预装 APK 的减负
zygote 预加载的类列表是开机时间里的固定开销,列表越长,fork 之前花的时间越多。这份列表在框架代码里维护,默认包含大量常用类。裁剪的思路是:把列表导出,对照项目实际会跑的应用,把确认用不到的类剔除,然后观察首次启动和二次启动的差异。这里有个容易忽略的点,预加载类被砍之后,首个使用到这些类的应用会变慢,所以指标不能只看开机,响应速度那一列也要同步回归。
预装 APK 的影响更直接。开机时包管理器要扫描预装目录,解析每个 APK 的清单、签名和资源,APK 越多、单个越大,扫描耗时越长。方案里的做法是先统计,再决定删哪些。
# 统计预装 APK 的分布,作为裁剪依据 adb shell ls /system/app | wc -l # 系统分区预装数量 adb shell ls /data/app | wc -l # 数据分区预装数量 adb shell pm list packages -f | grep "^package:/system" | wc -l adb shell du -sh /system/app/* | sort -rh | head -20 # 找出体积最大的前 20 个统计出来之后按两条线裁剪:一是功能重叠的(同一类功能装了多套),二是项目里明确不会用的。车机上尤其要注意,很多为了手机形态准备的应用在车机场景里根本不会启动,留着纯属给包扫描加负担。删完之后一定要复测一次首次开机(刷机后第一次启动,此时所有缓存都是冷的)和二次开机,这两个数差得很多,方案里必须分开写。首次开机慢通常是正常的,但慢到用户能感知就是问题,刷机后首次启动的加载时间优化,主要靠减少预装和推迟非必要的初始化任务。
3.3 14 个集成应用的冷热启动测量方案
响应速度的核心是"能不能稳定测出来"。项目里锁定的是 14 个集成应用:桌面、电话、联系人、短信、相机、相册、云邮箱、音乐、视频、应用中心、安全精灵、主题中心、设置、文件浏览器。选这 14 个的理由是它们都涉及大量数据的导入导出和频繁的数据交互,属于最容易被用户感知到卡顿的场景。其余像闹钟、计算器、录音机、便签、日历、浏览器、输入法、天气以及第三方应用和游戏,性能问题大多能被这 14 个覆盖,不单独列为测量对象。
测量手段有两套,互为交叉验证。一套是高速摄像机,逐帧看屏幕从点击到首帧出现的过程,优点是完全符合用户真实感知,缺点是成本高、逐帧分析慢,一般只在版本对比的关键节点用。另一套是代码内置测量,用am start -W直接拿到启动耗时,日常回归用它。
# 冷启动测量:先 force-stop 清掉进程,再启动并计时 for PKG in com.demo.launcher com.demo.dialer com.demo.contacts; do adb shell am force-stop $PKG # 冷启动前提:进程不在 adb shell am start -W -n $PKG/.MainActivity | \ grep -E "TotalTime|WaitTime" | sed "s/^/$PKG /" sleep 2 # 给系统一点时间回收,避免连续测量互相干扰 done-W会等待启动完成并输出三个值:TotalTime是应用自身从创建到首帧绘制完成的时间,WaitTime是从发出命令到拿到结果的总时长,Status表示是否超时。做横向对比时用TotalTime,因为它剔除了 am 命令本身的开销。热启动就是把force-stop去掉,此时进程还在,测的是回到前台的时间。两个数必须分开记录,冷热差得巨大是正常现象,把它们混在一起算平均是没有意义的。
测量里有几个必须固定下来的变量:同一台机器、同一版本、同一亮度、同一后台负载、连续 5 次取中位数。后台负载尤其容易被忽略,如果测量时后台正在跑同步或扫描,数据会明显偏高。我习惯在测量前统一执行一次清理,把非系统进程全部杀掉,再开始跑。
3.4 锁屏解锁链路与功耗锁释放
锁屏和解锁这两个动作经常被漏掉,但它们其实是一次完整的性能事件:期间要播放解锁音效、加载或解除功耗锁、刷新壁纸和状态栏,任何一环卡住都会让用户觉得"这机器反应慢"。测量方式是在代码里打点,记录Keyguard解除开始到桌面真正可交互的时间戳,再配合高速摄像机抽查几次,确认打点位置没有偏差。
# 解锁过程相关状态与 wakelock 的观测 adb shell dumpsys window | grep -i keyguard # 当前锁屏状态 adb shell dumpsys power | grep -i wake # 当前持有的 wakelock 列表优化方向主要有两个。一是音效资源,锁屏解锁音如果是一段较长的音频文件,解码和播放就会挤占这段时间,替换成更短的音频或者改为异步播放都能缓解。二是功耗锁的申请与释放时机,很多实现是在解锁瞬间同步申请一批 wakelock,再在某个超时后统一释放,这个同步申请的过程会阻塞主线程。改成按需申请、释放走异步,解锁时间的改善通常能到百毫秒级。这部分改动风险不低,涉及电源管理,改完必须跑一轮完整的待机功耗对比,确认没有引入额外的耗电。
4. 内存管理与系统稳定性:从 meminfo 水位到 tombstone 符号化
内存和稳定性这两块要一起看,因为大量稳定性问题的根因就是内存。Android 给每个应用进程分配了堆上限,常见是 16MB 到 24MB,一旦某个进程因为泄漏涨过这条线,系统就会把它杀掉;如果出问题的是 system_server 这类系统进程,结果就是整机重启。所以内存管理的目标不是"用得越少越好",而是"该释放的能释放、不该涨的不涨"。
4.1 meminfo 字段怎么读,剩余内存到底看哪几个数
/proc/meminfo一屏几十个字段,第一次看很容易懵。实际判断系统还剩多少可用内存,看的是MemFree + Cached,而不是只看MemFree。原因是 Cached 里的页在被回收之前充当文件缓存,一旦有内存压力就能立刻释放出来,本质上属于"随时可用的内存"。设置里的"运行中"页签显示的数字还要再减掉一个固定预留值,那个值是为二级服务预留的,属于系统内部约定,方案里统一按MemFree + Cached口径对账,避免前后对不上。
adb shell cat /proc/meminfo | grep -E "MemTotal|MemFree|Buffers|Cached|SwapTotal|Slab|Shmem"几个关键字段的读法:MemTotal是可用 RAM 总量,已经减掉了内核代码和预留位;Buffers是块设备缓冲,只记文件系统的元数据;Cached是文件缓存,也是回收时最先被释放的部分;Slab是内核数据结构缓存,如果这个值持续上涨而业务没有变化,多半是驱动里有对象没释放;SwapTotal在多数移动设备上是 0,看到非零要确认是否开了交换分区,开了之后内存表现会完全不同,测试结论不能直接套用。低内存设备上还要关注LowTotal/LowFree和HighTotal/HighFree,低位内存是内核自己也要用的区域,LowFree 见底比总可用内存见底更危险。
4.2 内存泄漏与 OOM 的定位路径
先纠正一个常见误解:Java 有垃圾回收,不代表没有泄漏。只要还有引用指向一个已经不再使用的对象,回收器就动不了它,这块内存就一直被占着。Android 的每个应用跑在自己的 Dalvik 虚拟机实例里,由 zygote 孵化,堆上限固定,泄漏累积到超过上限就会抛 OOM 或者被系统直接杀掉。好在影响范围限于自身进程,不至于拖垮整机——除非出问题的是系统进程。相比 C/C++ 那种进程退出都不释放的泄漏,Java 层的可控性要好得多。
定位泄漏的标准路径是"看趋势不看快照"。单次dumpsys meminfo只能告诉你现在占多少,看不出是不是在漏。
# 每 30 秒采一次目标进程的 PSS,连续采 20 次,观察是否单调上涨 for i in $(seq 1 20); do adb shell dumpsys meminfo com.demo.browser | grep -E "TOTAL|Java Heap|Native Heap" \ | sed "s/^/$(date +%H:%M:%S) /" sleep 30 done判读方法:Java Heap持续上涨而Native Heap平稳,通常是 Java 层持有引用没放,重点查静态集合、单例里的 Context、未反注册的监听器和回调;Native Heap上涨则要往 Bitmap、音视频解码器、JNI 层的NewGlobalRef上找。TOTAL里的 PSS 值配合procrank -u的排序,能快速锁定是哪个进程在涨。确认了嫌疑进程之后,再抓堆转储做对象引用链分析,这一步比较重,不建议一上来就做。
除了泄漏,空指针这类低级问题也要纳入检查范围。它们的共同特征是"偶发、难复现、日志里只有一行堆栈",所以方案里要求:所有空指针和 Crash 都必须能从 dropbox 或 tombstone 里找到完整现场,找不到现场的,先补日志再谈修复。
4.3 ANR 与 Crash:日志抓取与符号化还原
ANR 的现场在/data/anr/下,Crash 的现场在/data/tombstones/下,跑分和性能异常的现场在/data/system/dropbox/下。三个目录都要在采集脚本里带上,出问题时第一时间抓,晚了可能被新的事件覆盖。ANR 日志里最关键的是主线程的堆栈,看它卡在锁等待、IPC 调用还是磁盘 IO 上,基本就能定位方向。
Native 崩溃的堆栈默认是一串地址,像libxxx.so 0x00012345,直接看等于看天书,必须符号化。工具链里的三个命令要会用:addr2line把地址翻译成文件名和行号,nm列出符号表,objdump反汇编看具体指令。
# 用带符号的 so 还原崩溃地址对应的源码位置 arm-eabi-addr2line -f -C -e out/target/product/demo/symbols/system/lib/libdemo.so 0x00012345 # 导出符号表,便于人工检索可疑函数 arm-eabi-nm -l -C -n -S libdvm.so > dvm.data # 反汇编指定库,排查指令级问题 arm-eabi-objdump -C -d libc.so > libc.s-f输出函数名,-C做 C++ 符号还原(不加这个参数看到的是被修饰过的名字),-e指定要解析的库文件。这里有个前提:必须用和出问题版本完全对应的带符号库,版本对不上解析出来的位置是错的,这一点吃过亏的话印象会很深。所以方案里要求每次发版都把符号表一起归档,命名带上构建号,别等出事了再回头找。
4.4 Monkey 与 CTS:把稳定性做成可重复的门禁
稳定性不能靠"用着感觉还行"来判断,得有可重复的门禁。Monkey 负责随机压力,CTS 负责兼容性基线,两个都要跑,而且都要有明确的通过标准。
# 随机压力测试:固定 seed 保证可复现,忽略超时和崩溃继续跑,跑满 8 小时 adb shell monkey -s 20240101 -v -p com.demo.launcher \ --throttle 300 --ignore-timeouts --ignore-crashes --pct-touch 40 \ --pct-motion 20 --pct-nav 20 --pct-appswitch 20 28800-s固定随机种子,这一点非常关键:不固定种子,出了问题没法复现,等于白跑。--throttle控制事件间隔,太小会被人为放大成性能问题,太大会跑不完,300 毫秒是个常用起点。--pct-*调整各类事件的比例,车机场景应该提高appswitch的权重,因为用户会频繁在应用间切换。跑的过程中要同步监控 logcat,一旦出现 ANR 或 Crash 立刻记录时间点和包名,跑完汇总成"每千次操作的异常数",这个数比"跑了多久没崩"有意义得多。
CTS 这块的重点不在跑通,而在跑不通的时候怎么收。常见失败分三类:一是系统接口签名或行为与基线不一致,多由框架层改动引起;二是权限与安全策略变更导致的用例失败;三是第三方应用兼容性问题,表现为同一个应用在不同版本上表现不一致。前两类改代码,第三类要建立兼容性清单,把验证过的第三方应用版本固定下来,后续每次发版回归。数据区的及时清理和进程归并也放在这一块做,常驻进程太多会同时拉高内存和稳定性风险,归并策略要结合业务实际,不能一刀切。
5. 传输、流量、存储与模块功耗:外围四块的落地与常见问题排查
前面四块是主攻方向,剩下的四块属于"不做会挨骂、做过头没收益"的类型。这一章的重点不是把每个指标都压到极致,而是给出可执行的测量方法和治理手段,让这四块有据可依、不拖后腿。
5.1 WIFI、蓝牙与 SD 卡:怎么把速度测准
传输速度的测量,最大的坑是"测的不是想测的东西"。WIFI 这块要分两个指标:连接速度和 AP 切换重连速度。前者是首次连上 AP 的耗时,后者是从一个 AP 漫游到另一个 AP 的耗时,后者对车机尤其重要,因为车辆移动时 AP 会不断变化。测量方式是脚本记录状态机切换的时间戳,而不是肉眼看图标。
蓝牙要测配对连接速度和传输速度,配对慢通常是协议栈的扫描间隔设置问题,传输慢则要区分是蓝牙版本限制还是应用层分片策略问题。SD 卡这一块分内置和外置,测的是顺序读写速度,用 dd 就够了。
# 外置 SD 卡顺序写入测速:256MB,bs=1M,conv=fsync 确保真正落盘 adb shell dd if=/dev/zero of=/mnt/sdcard/test.bin bs=1M count=256 conv=fsync # 顺序读取测速:先清缓存,再读,避免命中页缓存导致数字虚高 adb shell "sync; echo 3 > /proc/sys/vm/drop_caches" adb shell dd if=/mnt/sdcard/test.bin of=/dev/null bs=1Mconv=fsync这个参数不能省,不加的话 dd 只是把数据写进页缓存就返回了,测出来的是内存速度不是存储速度。读取前清缓存同理,不清的话第二次读全命中缓存,数字会好看得离谱。测完记得把测试文件删掉,不然它会一直占着存储空间,后面测存储容量时又会来干扰。云同步速度的测量更接近端到端体验,建议按"发起同步到数据可用"的完整链路计时,而不是只测网络层吞吐。
5.2 数据流量治理:谁在后台偷跑
流量治理的目标很明确:防止应用在后台偷跑流量。手段分两层,一层是统计,一层是限制。统计可以直接读网卡计数器的差值,简单可靠;限制则要靠系统层的联网策略,把后台联网权限收起来,非前台应用不允许随意发起网络请求。
# 读取网卡累计收发字节,前后两次相减即为该时段流量 adb shell cat /proc/net/dev | grep -E "wlan0|rmnet"wlan0是 WIFI 接口,rmnet系列是移动数据接口(不支持电话短信的型号上通常不存在或不激活)。做待机流量测试时,要求设备静置 24 小时,前后两次取差值,正常情况下应该接近零。如果发现明显增长,用netstat -ano看当前连接,再结合进程列表定位到具体应用。方案里还要求:应用退出时必须释放网络连接,长连接要有心跳上限,避免为了保活而持续发包。这部分不需要复杂技术,需要的是把规则写进验收清单,每次发版都过一遍。
5.3 存储容量:清理策略要写在系统里,不能靠人
存储容量的问题往往不是"空间不够",而是"该删的没删"。日志、缓存、下载临时文件、崩溃转储,这四类是最常见的空间黑洞。方案里的做法是:先按目录统计占用,排出 Top 项,再针对每一项定清理策略——日志按大小滚动、缓存按时间过期、临时文件在任务结束后立即删除、崩溃转储在采集上报后删除。
# 按占用从大到小排出 /data 下的目录,定位空间黑洞 adb shell du -sh /data/* 2>/dev/null | sort -rh | head -15清理动作要由系统定时触发,不能靠用户手动点。可以挂在设备空闲时执行,避免在用户使用过程中做大量删除引起卡顿。删除策略要有上限保护,比如缓存目录最多占多少、日志最多留几天,超了才删,不超就不动,这样既控制了空间又不会频繁 IO。外置 SD 卡的冗余数据清理同理,但要额外注意别误删用户自己的文件,只清理应用自己创建的目录。
5.4 模块功耗:能做多少做多少,做不了就守住对比基线
车机上功耗不是主攻方向,只要不劣化即可;平板上受限于 BSP 的打包方式,能做的主要在应用层和系统层。可下手的地方有四个:音视频编解码尽量走硬件通路,避免软解带来的持续高负载;背光亮度曲线做平滑过渡,避免频繁大幅跳变;LCD 和触摸屏的刷新频率按场景调整,静态界面降频;modem 模块在无信号或弱信号场景下的搜网策略要收敛,避免长时间高功率搜网。
# 观察功耗相关状态:电池信息、唤醒锁、各服务耗电统计 adb shell dumpsys battery adb shell dumpsys batteryinfo | head -50 adb shell dumpsys power | grep -i "wake lock"dumpsys batteryinfo输出的是各功能模块的耗电统计,适合做粗略排序,找出耗电大户;dumpsys power里的 wakelock 列表能看出谁在阻止系统休眠,长时间持有的 wakelock 是待机耗电的头号嫌疑。对平板这类无法深入 BSP 的设备,最终交付的是一份"版本对比结论":相同场景、相同脚本、不同 BSP 版本,各测三次取均值,判断新版本是否达标。这个结论虽然不产生优化动作,但它是验收的一部分,必须有。
5.5 四块里的常见问题排查:现象、原因与解决
外围四块看起来简单,实际做的时候翻车点不少,下面几条是最常遇到的。
现象一:SD 卡测速结果忽高忽低,同一台机器两次能差一倍。原因多半是测量前没有统一缓存状态,第一次读命中了页缓存,第二次没有;也可能是有后台任务在同时读写。解决办法是固定测量流程:先drop_caches,再立即测量,连续 5 次去掉最高最低取中间值,测量期间杀掉非系统进程。
现象二:待机流量统计出来是负数或者明显偏小。原因是网卡计数器在接口重启时会归零,两次采样之间如果发生过 WIFI 重连,差值就不可信了。解决办法是采样前先记录接口状态,发现重连就重新开始计时,或者改用系统自带的分应用流量统计做交叉验证。
现象三:清理脚本跑完之后可用空间没变。原因是删除的文件还被进程持有句柄,空间要等进程退出才真正释放,du统计的是目录项而不是实际可回收空间。解决办法是清理后主动触发一次垃圾回收或重启相关服务,并在清理前后各记录一次df而不是du。
现象四:功耗对比结论和上一轮完全相反。原因是测试环境变了——屏幕亮度、后台应用、信号强度任意一项不同,电流曲线就会漂。解决办法是把测试场景写成脚本,每次执行前自动设置亮度、清理后台、记录信号强度,环境不一致的数据直接作废。
现象五:WIFI 重连时间测得很好,用户体验依然很差。原因是测量只覆盖了链路层重连,没算应用层重新建连和首包往返的时间。解决办法是把测量终点从"关联成功"改为"首个业务请求返回成功",虽然数字会变难看,但那个数才是用户感知到的。
6. 验收与答辩:把优化结果讲成一条能复现的曲线
验收这件事,我见过最常见的失败方式是把一堆"优化前 2800ms、优化后 1900ms"的数字堆在 PPT 上,然后被问一句"这个数怎么测的"就哑了。真正能站得住的验收,靠的是三样东西:统一的测量口径、可复现的采集脚本、版本间的对比曲线。口径在前面已经定好,脚本在基线采集那一步已经写完,验收环节要做的只是把它们跑一遍,把结果画成图。
答辩或评审时的讲述顺序,我一般按这个来:先讲设备形态决定了哪些模块做、哪些不做,这一段能体现判断力;再讲每个模块的量化口径和采集方式,这一段能体现严谨;然后挑两到三个真正有改善的模块,展示改动前后的对比曲线,注意是曲线不是单点,一条随版本变化的趋势线比一个漂亮的单点有说服力得多;最后讲没做好的部分和原因,比如平板上的 BSP 功耗只能做版本对比、某项指标受硬件限制无法继续压。主动交代边界,比被人问出来强。
| 验收项 | 交付物 | 判定方式 |
|---|---|---|
| 开机速度 | 四段链路时间戳表 + 首/二次开机对比 | 相比基线降低 15% 以上 |
| 响应速度 | 14 个应用的冷/热启动中位数表 | 冷启动中位数不超过 1200ms |
| 内存管理 | 8 小时 PSS 趋势曲线 | 无单调上涨,无 OOM |
| 系统稳定性 | Monkey 报告 + ANR/Crash 统计 | ANR 率低于 0.1% |
| 传输与流量 | 重连耗时表 + 24 小时待机流量 | 重连不超过 5s,待机零业务流量 |
| 存储与功耗 | 空间占用 Top 表 + 版本对比结论 | 7 天不塌陷,不劣于对比版本 |
表格里每一项都对应一个具体文件,评审时随时能调出来看,这比口头描述有力得多。如果时间允许,现场跑一次采集脚本是最有杀伤力的——设备接上、脚本一跑、数据出来,所有的质疑都会自动消失。
还有个细节值得提:所有对比数据必须来自同一台设备。拿 A 机器的优化前和 B 机器的优化后做对比,是这类方案里最隐蔽也最致命的错误,硬件批次不同、闪存颗粒不同,差异可能比优化本身还大。我现在的习惯是每次验收前,先把测试设备的构建号、闪存型号、电池状态全部记一遍,跑完再核对一次,确认中途没被刷过机。从那以后我每次提交性能报告,都强制走一遍"同机、同版本、同场景、连续五次取中位数"的流程,宁可数据难看一点,也不要一个经不起追问的漂亮数字。希望帮到你。
本文还有配套的精品资源,点击获取