如果你刚接触安卓系统开发第一件让你头疼的事往往是“整个系统是怎么跑起来的”。我当年第一次拿到一块开发板烧录完镜像看到屏幕亮起开机动画再到Launcher出现前后可能有三四十秒但完全不知道这中间发生了什么。后来为了优化开机时间我把安卓系统启动流程从头到尾拆了一遍才真正明白每个阶段各自在做什么、哪些环节最耗时、哪些问题会直接导致起不来。这篇文章就按我实际排查问题的顺序把安卓系统启动流程完整讲一遍。适合系统开发、驱动移植、应用开发者读也适合想搞懂“手机开机时系统在忙什么”的爱好者。对着这个流程去读源码和日志会轻松很多。1. 按下电源键之后BootROM、Bootloader与内核的接力1.1 BootROM所有固化逻辑的起点很多人以为安卓系统启动是从内核开始的其实是错的。处理器上电后的第一段代码并不是安卓源码里的任何一部分而是芯片出厂时固化的BootROM。它在芯片内部ROM里上电后由硬件逻辑直接跳转执行负责初始化最基本的时钟、存储控制器然后把Bootloader从固定存储位置读到SRAM或DDR里。从系统工程师的角度看BootROM的存在意义有两个一是保证硬件上电后一定能找到引导代码不依赖外部存储内容是否完整二是通过熔丝位、签名校验等机制建立一条信任链。很多设备变砖后还能进入应急下载模式靠的就是这段固化逻辑还在工作。也就是说在这第一阶段内核根本还没参与。排查启动问题如果看到CPU不上电、无任何串口输出基本要先怀疑BootROM阶段或者供电时序。这部分不是你改安卓源码能解决的通常是芯片厂商的固件逻辑。1.2 Bootloader和分区表系统镜像的合法入口Bootloader是一段独立于安卓系统之外的程序常见实现有U-Boot、ABL等。它的工作可以分为三步初始化DDR、UART、存储、显示等基础硬件根据分区表读取boot分区或者vendor_boot分区里的内核映像在内存中准备好设备树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.selinuxpermissive、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_completed1这种属性触发动作。很多设备厂商的定制服务也都挂在这条链路上。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通过forkexec将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再去看正常流程日志。因为启动问题九成是异常打乱了正常链路先把异常找出来正常流程的分析才有意义。