☰
安卓系统启动流程全解析:从BootROM到Launcher
2026/10/19 13:33:46 网站建设 项目流程

如果你刚接触安卓系统开发,第一件让你头疼的事往往是“整个系统是怎么跑起来的”。我当年第一次拿到一块开发板,烧录完镜像,看到屏幕亮起开机动画,再到Launcher出现,前后可能有三四十秒,但完全不知道这中间发生了什么。后来为了优化开机时间,我把安卓系统启动流程从头到尾拆了一遍,才真正明白每个阶段各自在做什么、哪些环节最耗时、哪些问题会直接导致起不来。

这篇文章就按我实际排查问题的顺序,把安卓系统启动流程完整讲一遍。适合系统开发、驱动移植、应用开发者读,也适合想搞懂“手机开机时系统在忙什么”的爱好者。对着这个流程去读源码和日志,会轻松很多。

1. 按下电源键之后:BootROM、Bootloader与内核的接力

1.1 BootROM:所有固化逻辑的起点

很多人以为安卓系统启动是从内核开始的,其实是错的。处理器上电后的第一段代码并不是安卓源码里的任何一部分,而是芯片出厂时固化的BootROM。它在芯片内部ROM里,上电后由硬件逻辑直接跳转执行,负责初始化最基本的时钟、存储控制器,然后把Bootloader从固定存储位置读到SRAM或DDR里。

从系统工程师的角度看,BootROM的存在意义有两个:一是保证硬件上电后一定能找到引导代码,不依赖外部存储内容是否完整;二是通过熔丝位、签名校验等机制,建立一条信任链。很多设备变砖后还能进入应急下载模式,靠的就是这段固化逻辑还在工作。

也就是说,在这第一阶段,内核根本还没参与。排查启动问题如果看到CPU不上电、无任何串口输出,基本要先怀疑BootROM阶段或者供电时序。这部分不是你改安卓源码能解决的,通常是芯片厂商的固件逻辑。

1.2 Bootloader和分区表:系统镜像的合法入口

Bootloader是一段独立于安卓系统之外的程序,常见实现有U-Boot、ABL等。它的工作可以分为三步:

  1. 初始化DDR、UART、存储、显示等基础硬件;
  2. 根据分区表读取boot分区或者vendor_boot分区里的内核映像;
  3. 在内存中准备好设备树(DTB),然后跳转到内核入口。

这里有几个非常容易踩坑的细节。第一,boot分区里存放的不只是内核二进制,还包含内核命令行参数、ramdisk地址等信息,格式通常是boot_img_hdr。如果这个头部的校验和不对,Bootloader会直接拒绝加载。第二,设备树和内核版本需要匹配,否则可能出现触摸屏失灵、电量读不到这类奇怪现象,因为设备树里的节点信息是外设驱动匹配的依据。

从启动流程角度来看,Bootloader的耗时往往被忽视。它要驱动显示、读取存储、可能还要做校验和解压,遇到老式eMMC或慢速存储,这段耗时能达到几百毫秒甚至更高。我的经验是,如果优化启动速度只看内核之后的耗时,而不看Bootloader的串口日志,会漏掉一大块时间。

1.3 内核启动到init交接,中间发生了什么

内核被Bootloader跳转执行后,首先进行的是解压和重定位。如果你的内核是GZIP或LZ4压缩的,Bootloader会按头信息里的格式解压。之后内核开始真正的初始化流程。

  • setup_arch:初始化架构相关逻辑,如页表、内存布局;
  • start_kernel:完成调度器、内存管理、定时器、中断等核心子系统初始化;
  • rest_init:创建两个内核线程,其中kernel_init线程会执行init进程的加载;
  • kernel_init最终根据命令行参数找到根文件系统里的init程序,exec成为第一个用户态进程。

从这个阶段开始,启动控制权便交给了init进程。用户在串口上看到“Freeing unused kernel memory”后,紧接着出现init相关的日志,就说明交接成功了。

在这段流程里,最值得关注的其实是内核启动参数。比如androidboot.console=、androidboot.selinux=permissive、msm_drm.dsi_display等,都写在Bootloader拼接的命令行里。排查开机问题时,通过串口看到的内核日志开头会打印完整命令行,一定要习惯先看一眼,因为某些参数会直接影响后续init的加载路径。

2. init进程:用户态一切服务的父亲进程

2.1 init.rc:一字排开的启动脚本与触发器

内核把控制权交给init时,init首先要做的是挂载tmpfs、proc、sysfs这些基础文件系统,然后解析init.rc文件。这个文件是安卓启动流程的“总指挥”,它用一套类INI语法来声明动作(action)、服务(service)以及触发条件(trigger)。

