首页 / 资讯中心 / 文章详情

Android系统性能优化:启动、内存、稳定性与adb排障

Android系统性能优化:启动、内存、稳定性与adb排障 ★ FEATURED ARTICLE
简介一份面向Android应用与系统开发者的性能优化技术文档聚焦PAD与车机两类项目的实际调优场景围绕开机速度、响应速度、内存管理、系统稳定性、数据传输、数据流量、存储容量与模块功耗八个维度展开兼顾冷热启动差异、ANR与Crash排查、Monkey及CTS兼容性测试等常见问题。文档为docx格式压缩包内共1个文件约10.32MB以图文说明和命令示例为主章节划分清晰便于按模块检索。相关主题已有687人学习下载适合需要建立系统化优化思路、对照项目排查瓶颈的中高级开发者参考。内容既给出各模块的优化切入点与衡量指标也整理了adb常用命令及adb shell操作说明可用于响应时间测试、设备状态查看与性能调试对PAD功耗受限、车机启动速度要求高等差异也有针对性说明读者可据此梳理优化清单、明确验证方式并减少重复试错。1. 开机速度为什么删日志和驱动比预加载更划算多数人接到「Android 性能优化」任务时第一反应是做加法——加缓存、加预加载、把串行步骤并行化。真到系统层去量会发现开机路径上真正吃时间的是那些没人看、但每一步都要落盘的动作kernel 和中 init 进程打出来的调试日志Android 服务启动时打的 log编译进内核却没接任何外设的驱动模块preload-class 里塞的一长串开机根本用不到的业务类以及刷机后首次启动要全量扫描的预装 apk。这些动作的共同点是串行、依赖 I/O、且不产生任何用户可感知价值。先做减法把这一段砍掉收益比任何并行化都稳定而且不引入回归风险。想搞懂开机优化的底层逻辑得先看清这条启动链上到底谁在排队。2. 响应速度冷启动和热启动的差值究竟出在哪一段开机之后用户对性能的第一感知来自应用。桌面图标点下去到首帧出来中间隔着进程 fork、类加载、资源解析、数据读取四段冷启动和热启动的差距全在这四段里的某些段被省掉与否。要优化就得先把这四段分别量出来而不是笼统看一个「打开慢」。2.1 用高速摄像机和代码埋点量同一件事单一测量方式都有系统性偏差。高速摄像机是外部视角拍的是屏幕真实点亮的时刻客观但受拍摄帧率和光照影响代码埋点是内部视角记录的是应用收到 Intent 到 onResume 结束的时间戳精确但不包含绘制合成和上屏的耗时。两套数据放在一起看差值稳定在某一区间说明测量可信差值忽大忽小说明中间某段常是 SurfaceFlinger 合成或 GPU 上屏不稳定。常见的代码埋点做法是在启动 Activity 起止各打一条带时间的日志用过程时间戳做差如果手上是原生 adb 环境一条命令也能拿到近似值它返回的输出里TotalTime是应用侧耗时WaitTime包含了 AMS 调度等待# 强制冷启动先杀掉进程再从零拉起 adb shell am force-stop com.example.launcher adb shell am start -W -n com.example.launcher/.LauncherActivity # 输出关键字段 # TotalTime: 应用从 startActivity 到首帧的耗时毫秒 # WaitTime: 含 AMS 排队等待的总耗时毫秒-W让命令阻塞直到启动完成再打印适合脚本化循环。连续跑时一定要在每轮之间执行am force-stop否则拿到的全是热启动数据这是最常见的翻车点。TotalTime和WaitTime的差值本身就是信号差值大说明卡在调度层多半是同一时刻有多个应用在被拉起AMS 在排队差值小但TotalTime本身就大才是应用自身的类加载和资源解析问题。2.2 十四款集成 APK 的响应时间矩阵集成应用的数量决定了要量多少组。桌面、电话、联系人、短信、相机、相册、云邮箱、音乐、视频、应用中心、安全精灵、主题中心、设置、文件浏览器这十四款构成了用户 90% 的启动入口。它们有个共同特征都要做大数据的导入导出和频繁的数据交互——相册要扫媒体库联系人要读 provider文件浏览器要遍历目录这些 I/O 密集动作在冷启动时全部压在首帧之前。做法是把每款应用、每种启动模式冷 / 热的组合都跑 N 轮取中位数而不是平均值因为偶发的 GC 或 I/O 抖动会把平均值拉偏。下面这段脚本把矩阵铺开#!/system/bin/sh # 冷热启动响应时间矩阵采集 appslauncher:com.example.launcher/.LauncherActivity dialer:com.example.dialer/.DialerActivity contacts:com.example.contacts/.ContactsActivity camera:com.example.camera/.CameraActivity for item in $apps; do name${item%%:*} comp${item#*:} # 冷启动先强杀再拉起循环 10 次 for i in 1 2 3 4 5 6 7 8 9 10; do adb shell am force-stop ${comp%/*} adb shell am start -W -n $comp | grep -E TotalTime|WaitTime | sed s/^/$name cold / done # 热启动直接拉起进程还在循环 10 次 for i in 1 2 3 4 5 6 7 8 9 10; do adb shell am start -W -n $comp | grep -E TotalTime|WaitTime | sed s/^/$name warm / done done脚本里${comp%/*}是拿走组件名里最后一个斜杠之后的 Activity 部分只留包名给force-stop用。每一轮之间没有 sleep因为-W本身阻塞到启动完成天然形成节拍。采集结果按应用、按冷热分成两组落表一眼能看出哪些应用的冷启动异常——通常是那几个要全量扫盘的。值得注意的是闹钟、计算器、录音机、便签、日历、浏览器、输入法这类轻应用不用单独列进矩阵它们的耗时基本被上面这十四款的共性路径覆盖第三方应用和游戏同理它们的性能瓶颈更多来自自身代码不在系统优化职责内。2.3 锁屏解锁这条被忽略的路径大多数性能测试只看应用启动漏掉了锁屏和解锁。这条路径上挂着两类真实开销一是播放锁屏 / 解锁音效要打开音频通路、加载音频数据、走一次完整的 AudioFlinger 链路二是功耗锁的解除与重新加载涉及 PowerManager 的 wakelock 状态迁移。这两件事都是百毫秒量级的用户滑动解锁时的手感差异主要来自这里。量法和应用启动一样用高速摄像机从手指抬起的帧开始数到桌面首帧出现同时在代码里记录KeyguardViewMediator的解锁开始和结束时间戳。两边对不上时优先怀疑音效——把锁屏音效关掉再测一次如果耗时立刻掉一个台阶说明问题就在音频通路上可以考虑延迟加载音频资源而不是开机就初始化。3. 内存管理Java 有 GC为什么还会 OOM「Java 有垃圾回收所以不会有内存泄漏」是流传最广的一句错话。GC 回收的是不可达对象判断可达性靠的是引用链——只要还有一条引用指向某个对象哪怕业务上早就不用了GC 也不敢动它。这些被业务逻辑遗弃、但被静态变量、单例、内部类、未注销的监听器死死拽住的对象就是泄漏源。它们占的内存永远不会回来直到进程被杀。3.1 引用可达性与进程上限Android 和桌面 JVM 的关键差别在进程模型。每款应用跑在自己独立的 Dalvik 虚拟机实例里这个实例由 Zygote 服务进程 fork 出来共享 Zygote 预加载的类和方法但堆是私有的。系统给每种类型的进程分配了内存使用上限普通应用通常在 16MB 到 24MB 之间具体值随设备配置浮动。应用运行中堆占用若越过这个上限系统不会让整个机器慢下来而是直接把进程杀掉。这个设计本身是保护性的——单个应用的内存泄漏只影响它自己不会拖垮系统system_process 这类系统进程出问题才会引起整机重启。代价是泄漏的后果被放大成「应用闪退」用户看到的是崩溃实际原因是内存涨过了阈值。所以排查内存问题的第一件事不是找泄漏而是确认现象属于哪一类堆占用持续单调上涨、GC 后回落不到基线是泄漏堆占用正常但瞬间分配了大对象大图、大数组是 OOM 而不是泄漏堆占用和 PSS 都在涨但 GC 能回收是缓存策略问题不是泄漏。三类问题的解决路径完全不同混在一起查会浪费大量时间。3.2 VSS / RSS / PSS / USS 四种口径查内存绕不开这四个指标它们的差别在共享库怎么算指标全称计算口径适用场景VSSVirtual Set Size虚拟地址空间总量含已映射未使用的部分判断进程地址空间规模几乎不反映真实占用RSSResident Set Size实际驻留物理内存共享库被完整计入每个进程快速看总量会严重高估PSSProportional Set Size共享库按共享进程数均分后计入系统级内存统计的默认口径USSUnique Set Size只算进程独占的物理内存不含共享判断某进程被杀能释放多少内存举个具体场景一个应用加载了 4MB 的共享库另有三个应用也加载了同一个库。RSS 口径下每个进程都记 4MB四个进程加起来是 16MBPSS 口径下每个进程只记 1MB合计 4MB和真实物理占用对得上。所以系统统计和杀进程策略看 PSS判断「干掉它能省多少」看 USS。对应的 adb 采集命令# 按 PSS 排序所有进程定位内存大户 adb shell procrank # 只看某个包的详细内存构成 adb shell dumpsys meminfo com.example.launcher # 看某进程虚拟内存区域的逐段映射 adb shell cat /proc/1234/smaps # 按 USS 降序判断杀谁最省内存 adb shell procrank -uprocrank的输出四列并存VSS / RSS / PSS / USS正好用来做交叉验证某进程 RSS 大但 USS 小说明它主要是在用共享库杀掉收益有限USS 大才是真正的内存消耗者。dumpsys meminfo加包名会给出 Dalvik Heap、Native Heap、Stack、Code、Private Dirty 等分项泄漏排查时重点看 Dalvik Heap 的 PSS 随时间的曲线。3.3 /proc/meminfo 里每个字段到底谁在用整机内存状态看/proc/meminfo但这个文件有三十多个字段真正对优化决策有意义的只有几个adb shell cat /proc/meminfo # 关键字段解读 # MemTotal: 所有可用 RAM已扣除内核保留和二进制代码占用 # MemFree: 完全空闲的物理页LowFree 与 HighFree 之和 # Cached: 文件页缓存可直接回收不算「已用」 # Buffers: 块设备元数据缓冲通常很小 # Active / Inactive: LRU 链表两端决定回收顺序 # Slab: 内核数据结构缓存SReclaimable 可回收 # SwapTotal / SwapFree: 交换区移动设备通常为 0系统「可用内存」的正确算法是MemFree Cached不是单看MemFree。设置里的「运行中」页签显示的数字还要再减掉一个二级服务器预留值那是 Settings 应用自己算的展示口径不能拿来当真实余量。所以常见的一个误判是看设置里剩余内存很少以为系统吃紧实际上 Cached 里躺着几百兆随时可回收的页缓存真实可用很宽裕。HighTotal / LowTotal 这对字段在老设备上还有意义——高地址内存区大致 860MB 以上主要给用户空间和页缓存用内核访问它要走不同的映射手法比低地址慢低地址区则留给内核自己的数据结构。新设备上这个区分已经弱化但看到 HighFree 远大于 LowFree 时说明缓存压力集中在低端区可能影响内核态分配效率。4. 系统稳定性从 ANR 现场到 tombstone 符号还原内存问题的外显形式是崩溃而崩溃和 ANR 的排查本质上是一场「现场还原」——设备上留下的日志是唯一的黑匣子能不能读懂它决定了排查要花一天还是十分钟。Android 把不同性质的现场分门别类放在固定路径下每个路径对应一种失败模式。4.1 ANR 与 Crash 的落盘位置四个位置要记牢它们覆盖了 95% 的异常现场/data/anr/—— ANR 现场里面是 ANR 发生瞬间所有线程的堆栈快照traces 文件里每段一个进程/data/tombstones/—— native crash 现场每个文件对应一次信号触发的崩溃/proc/kmsg—— 内核环形缓冲区的当前内容适合看正在发生的内核告警/proc/last_kmsg—— 上一次重启前内核日志的存档整机重启类的疑难问题只能靠它。ANR 的 traces 文件读法是先找main线程再看它的状态。如果主线程停在BinderProxy.transact或某个synchronized块上说明它在等别的进程或等锁如果停在MessageQueue.nativePollOnce说明消息队列空了主线程其实在正常等待ANR 的成因在别处比如输入事件分发超时。这两种情况的修复方向完全不同前者要去查被依赖方为什么慢后者要去查输入事件的分发链路。主线程之外还要看有没有 CPU 占用异常高的线程或者大量线程同时 BLOCKED 在同一把锁上——那个锁的持有者往往才是真凶。一个经验法则ANR 的 traces 里如果主线程状态是Runnable且 CPU 占用饱和问题在算力如果是Blocked问题在锁如果是Native问题在 JNI 或驱动。4.2 addr2line 把十六进制地址还原成函数名tombstone 文件里的调用栈长这样#00 pc 00012345 /system/lib/libxxx.so。十六进制偏移量本身没有意义得用工具翻译成文件名和行号。工具链是交叉编译用的那一套arm-eabi-addr2line负责翻译地址arm-eabi-nm负责导出符号表arm-eabi-objdump负责反汇编# 1. 用 addr2line 把崩溃地址翻译成源码位置 # -e 指定对应的 so 文件必须是带符号的版本 arm-eabi-addr2line -e libxxx.so 0x00012345 # 2. 用 nm 导出整个 so 的符号表按地址排序 # -l 显示行号-C 反解 C 符号-n 按地址排序-S 显示大小 arm-eabi-nm -l -C -n -S libdvm.so dvm.data # 3. 用 objdump 反汇编定位崩溃点附近指令 arm-eabi-objdump -C -d libc.so libc.s第一条命令的输出直接是xxx.cpp:123形式的源码位置前提是 so 文件没被 strip 掉调试信息。生产版本的 so 通常都 strip 过所以设备上跑的和本地留的必须是同一份构建产物否则地址对不上——这是最容易踩的坑拿旧版本 so 去翻译新版本崩溃地址得到的函数名完全是错的还会误导后续排查。nm导出的符号表特别适合批量分析崩溃地址落在哪个符号的地址区间内就说明崩在哪个函数里即使没有精确到行号也能定位到模块。objdump反汇编则是最后手段只有在怀疑是编译器优化导致的异常跳转时才需要看指令级。4.3 Monkey 和 CTS 的达标口径稳定性不能只靠人工点要有可重复的压测口径。Monkey 是最容易上手的随机事件压测# -v 输出详细日志-p 限定被测包100000 是事件数 adb shell monkey -v -p com.example.launcher 100000 # 复现某次崩溃拿到上次的 seed 值用它重跑 adb shell monkey -v -p com.example.launcher -s 1234567890 100000-s参数是随机种子每次跑完日志开头会打印本次的 seed记下来就能精确复现同样的操作序列。没有 seed 的 Monkey 测试等于没测——出了崩溃也无法重现。CTS 兼容性测试的口径不同它测的不是压不压得住而是系统 API 行为是否符合 Android 兼容性定义。CTS 挂项通常意味着虚拟机或框架层做了非标准改动比如替换了原生类加载器、改写了某个系统服务的返回语义。这类问题不适合当 bug 修而是要评估改动是否必要——因为一旦 CTS 挂项第三方应用就可能在某条冷门路径上崩掉而且这种崩溃在自家测试里永远复现不出来。5. 常见问题排查adb 采集链路上最容易翻车的几个点前面的分析都依赖数据采集而采集链路本身就会出问题。下面几条是实际项目里反复出现、且每次都要重新踩一遍的。5.1 现象logcat 拉了半天崩溃现场却不在里面原因Android 的 log 缓冲区是环形结构默认容量有限主缓冲区通常 256KB按设备而异高频日志会瞬间把老日志冲掉。崩溃前的上下文尤其容易被冲——因为崩溃瞬间系统本身要打大量日志。另外logcat默认只读 main 缓冲区system、crash、events 这几个是独立缓冲区。解决抓日志时把几个缓冲区全开并且优先落盘而不是刷屏# 一次读全部缓冲区带时间戳落盘 adb logcat -b main,system,crash,events -v time all_log.txt # -s 只输出指定 tag例如只看崩溃相关 adb logcat -b crash -v time -s AndroidRuntime DEBUG另一个常被忽视的点是dmesg——内核态的空指针、OOM killer 记录、驱动异常都在里面用户态 logcat 完全看不到。整机重启类问题必须同时拿/proc/last_kmsg它是上一次生命周期结束前的内核日志存档重启后依然存在。5.2 现象procrank 显示某进程 80MBdumpsys meminfo 显示 40MB情绪稳定地不知道该信谁原因两个工具的口径不同。procrank默认输出 VSS / RSS / PSS / USS 四列PSS 是共享库均分后的值dumpsys meminfo给的是应用进程视角的分项TOTAL PSS 理论上应该和 procrank 的 PSS 列接近但如果两次采集之间隔了几秒内存状态已经变了。解决同一次排查里固定一个工具做趋势对比跨工具只做量级校验。要判断泄漏用dumpsys meminfo连续采样同一个包看 Dalvik Heap 的 PSS 曲线要判断系统级内存压力用procrank -u看 USS 排序。两个工具的采集时间戳要记下来差值超过两秒基本没法直接比对。还有一个隐藏问题dumpsys meminfo的数值包含 Debug 和 Release 两种构建的差异Debug 版本有额外的调试开销拿 Debug 包测出的内存曲线不能代表线上表现。5.3 现象tombstones 目录 pull 回来是空的原因tombstones 和 anr 目录的读取权限受限普通 shell 用户读不到另外抓取时如果设备还没完成写入崩溃刚发生就 pull文件还没落盘。解决adb pull /data/tombstones前先adb root。如果设备不支持 root用adb shell run-as或直接从bugreport里取。批量抓取最好写成一个批处理把现场一次性打包# 一次性抓取全部崩溃现场先等设备就绪 adb wait-for-device adb root adb pull /data/anr/ anr/ adb pull /data/tombstones/ tombstones/ adb pull /data/system/dropbox/ dropbox/ adb shell ps process_list.txt adb shell procrank procrank.txt adb shell dumpsys dumpsys.txt adb shell dmesg kmsg.txt # 截屏辅助判断崩溃时的界面状态 adb shell screencap /sdcard/screenshot.png adb pull /sdcard/screenshot.png screenshot.pngadb wait-for-device是这套脚本的保险设备没连上时后面的命令会全部失败。adb root之后 adb 服务会重启一次紧随其后的命令可能撞上重连窗口稳妥做法是adb root之后加一次adb wait-for-device。5.4 现象adb remount或adb shell su报权限错误原因remount是把 system 分区从只读重新挂载为可写需要 bootloader 解锁su需要设备本身有 root 权限。这两个操作在正式出货的版本上都不可用只能在工程版本或调试版本上做。解决正式版本不要依赖这类命令做自动化。需要修改系统文件的场景走刷机包重新打包而不是 remount 后手动改——后者改动不持久且会破坏分区校验。调试阶段需要用的话确认设备是工程版本并在脚本里对失败做分支处理别让一个 remount 失败中断整条采集链。6. 设计取舍八大模块不可能同时优化先动哪一刀八个方向——开机速度、响应速度、内存管理、系统稳定性、数据传输、数据流量、存储容量、模块功耗——看上去同等重要实际投入产出比差得很远。取舍的第一原则是按产品形态分而不是按技术模块分。车机和 PAD 的差异是决定性的。车机烧油不烧电池功耗优化基本可以整体降权把资源全部投到启动和响应上同时车机对启动速度有硬性限制且稳定性要求高于消费电子——行驶中死机不是体验问题是安全问题。PAD 走电池功耗有意义但板级支持包是内嵌打包式的模块功耗层面基本无解只能在应用层做削减能做的只是拿不同 BSP 版本做功耗对比确认新版本没有劣化。PAD 部分型号不支持通话短信移动数据流量问题自然消退只剩 WiFi 流量要管。按这个前提排优先级我的经验顺序是优先级模块判断依据1系统稳定性崩溃与 ANR 的修复周期最短、用户感知最强、复发成本最低2开机速度减法优化无回归风险一次投入长期收益3响应速度需要逐应用定位投入大但直接决定口碑4内存管理与稳定性强相关通常合并处理5数据传输WiFi 与蓝牙的链路层调优收益中等6存储容量定期清理 冗余数据删除规则化即可7数据流量应用访问网络的管控防止后台偷跑8模块功耗视 BSP 开放程度决定能做的有限这张表最值得说的是第一行和第二行的次序。稳定性排第一不是因为更重要而是因为它有复利——一个反复崩溃的系统其他所有优化都会被用户归零感知而开机速度排第二是因为它的优化手段几乎全是删减删掉的日志、驱动、服务不会在别处引发新问题风险最低。反过来功耗优化排在最后不是它不重要而是它的可控空间取决于 BSP 是否开放投入进去可能一无所获。验证层面的取舍也值得说一句。高速摄像机测响应时间客观但成本高代码埋点便宜但看不到绘制上屏两者必须配合使用任何单一手段得出的数字都不该直接拿去做决策。Monkey 和 CTS 是两条独立的防线前者测随机路径的健壮性后者测系统 API 的兼容性缺一条都会留下盲区。从那以后我每次接手一个性能优化项目都强制先把八个模块列成表逐项标注「可优化空间」和「验证手段」两列填不出验证手段的直接划掉——因为没法验证的优化等于没做。这份清单能省掉大量「看起来很忙但拿不出数据」的返工。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站