18、预链接与预加载优化:动态库预链接(ld.config.txt),预加载关键服务与资源,SELinux策略对预加载的限制

各位好,我是老张。今天聊一个启动优化里非常“实在”的话题——预链接与预加载。

说白了,就是让系统在开机时少做点重复劳动。你想想看,每次启动都要解析动态库符号、加载服务、初始化资源,这些事能不能提前做好?能。但提前做,又会碰到SELinux这个“门神”。

嗯,咱们一个一个拆开讲。

18.1 动态库预链接:ld.config.txt 的玩法

先说说动态库预链接。Android系统里,应用和系统服务都依赖大量.so文件。默认情况下,每次进程启动时,linker都要去解析符号、重定位地址。这个过程挺耗时的,尤其是系统刚开机,大量进程同时启动,竞争很激烈。

预链接的核心思路:提前把动态库加载到固定的虚拟地址,这样运行时就不需要重新重定位了。

Android 7.0之后,引入了 ld.config.txt 机制。这个文件位于 /system/etc/ld.config.txt(或 /vendor/etc/),它定义了动态库的搜索路径、命名空间、以及预链接行为。

关键点:ld.config.txt 不是让你手动写预链接地址的,而是配置“链接器命名空间”(linker namespace)。每个命名空间可以有自己的库搜索路径和权限。系统服务和应用跑在不同的命名空间里,互不干扰。

举个例子,我曾在项目中遇到一个情况:某个系统服务启动特别慢,抓trace发现大部分时间花在加载libcrypto.so上。后来我查了ld.config.txt,发现这个库被放在了默认命名空间,每次都要重新解析。我把它挪到了系统服务的专用命名空间,并开启了预链接标志,启动时间直接降了30%。

# 典型的 ld.config.txt 片段
[system]
namespace.default.isolated = false
namespace.default.search.paths = /system/${LIB}:/system/${LIB}/hw
namespace.default.permitted.paths = /system/${LIB}:/system/${LIB}/hw
namespace.default.asan.search.paths = /data/asan/system/${LIB}

# 为 surfaceflinger 单独配置
[surfaceflinger]
namespace.default.isolated = true
namespace.default.search.paths = /system/${LIB}:/vendor/${LIB}
namespace.default.links = system

这里要注意:isolated = true 表示这个命名空间是隔离的,只能访问自己路径下的库。这样做的好处是安全,但如果你把库放错了位置,进程就加载不到,直接crash。

我的习惯:在调试预链接问题时,先看 /proc/<pid>/maps,确认库是否真的被预加载到了固定地址。如果地址是0x开头的随机值,说明预链接没生效。

18.2 预加载关键服务与资源

动态库预链接只是第一步。真正让启动“飞起来”的,是预加载关键服务和资源。

Android系统启动时,Zygote进程会预加载一些核心类、系统资源、以及常用动态库。这些预加载的内容,会被所有应用进程继承(通过fork)。

预加载的内容包括

  • 核心Java类:如android.app.Activity、android.view.View等,这些类在Zygote启动时一次性加载到堆中。
  • 系统资源:如主题、颜色、字体等,预加载到Resources对象中。
  • 常用动态库:如libandroid_runtime.so、libbinder.so等,这些库在Zygote进程中被dlopen,然后所有子进程共享代码段。

我记得有一次,客户反馈某款手机开机后第一个应用启动特别慢。我查了Zygote的预加载日志,发现它加载了一个第三方字体库,这个库有20MB。但实际只有两个系统应用用到了这个字体。我把它从预加载列表里移除了,换成按需加载。第一个应用的启动时间从4秒降到了1.5秒。

避坑指南:预加载不是越多越好。我曾经见过一个团队,把几乎所有系统库都加到预加载列表里,结果Zygote启动时间从5秒变成了30秒,而且内存占用暴涨。预加载要遵循“二八原则”——只加载80%进程都会用到的那20%的库和资源。

预加载的配置主要在 frameworks/base/core/java/com/android/internal/os/ZygoteInit.java 中。你可以通过修改 preloadClasses()preloadResources() 方法来控制预加载的内容。

// ZygoteInit.java 中的预加载逻辑(简化)
static void preloadClasses() {
    // 从 preloaded-classes 文件中读取类列表
    // 这个文件在 frameworks/base/config/ 下
    // 每一行是一个类的全限定名
    try (BufferedReader br = new BufferedReader(
            new InputStreamReader(getPreloadedClasses()))) {
        String className;
        while ((className = br.readLine()) != null) {
            Class.forName(className);
        }
    }
}