举几个最常见的片段:

on early-init start ueventd on init mkdir /dev/mnt 0755 root root service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote class main socket zygote stream 660 root system onrestart restart zygote

这里面的on early-init、on init、on boot都是系统内置的触发阶段。init进程按顺序执行脚本里的命令,启动对应服务,并通过class main这种分组方式让服务可以在启动、停止、重启时被批量管理。

对于第一次读启动流程的人来说,不需要背语法,但一定要建立“init进程是按阶段驱动服务”的概念。最常见的启动定制,比如增加一个开机自启脚本,就是在init.rc里增加一个service,或者用on property:sys.boot_completed=1这种属性触发动作。很多设备厂商的定制服务,也都挂在这条链路上。

2.2 属性系统与SELinux:启动期的双重守护

init进程除了解析脚本,还建立了安卓属性服务。属性(property)类似全局键值对,所有进程都能通过__system_property_get读取,但设置权限受控。启动流程里的很多关键标志都是通过属性传递的,比如sys.boot_completed、service.bootanim.exit、sys.usb.config等。

属性系统在启动期的典型作用,是让init与zygote、system_server之间传递状态。例如开机动画进程会监听service.bootanim.exit属性,一旦系统服务把它置为1,动画才会退出。如果这个属性没有被正确写入,就会看到开机动画一直转、桌面却已经能用的“假卡死”。

SELinux则是另一道重要防线。安卓从内核态到用户态全程开启SELinux,采用默认拒绝策略。启动过程中每个服务的启动、每个文件的访问,都需要有对应的SELinux策略允许,否则init会直接拒绝。

我在实际调试中遇到过很多次这样的问题:某个新加的服务在init.rc里写好了,却一直启动失败,串口日志里只有一句avc: denied { execute }。这时不是语法问题,而是没有给这个服务的可执行文件打上正确的安全上下文。排查命令一般是:

adb shell dmesg | grep avc

这能让你快速找到是哪条策略被拦截,然后去对应的te文件里补充allow规则。启动流程的安全策略从来不是可有可无的限制,不理解它,很多诡异问题都无从下手。

2.3 Zygote如何被孵化

init进程完成基础服务启动后,会触发class main,其中最关键的一个服务就是zygote。安卓系统在早期版本存在app_process、app_process32、app_process64的区别,现在主流设备已经64位化,但启动方式仍然一致:init通过fork+exec,将app_process进程变成zygote。

zygote启动后做的第一件事是创建/dev/socket/zygote这个本地套接字,然后调用RuntimeInit进入Java世界,接着预加载系统资源、通用类、共享库。预加载的过程是整个启动流程里耗时最长的阶段之一,因为要加载大量framework的class和资源,通常要用掉一两秒甚至更久。

从架构上看,zygote之所以叫“受精卵”,是因为它之后的所有应用进程都由它fork出来。这种模式能大幅缩短应用的启动时间,因为子进程直接共享了父进程预加载的类与资源,同时通过写时复制机制节省物理内存。如果你发现每次冷启动一个新应用很慢,或者系统整体内存吃紧,zygote的预加载质量和数量就是一个绕不开的优化点。

3. SystemServer与Zygote的分工:应用框架层的幕后编排

3.1 Zygote究竟扮演什么角色

Zygote进程在完成启动后,除了等待来自socket的创建应用请求,还会启动SystemServer,也就是系统服务的宿主进程。这一步很有意思,它是在zygote的main方法里通过ZygoteInit.forkSystemServer完成的一次特殊fork。

forkSystemServer出来的进程,不会立即去启动一个普通应用,而是调用SystemServer.main(),然后进入系统服务初始化流程。这也是为什么Android架构里,应用进程和系统服务进程是同一个“父亲”出来的兄弟进程。

从启动流程上看,这里最值得关注的是“哪个进程先有,哪个进程后有什么依赖”。Zygote需要等init先启动完成、属性系统可用、SELinux策略加载完毕后才能运行,否则它fork出的SystemServer没有权限干活。而SystemServer又需要Zygote在里面已经预加载好基础类,否则服务启动会很慢。

实际调试中,我会通过ps | grep zygote和ps | grep system_server看两个进程是否存在,来判断流程卡在哪一段。如果zygote已经起来,但system_server迟迟没有出现,那么问题基本锁在SystemServer入口前的初始化逻辑里,比如资源加载、类加载抛异常等。

3.2 SystemServer的服务注册顺序暗藏玄机

