1. 项目缘起:为什么我们需要关注安卓启动性能?
如果你是一名安卓系统开发者、设备厂商的工程师,或者是一名热衷于折腾自己设备的发烧友,那么“开机慢”这个问题你一定不陌生。尤其是在开发阶段,每次修改完系统代码,都需要经历一次完整的编译、烧录和重启过程。如果系统启动时间从30秒优化到20秒,看似只节省了10秒,但在日复一日的调试循环中,这节省下来的时间累积起来是相当可观的。更不用说在消费电子领域,开机速度是用户体验最直观的指标之一,直接关系到用户对产品“快”与“慢”的第一印象。
那么,当系统启动变慢时,我们如何定位瓶颈?是内核初始化耗时太长?是某个关键系统服务启动阻塞?还是应用层某个APK的初始化逻辑过于臃肿?靠猜是没用的,我们需要一个能“看见”整个启动过程的工具。这就是bootchart的价值所在。它不是一个主动的优化工具,而是一个强大的性能剖析和可视化工具,能够将安卓系统从内核启动到桌面就绪的整个过程中,所有进程的CPU、I/O和磁盘活动以图表的形式清晰地呈现出来。通过分析bootchart生成的图表,我们可以像看“病例”一样,精准地找到启动过程中的“病灶”——是哪个进程消耗了过多的CPU时间,又是哪个进程在频繁地进行磁盘读写,从而拖慢了整体进度。
网络上关于bootchart的资料不少,但大多比较零散,或是基于较旧的安卓版本。很多开发者在尝试配置时,会遇到各种环境问题、编译问题以及数据采集不全的困扰。本文将基于最新的AOSP主线开发环境,手把手带你完成bootchart从源码配置、数据采集到图表生成的完整流程,并分享我在实际项目中踩过的坑和总结的实用技巧。
2. 深入理解bootchart:它到底采集了什么数据?
在动手配置之前,我们必须先搞清楚bootchart的工作原理。它不是魔法,其核心是数据采集和数据可视化两个部分。在安卓系统中,bootchart的采集端集成在init进程中。init作为用户空间的第一个进程,是所有进程的祖先,由它来负责采集数据再合适不过。
2.1 数据采集的三大支柱
bootchart采集的数据主要分为三类,它们共同描绘了系统启动时的资源竞争全景图:
进程树与生命周期信息:这是最核心的数据。
init进程会周期性地(默认每秒一次)遍历/proc文件系统,读取所有进程的stat、statm和cmdline文件。从中可以获取:- 进程ID (PID)和父进程ID (PPID):用于构建整个启动期间的进程派生关系树。
- 进程名称:让我们知道具体是哪个程序在运行。
- 进程状态:是正在运行(R)、睡眠(S)还是僵尸(Z)等。
- 启动和退出时间戳:精确记录每个进程何时开始运行,何时结束。这对于分析服务启动顺序和依赖关系至关重要。
CPU占用率:系统整体的CPU使用情况。通过读取
/proc/stat文件,可以计算出每个采样周期内,CPU在用户态、内核态、空闲(idle)等状态的时间比例。这能告诉我们系统在启动阶段是CPU密集型还是I/O密集型。磁盘I/O吞吐量:通过读取
/proc/diskstats文件,获取整个系统对各个块设备(如mmcblk0,sda等)的读写次数和扇区数。在安卓设备上,eMMC或UFS存储的性能往往是启动瓶颈,频繁的小文件读写会显著拉长启动时间。通过I/O图表,我们可以一眼看出启动过程中磁盘活动的峰值出现在哪个阶段,对应了哪些进程。
注意:bootchart采集的是系统级的聚合数据,它不采集单个进程的详细函数调用栈(那是
perf或systrace的工作),也不采集具体的内存分配细节。它的优势在于宏观的、时间线式的全景展示。
2.2 bootchart与systrace的定位差异
很多同学会混淆bootchart和systrace。简单来说:
- bootchart:关注进程级的资源和生命周期,时间跨度从内核启动到系统完全就绪(几分钟),用于分析启动阶段的资源竞争和进程调度问题。它的图表是“上帝视角”的甘特图。
- systrace:关注线程级的执行流程和内核事件,时间跨度通常较短(几秒),用于分析应用卡顿、渲染延迟、锁竞争等微观性能问题。它的图表是“显微镜视角”的时序图。
在优化启动速度时,通常先用bootchart找到可疑的时间段和进程,再使用systrace深入该进程内部进行细粒度分析。
3. 实战配置:为AOSP源码启用bootchart
理论清楚了,我们开始动手。这里假设你已经有了一套可以编译的AOSP源码环境(例如,在Ubuntu 20.04/22.04上,源码目录为~/aosp)。
3.1 第一步:配置编译环境,启用bootchart
bootchart的代码位于AOSP的system/core/init/bootchart.cpp。默认情况下,它可能没有被编译进init二进制文件。我们需要通过编译配置来启用它。
安卓的编译系统主要使用BoardConfig.mk和Product配置文件。最直接的方法是在你的设备产品定义中启用BOOTCHART。
查找你的设备产品定义文件。如果你在编译模拟器(如aosp_x86_64-eng),可以修改通用的
aosp_arm64.mk或aosp_x86_64.mk。如果是真实设备,文件通常在device/<manufacturer>/<device>/目录下。# 例如,为eng版本的通用ARM64镜像启用bootchart cd ~/aosp vim build/make/target/product/aosp_arm64.mk在产品的Makefile中添加一行。在文件末尾或其他合适位置添加:
# 启用bootchart数据采集 PRODUCT_BOOTCHART_ENABLED := true这一行配置会确保在编译
init时,定义宏BOOTCHART_ENABLED,从而将bootchart的采集代码编译进去。另一种更灵活的方法:使用环境变量。AOSP的
init也支持在运行时通过androidboot.bootchart这个内核命令行参数来控制。我们可以在编译时,直接修改内核命令行参数。编辑你的设备对应的BoardConfig.mk文件:# 例如,对于模拟器或一些开发板 vim device/generic/goldfish/arm64-v8a/BoardConfig.mk找到
BOARD_KERNEL_CMDLINE的定义,在其中追加:BOARD_KERNEL_CMDLINE += androidboot.bootchart=100这里的数字
100表示采集时长(秒)。例如设为100,表示init会采集从启动开始100秒内的数据。你可以根据你系统的实际启动时间设置一个足够大的值,比如150或200。
实操心得:我强烈推荐使用内核命令行参数的方式。因为它无需重新编译整个系统,只需要重新编译
boot.img(包含内核和initramfs),刷机速度更快,调试效率更高。PRODUCT_BOOTCHART_ENABLED := true的方式通常用于产品级的默认配置。
3.2 第二步:重新编译并刷入系统
启用配置后,需要重新编译boot.img和system.img(如果修改了产品Makefile)。
设置编译环境并编译:
cd ~/aosp source build/envsetup.sh lunch aosp_x86_64-eng # 选择你的目标,例如aosp_arm64-eng make -j$(nproc) bootimage systemimage如果只修改了内核命令行,理论上只编译
bootimage即可。刷入设备。对于模拟器,启动时会自动使用新镜像。对于真实设备,使用
fastboot刷入:fastboot flash boot out/target/product/<device_name>/boot.img fastboot flash system out/target/product/<device_name>/system.img fastboot reboot
3.3 第三步:验证bootchart是否生效
设备启动后,我们需要确认bootchart采集功能已经开启。
连接设备ADB。确保设备已通过USB连接并开启了USB调试,或者如果是模拟器则已经启动。
adb devices # 确认设备在线检查内核命令行:
adb shell cat /proc/cmdline | grep bootchart如果看到输出中包含
androidboot.bootchart=100之类的字样,说明内核参数已生效。检查
init是否在采集。bootchart采集的数据会先临时存放在/data/bootchart目录下。我们可以查看这个目录:adb shell ls -la /data/bootchart/在设备启动后立即执行此命令,如果bootchart正在工作,你应该能看到一些以时间戳命名的目录(如
/data/bootchart/2025-04-10-15-30-00),里面包含header、proc_stat.log、proc_ps.log、proc_diskstats.log等文件。如果目录不存在或为空,请检查前面的配置步骤。
踩坑记录:有时即使配置了参数,
/data/bootchart目录下也没有数据。一个常见的原因是SELinux策略。在enforcing模式下,init进程可能没有权限在/data分区创建目录或写入文件。临时解决方案是将设备切换到permissive模式进行调试:adb shell setenforce 0。但这不是长久之计,正式产品中需要在SELinux策略文件(.te文件)中为init域添加对data_bootchart目录的读写权限。
4. 数据提取与图表生成:让数据“说话”
采集到数据后,下一步是把这些原始日志转换成直观的图表。AOSP源码中自带了一个Python脚本工具来完成这个工作。
4.1 提取原始数据
首先,我们需要将设备上的bootchart数据打包并拉取到开发主机上。
- 在设备上执行打包脚本。AOSP在
system/core/init/目录下提供了一个grab-bootchart.sh脚本。最简单的方法是直接使用ADB shell来调用设备上可能存在的这个脚本,但更可靠的方法是使用主机上的脚本去拉取。
如果没有# 在开发主机上,进入AOSP源码目录 cd ~/aosp # 使用源码中的脚本,它会自动执行adb命令打包并拉取数据 sudo system/core/init/grab-bootchart.shsudo权限,或者脚本执行失败,我们可以手动操作:
解压后,你会得到一个以时间戳命名的目录,里面就是原始的日志文件。# 1. 在设备上打包/data/bootchart下的数据 adb shell 'cd /data && tar -czf /sdcard/bootchart.tgz bootchart' # 2. 将打包文件拉取到主机 adb pull /sdcard/bootchart.tgz . # 3. 解压 tar -xzf bootchart.tgz
4.2 安装依赖并生成图表
生成图表的工具bootchart.py依赖于Python的drawing库(通常通过reportlab实现)来绘制PNG图片。我们需要先安装依赖。
安装Python及Pillow库。现代系统中
bootchart.py可能使用Pillow(PIL的分支)进行图像绘制。# 在Ubuntu/Debian上 sudo apt-get update sudo apt-get install python3 python3-pip pip3 install Pillow # 如果提示权限问题,可以使用 --user 选项 pip3 install --user Pillow使用AOSP中的工具生成图表。AOSP在
system/core/init/目录下也有一个bootchart.py脚本,但它可能是一个包装器。更通用的工具位于external/bootchart目录下。cd ~/aosp/external/bootchart python3 bootchart.py ~/path/to/your/bootchart/directory例如,如果你的数据目录是
/tmp/bootchart/2025-04-10-15-30-00,则命令为:python3 bootchart.py /tmp/bootchart/2025-04-10-15-30-00执行成功后,会在当前目录下生成一个
bootchart.png图片文件。如果遇到“No module named 'drawing'”错误,说明脚本使用的是旧的
drawing模块。你可以尝试安装reportlab库,并修改bootchart.py脚本中的引用。或者,使用一个更现代、维护更好的第三方bootchart工具,比如从GitHub上获取的pybootchartgui。pip3 install --user pybootchartgui # 使用pybootchartgui渲染 bootchart ~/path/to/your/bootchart/directorypybootchartgui通常会生成更美观、交互性更好的SVG格式图表,并且支持缩放和查看详细信息,强烈推荐。
4.3 解读bootchart图表
生成的图表信息量巨大,我们来看关键部分:
顶部区域 - CPU和I/O利用率曲线:两条曲线分别表示CPU占用率和磁盘I/O吞吐量随时间的变化。纵坐标是百分比。如果CPU长时间处于100%饱和状态,或者I/O曲线出现持续的高峰,那么对应的时段就是优化重点。
中部主体区域 - 进程甘特图:每一行代表一个进程。横轴是时间线。每个进程的条形块长度代表了它的存活时间,颜色深浅可能代表CPU使用强度(不同工具渲染效果不同)。通过这个图,你可以清晰地看到:
- 进程启动的先后顺序:哪些服务是并行启动的,哪些是有严格的先后依赖。
- 进程的生命周期:有些进程启动后很快结束(如一些初始化脚本),有些则持续运行(如
system_server,surfaceflinger)。 - 资源竞争:当多个进程的条形块在时间线上重叠,并且顶部CPU/I/O曲线很高时,说明它们可能在竞争资源。
进程树结构:图表通常会以缩进形式显示进程的父子关系,这有助于理解进程的派生关系。
分析案例:假设图表显示,在启动后第10秒到第15秒,CPU利用率达到100%,同时段有dex2oat(Android运行时编译服务)和system_server在大量活动。这表明系统正在激烈地进行应用预编译和系统服务初始化,这个阶段可能就是启动瓶颈。优化方向可以考虑:能否将部分dex2oat工作推迟到后台?system_server中初始化的服务能否减少或延迟加载?
5. 高级技巧与疑难排查
掌握了基础流程后,下面分享一些能提升效率和处理常见问题的进阶技巧。
5.1 自动化数据采集与分析脚本
在反复调试时,手动执行ADB命令很繁琐。可以编写一个简单的Shell脚本来自动化整个过程:
#!/bin/bash # auto_bootchart.sh DEVICE_SERIAL=$1 # 可以传入设备序列号,用于多设备情况 BOOTCHART_DURATION=120 echo “1. 重启设备并开始采集...” adb -s $DEVICE_SERIAL reboot sleep 5 # 等待设备进入bootloader或开始启动 echo “2. 等待设备启动完成...” adb -s $DEVICE_SERIAL wait-for-device # 等待系统服务完全启动,而不仅仅是adb连接 sleep $BOOTCHART_DURATION echo “3. 提取bootchart数据...” adb -s $DEVICE_SERIAL shell ‘cd /data && tar -czf /sdcard/bootchart.tgz bootchart 2>/dev/null’ adb -s $DEVICE_SERIAL pull /sdcard/bootchart.tgz . TIMESTAMP=$(date +%Y%m%d_%H%M%S) mkdir -p bootchart_logs tar -xzf bootchart.tgz -C bootchart_logs/ mv bootchart.tgz bootchart_logs/bootchart_$TIMESTAMP.tgz echo “4. 生成图表...” LATEST_LOG=$(ls -dt bootchart_logs/*/ | head -n1) if [ -n “$LATEST_LOG” ]; then # 使用pybootchartgui bootchart $LATEST_LOG --output=“bootchart_$TIMESTAMP.svg” echo “图表已生成: bootchart_$TIMESTAMP.svg” else echo “未找到bootchart日志!” fi5.2 常见问题与解决方案
/data/bootchart目录为空:- 检查内核参数:
adb shell cat /proc/cmdline确认androidboot.bootchart已设置。 - 检查SELinux:运行
adb shell getenforce。如果是Enforcing,尝试adb shell setenforce 0后重启再试。长期方案需修改SELinux策略。 - 检查
init日志:adb logcat -b all | grep -i bootchart,查看是否有相关错误信息。 - 确认存储空间:
/data分区是否已满?
- 检查内核参数:
生成的图表时间轴很短或数据不全:
- 采集时间不足:增大内核参数中的时间值,如
androidboot.bootchart=200。 init进程提前结束采集:bootchart采集在init完成启动阶段后会停止。如果系统启动很快,可能在你拉取数据前,init已经清理了临时文件。可以尝试在init.rc文件中添加一个service,在启动完成后立即打包数据。
- 采集时间不足:增大内核参数中的时间值,如
使用
pybootchartgui时渲染失败:- 确保安装的
pybootchartgui版本与Python3兼容。 - 如果遇到
TypeError,可能是Python库版本问题。可以尝试在虚拟环境中安装指定版本:pip3 install pybootchartgui==0.14.5。
- 确保安装的
在非eng/userdebug版本上使用:
user版本(正式发布版)的init通常删除了bootchart等调试功能。性能分析必须在eng或userdebug版本上进行。
5.3 与其他工具联动分析
bootchart给出了宏观瓶颈,要进一步定位代码级问题,需要结合其他工具:
- systrace:在bootchart定位到的高负载时间段,针对特定进程(如
system_server)进行systrace抓取,分析其主线程及binder线程的详细执行情况。 - ftrace:对于内核层面的延迟,可以启用
ftrace来跟踪调度器行为、中断关闭(IRQ off)等情况,特别是分析init进程在内核中的执行路径。 - 自定义log与Trace:在怀疑的耗时模块代码中,加入
ALOGD或ATRACE_BEGIN/END宏,然后通过logcat和systrace查看,进行精确的代码块耗时测量。
bootchart是安卓启动性能优化的“地图”。它不会直接告诉你哪行代码有问题,但它能清晰地指出“战场”在哪里、哪个“部队”(进程)投入战斗最久、哪种“资源”(CPU/I/O)最紧张。有了这张地图,你后续使用systrace、perf等“显微镜”工具进行深入排查时,就能做到有的放矢,极大提升优化效率。在实际项目中,我习惯将bootchart作为启动性能分析的必选第一步,它的全景视图能帮助团队快速对齐对问题的认知,避免在错误的方向上浪费时间。