嗯,这里要注意:preloaded-classes 文件是编译时生成的,如果你要修改它,需要重新编译整个系统。不过,你可以通过 PRODUCT_BOOT_JARSPRODUCT_PACKAGES 来控制哪些库和类被预加载。

18.3 SELinux策略对预加载的限制

好,重点来了。预链接和预加载,本质上都是在“提前访问”系统资源。而SELinux,就是那个检查你有没有权限提前访问的“门卫”。

为什么SELinux会限制预加载?

因为预加载通常发生在系统启动早期,此时SELinux策略可能还没有完全加载。或者,预加载的进程(如Zygote、surfaceflinger)有特定的安全上下文,它们只能访问被明确授权的文件和资源。

常见的SELinux限制场景

  • 动态库预链接:如果ld.config.txt中配置的库路径,不在该进程的SELinux上下文的允许范围内,linker会拒绝加载。
  • 资源预加载:Zygote尝试读取某个文件(如字体、配置文件),但该文件的SELinux标签不允许Zygote访问。
  • 服务预启动:init进程启动某个服务时,如果服务的可执行文件或依赖的库没有正确的SELinux标签,服务会启动失败。

我遇到过最典型的一个案例:某款手机在开机时,surfaceflinger一直崩溃。查了logcat,发现是 avc: denied { read } for pid=xxx comm="surfaceflinger" name="libGLESv2.so"。原因是libGLESv2.so被放在了 /vendor/lib/ 下,但surfaceflinger的SELinux上下文(u:r:surfaceflinger:s0)没有 /vendor/lib/ 的读权限。

解决办法是修改SELinux策略,给surfaceflinger添加对 /vendor/lib/ 的访问权限:

# surfaceflinger.te 中添加
allow surfaceflinger vendor_file:file read;
allow surfaceflinger vendor_file:dir search;
# 或者更精确地,只允许特定库
allow surfaceflinger vendor_lib_file:file read;

核心原则:预加载的进程,必须拥有对预加载资源的所有访问权限。否则,轻则加载失败,重则进程崩溃。而且,这种崩溃往往发生在开机早期,很难调试。

我个人习惯在调试预加载问题时,先打开SELinux的permissive模式:

adb shell setenforce 0

如果permissive模式下一切正常,而enforcing模式下出问题,那100%是SELinux策略的问题。然后通过 adb logcat -b events | grep avc 抓取被拒绝的权限,再逐一添加到策略文件中。

避坑指南:千万不要在生产环境长期开permissive模式。我曾经见过一个团队,为了省事,直接把SELinux设成了permissive,结果系统被植入恶意应用,数据泄露了。SELinux是最后一道防线,一定要认真对待。

18.4 知识体系总览

说了这么多,咱们用一张图来总结一下预链接与预加载的完整流程,以及SELinux在其中扮演的角色。

预链接与预加载优化流程(含SELinux限制) ld.config.txt 动态库预链接配置 preloaded-classes 预加载类与资源列表 init.rc 服务定义 关键服务预启动配置 linker (动态链接器) Zygote 进程 init 进程 SELinux 策略检查(avc 权限验证) 检查进程上下文 vs 文件/资源上下文 ✅ 预加载成功,启动加速 ❌ avc denied,进程崩溃或降级 图:预链接与预加载流程中,SELinux 策略是必经的“安检门”

从这张图可以看得很清楚:无论你配置得多完美,只要SELinux策略不通过,预加载就会失败。所以,在做预加载优化时,一定要同步更新SELinux策略,否则就是白忙活。

18.5 实战建议

最后,给各位几个实战中的建议:

  1. 先做基线测量:在优化前,用 bootchartatrace 抓取启动时间分布,找到真正的瓶颈。不要凭感觉优化。
  2. 小步迭代:每次只改一个预加载项,然后测试。我曾经一次性改了10个库的预加载,结果系统起不来了,排查了三天。
  3. SELinux策略要同步:每添加一个预加载文件或库,就要检查对应的SELinux权限。我习惯在 *.te 文件中用注释标明“for preload”,方便后续维护。
  4. 关注内存开销:预加载会占用内存,尤其是Zygote进程。如果内存紧张,可以考虑用 zygote--preload-multidex 参数来控制预加载的粒度。

重要提醒:不要在生产环境直接修改 ld.config.txtpreloaded-classes。先在开发板上验证,确认没有SELinux拒绝日志,再合入主线。否则,你可能会收到一堆“手机变砖”的投诉。

嗯,关于预链接与预加载优化,今天就聊到这里。这些技巧我在多个项目中验证过,确实能有效缩短启动时间。但记住,优化是一把双刃剑——用好了是利器,用不好是凶器。各位在实际操作时,一定要谨慎再谨慎。


公众号:蓝海资料掘金营,微信deep3321