SystemServer进入Java世界后,会分三个阶段启动服务:引导服务、核心服务、其他服务。引导服务比如ActivityManagerService、PackageManagerService、PowerManagerService,这些是最早启动的;核心服务如BatteryService、VibratorService、WindowManagerService;其他服务则在之后陆续加入。

为什么服务还要分先后?因为服务之间存在依赖。比如PackageManagerService需要用到installd提供的安装能力,而这个能力依赖底层存储和用户管理;ActivityManagerService需要先获得进程管理框架,才能在后来的窗口服务启动时正确管理焦点。

启动顺序一旦被破坏,就会出现系统服务互相等待,甚至死锁。不少厂商在定制系统服务时,容易犯的错误是把自己的服务往startOtherServices里一贴,却忽略了对现有服务的依赖。其结果往往是开机慢、偶发ANR,或者特定功能异常。正确做法是先在系统已有的服务生命周期里做hook,尽量不要再开一个独立服务去抢启动窗口。

SystemServer启动完成后,会调用ActivityManagerService.systemReady(),然后通过系统回调通知各个服务“环境就绪”,接着才启动桌面Launcher。这段逻辑在源码里看起来只是几个方法调用,实际却是一个复杂的状态同步过程。

3.3 SystemServer崩溃后的重启策略

SystemServer是整个安卓系统最核心的Java进程,它一旦崩溃,普通应用可能还活着,但系统UI、输入、窗口都会失去响应。Android对SystemServer的崩溃处理是:由zygote的父进程init捕获到重启信号,然后重新启动zygote,进而再拉起SystemServer,整体回到一个新的用户态启动阶段。

从使用者感知来看,这种重启往往是屏幕一黑,接着又出现开机动画,看起来像是重启了,但内核和Bootloader没有重新跑。如果你在串口或者logcat里看到类似*** RESTART: /system/bin/app_process64的日志,那就说明SystemServer崩溃触发了zygote重启。

这个设计有它的道理:SystemServer崩溃后,进程状态可能已经被污染,继续只重启SystemServer而没有清理zygote预加载的环境,容易出现二次崩溃。所以Android选择整体重启用户态。排查这类问题,要看完整的tombstone和dropbox日志,定位到具体服务崩溃前最后调用了什么。不要一上来就去改init.rc或者拔电池,那是治标不治本。

4. 开机动画、Launcher与BOOT_COMPLETED:用户感知的启动完成点

4.1 开机动画:谁启动的,谁关闭的

用户能看到的开机动画,并不在启动链路的最初阶段。内核起来后,屏幕可能还黑着,一直到init开始启动bootanim或者surfaceflinger相关服务时,开机动画进程才会被拉起。

开机动画进程会监听属性变化,它的退出条件通常是属性service.bootanim.exit被设为1。谁设置这个属性?SystemServer完成关键服务启动后,会调用WindowManagerService的相关接口,最终通过SystemServer往这个属性写入1。也就是说,开机动画退出意味着整个系统框架已经基本就绪,桌面马上要出来了。

如果开机动画一直不退出,或者黑屏后过很久才出现图标,可以从动画服务的日志和属性值两个方向排查。一种常见情况是surfaceflinger还没完成首帧合成,动画进程拿不到帧缓冲就一直等待;另一种是开机动画进程自身崩了,init按策略不停重启它,和system_server抢资源。

4.2 系统就绪广播为什么姗姗来迟

安卓系统定义的“开机完成”,在框架里表现为ACTION_BOOT_COMPLETED广播。这个广播并不是系统一启动就发,而是要等SystemServer把核心服务全部就绪、数据库准备好、包管理扫描完已安装应用后,才由ActivityManagerService发送。

包管理扫描是一个很耗时的过程。每次开机,PackageManagerService要扫描/system/app、/vendor/app、/data/app等目录,解析每个APK的包信息、权限、组件,写入到缓存。如果你的设备装了大量应用,或者APK没做dex优化,这段扫描时间会非常可观。

很多开发者以为接收到了BOOT_COMPLETED就可以马上调用系统服务,其实不一定。广播发出时,桌面可能都还没首绘完成,部分服务也只是“刚就绪”,并不是完全稳定。所以经验上,应用里做开机自启逻辑,要尽量延迟初始化,等一等系统服务真的空闲了再做重活。

还有一种情况是广播根本不发。原因可能是某个服务启动超时被看门狗杀掉,或者系统服务初始化过程中异常退出了。此时看logcat里ActivityManager相关日志,找到BroadcastRecord条目,就能看到广播是否在排队,以及是哪个进程迟迟没有返回。

4.3 影响启动速度的隐藏因素

从BootROM到Launcher,整个启动流程每一个阶段都能成为性能瓶颈。最常见的隐藏因素有这么几个:

  • 内核日志级别和串口打印:开启完整的uart console日志会显著拖慢启动,因为打印本身是串行且阻塞的。量产机上通常会把console调成quiet。
  • 存储介质与文件系统:UFS和eMMC的读写性能差距很大,文件系统的挂载参数、f2fs还是ext4也会影响启动时间。
  • dex2oat与odex优化:如果APK没有提前编译,开机时在首次启动阶段要做JIT/AOT编译,耗时严重。
  • 开机阶段的IO并发:init脚本里大量服务同时去读存储,会造成IO争抢,延长每个环节的时间。

所以优化启动时,不要只盯某一个服务,而要在开启完整日志的情况下对每个阶段计时,找出真正的长尾。

5. 启动变慢或卡死的排查兵器谱

5.1 一条命令拿到跨阶段启动日志

排查启动流程,第一件事是拿到完整的启动日志。开发板或解锁设备可以先开串口,使用minicom或screen抓串口日志,记录从Bootloader到用户态的完整输出。 adb无法在早期使用,但可以用adb shell dmesg -w抓后续内核日志和用户的logcat。

如果是纯用户态启动问题的复现,我会同时开三个日志窗口:

adb logcat -b all -v threadtime adb shell dmesg -w adb shell cat /sys/kernel/debug/tracing/trace_pipe

其中trace_pipe需要root权限,适合分析内核态耗时。配合systrace在启动期间抓trace,可以得到一条按时间轴排列的进程活动曲线,能很直观地看到哪个阶段出现了长时间阻塞。

我在实际定位一个开机动画退出慢的问题时,就是用systrace发现zygote所在的CPU核心满载,而SystemServer刚启动时依赖的binder call却被排队,导致属性写入晚了1.2秒。这种问题如果不看trace,只看log很难解释。

5.2 常见卡死根因:从init崩溃到SELinux拒绝

启动卡死大多数不是神秘问题,而是某个前置条件没满足。最常见的卡死点:

  • init.rc语法错误:例如引用的宏不存在,或者service的可执行文件路径不对,init会报找不到文件,后续服务全部停摆。
  • SELinux拒绝:新增服务、修改文件上下文后忘记更新策略,被内核拒绝exec。
  • SystemServer初始化异常:某个服务依赖的apk没安装或损坏,导致启动抛异常。
  • 资源预加载失败:zygote加载framework-res.apk失败,无法进入system_server。
  • 显示服务未就绪:surfaceflinger迟迟不产生第一帧,Launcher虽然已经启动但屏幕仍黑。

遇到卡死,先确认是内核态还是用户态。如果串口停在starting kernel ...,多半是内核启动参数或设备树问题;如果串口已经打印init日志但一直没有system_server出现,问题就在用户态。

对于用户态的卡死,logcat里最需要注意的是Watchdog相关的日志,它会在系统服务锁超时后打印所有线程的栈信息,直指问题线程。很多“卡在启动界面”的案子,都能在Watchdog dump里看到某个服务线程阻塞在Binder调用,而对方进程又没有响应。

5.3 优化启动耗时,我看到过的三个有效做法

我做过几次启动时间优化,把从按电源键到桌面首帧的时间压缩了大概百分之三十。真正起作用的,并不是哪个花哨的配置,而是以下三个基础方法。

第一,减少串口日志。开发阶段需要详细打印,但量产配置里,把console_loglevel调低,或者直接关闭uart console,能省下大量时间。注意关闭后要通过其他途径保留崩溃日志,否则出问题会很难追。

第二,把开机阶段的IO操作分摊或者延后。比如应用预扫描、数据库预热这类工作,不要集中在启动早期做,可以用idle状态下再触发。资源预加载也要精简,zygote预加载每个类、每个资源都有成本,对启动慢的设备,剪掉无用共享库收益非常明显。

第三,合理安排并行启动。init脚本里同一阶段的服务其实可以并行,但有些依赖是隐性的。做并行前,一定要把所有服务的socket、属性、文件信号依赖梳理清楚。我会在init.rc里给不同服务设置不同的重启条件和阶段,而不是全部堆在class main里。

这套思路下来,启动时间优化不再是玄学,而是一个可以度量的工程问题。每次改完,对比同一条启动日志里的时间戳,就清楚改了什么、值不值了。

最后分享一个我自己常用的检查习惯:每次抓完启动日志,我都会先按关键字扫一遍avc: denied、failed to start、Fatal signal,再去看正常流程日志。因为启动问题九成是异常打乱了正常链路,先把异常找出来,正常流程的分析才有意义。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询