预加载优化
本文面向已经读过 Zygote预加载、preloadClasses、Zygote fork准备、fork与COW 和 启动时间分析 的读者。前面的文章回答“预加载如何执行”;本文回答“类表应该怎样产生、改动会影响谁、怎样判断改动是否真的更好”。
预加载优化不是简单地缩短 /system/etc/preloaded-classes。少加载一个类可以缩短 Zygote 初始化,却可能让数十个应用重复承担类解析、静态初始化和缺页;多加载一个类可以共享对象,也可能扩大 Zygote heap、执行错误的全局静态初始化,并增加每个子进程的私有脏页。本文以当前 release 的 profile 生成链、ART 编译链、运行时消费和测试为主线,不沿用旧 tools/preload 的历史阈值结论。
1. 优化对象
源码文件:frameworks/base/config/preloaded-classes
当前类表的文件头直接声明了它的双重作用:类会进入 boot image,并由 Zygote 强制初始化,以虚拟地址空间换取应用间共享。该 tag 中有 18784 条非注释记录,其中还包括数组类描述符;它不是常见 API 的手写清单。
Preloaded-classes filter file for phones.
Classes in this file will be allocated into the boot image, and forcibly
initialized in the zygote during initialization. This is a trade-off,
using virtual address space to share common heap between apps.一次类表改动至少要同时观察四个结果:
| 指标 | 主要消费者 | 改善方向 | 典型反作用 |
|---|---|---|---|
PreloadClasses 耗时 | Zygote 启动 | 更短 | 应用首次使用成本上升 |
| boot image/Zygote 内存 | ART、Zygote | 更小 | 共享对象减少 |
| 应用冷启动 | Zygote child | 更短 | Zygote 初始化可能变长 |
| private dirty pages | 每个 child | 更少 | 为布局优化增加构建复杂度 |
因此优化目标应写成“在指定启动终点和应用集合下,减少总时间或每进程私有内存”,而不是“类表行数减少”。owner 也不只有 Zygote:构建系统、dex2oat、profman 和运行时分别拥有不同状态。
2. 双重消费者
同一份 frameworks/base/config/preloaded-classes 沿两条路径生效。第一条在构建 boot image 时进入 dex2oat;第二条被复制到 system 分区,供 Zygote 逐行读取。
2.1 产品安装
源码文件:
build/make/target/product/base_system.mkframeworks/base/config/Android.bp
PRODUCT_COPY_FILES += $(call add-to-product-copy-files-if-exists,\
frameworks/base/config/preloaded-classes:system/etc/preloaded-classes)Make 产品配置给出最终分区路径;同目录的 Soong 模块则声明该文件作为可引用的 prebuilt_etc 构建输入:
prebuilt_etc {
name: "preloaded-classes",
src: "preloaded-classes",
filename: "preloaded-classes",
no_full_install: true,
}产品复制链决定 Zygote 最终看到的文件。add-to-product-copy-files-if-exists 允许源文件不存在时跳过复制;运行时 preloadClasses() 对目标文件缺失也只是记录错误并返回。因此“系统能启动”不能证明类表已经安装,A/B 实验必须先检查设备文件及构建来源。
2.2 Boot image
源码文件:build/soong/java/dexpreopt_bootjars.go
if !ctx.Config().GetBuildFlagBool(
"RELEASE_ART_NOPRELOAD_CLASSES_IN_PROFILE") &&
len(image.preloadedClassesFile) > 0 {
preloadedClassesPath := android.ExistentPathForSource(
ctx, image.preloadedClassesFile)
if preloadedClassesPath.Valid() {
cmd.FlagWithInput("--preloaded-classes=",
preloadedClassesPath.Path())
}
}framework boot image 配置把 frameworks/base/config/preloaded-classes 交给 dex2oat。构建 flag 开启时,denylist 会被写入 binary profile 的 classes-no-preload section,dex2oat 改为优先消费 profile,不再依赖文本文件判断 no-preload class。
源码文件:art/dex2oat/dex2oat.cc
bool PreparePreloadedClasses() {
if (preloaded_classes_files_.empty() && preloaded_classes_fds_.empty()) {
// We don't have any preloaded-classes files: handle all the classes as no-preload.
// TODO(b/383506474): deprecate this behavior and rely only on the profile.
} else if (profile_compilation_info_ != nullptr &&
profile_compilation_info_->HasNoPreloadSection()) {
compiler_options_->ignore_preloaded_classes_ = true;
} else if (!preloaded_classes_fds_.empty()) {
for (int fd : preloaded_classes_fds_) {
if (!ReadCommentedInputFromFd(
fd, nullptr, &compiler_options_->preloaded_classes_)) {
return false;
}
}
} else {
for (const std::string& file : preloaded_classes_files_) {
if (!ReadCommentedInputFromFile(
file.c_str(), nullptr,
&compiler_options_->preloaded_classes_)) {
return false;
}
}
}
return true;
}没有类表时,编译器把所有类当成 no-preload;有 classes-no-preload section 时,文本文件被忽略。这个选择影响静态方法是否需要生成 class-initialization check,而不是只影响 boot image 里“放几个类”。
// art/compiler/driver/compiler_options.cc
bool CompilerOptions::ShouldCompileWithClinitCheck(
ArtMethod* method) const {
if (method != nullptr &&
Runtime::Current()->IsAotCompiler() &&
method->IsStatic() &&
!method->IsConstructor() &&
!method->IsNative()) {
ScopedObjectAccess soa(Thread::Current());
ObjPtr<mirror::Class> cls =
method->GetDeclaringClass<kWithoutReadBarrier>();
return cls->IsInBootImageAndNotInPreloadedClasses();
}
return false;
}所以修改类表后只 push /system/etc/preloaded-classes 不能完整模拟正式构建:运行时初始化集合变了,boot image 的类状态和编译代码却仍来自旧输入。可靠实验必须重建并刷入匹配的 boot image。
3. Profile生成
当前主流程由设备真实 profile 驱动,而不是旧 WritePreloadedClassFile 的“加载时间超过 1250 微秒”规则。输入是 boot classpath/system server dex、多个设备 profile 和 denylist;输出包括 boot image profile、preloaded classes 与 system_server profile。
3.1 数据采集
源码文件:
art/tools/boot-image-profile-configure-device.shart/tools/boot-image-profile-extract-profile.sh
# 配置阶段:停止 framework,打开 boot classpath/system_server profiling,
# 清空旧 profile 后重新启动。输入是已刷入的测试构建。
adb root
adb shell stop
adb shell setprop dalvik.vm.profilebootclasspath true
adb shell setprop dalvik.vm.profilesystemserver true
adb shell 'find /data/misc/profiles -name "*.prof" -exec truncate -s 0 {} \;'
adb shell start
# 运行目标产品的关键用户流程后,快照 android profile 并拉到 host。
adb shell cmd package snapshot-profile android
adb pull /data/misc/profman/android.prof android.prof关键用户流程决定 profile 的覆盖面。只启动到桌面会偏向 SystemServer/SystemUI;加入相机、设置、输入法和常用应用后,跨包覆盖比例会改变。profile owner 是设备上 ART/package profile 状态,生成脚本只消费快照,不会替你补齐未执行的场景。
ZygoteInit.preloadClasses() 在 profile boot classpath 模式下会在预加载结束后重置 JIT counters,避免“因为类表已经强制加载”被误认为后续用户流程真实使用。
源码文件:frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
runtime.preloadDexCaches();
if (shouldProfileBootClasspath()) {
Trace.traceBegin(Trace.TRACE_TAG_DALVIK,
"ResetJitCounters");
VMRuntime.resetJitCounters();
Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
}3.2 阈值筛选
源码文件:art/profman/boot_image_profile.h
struct BootImageOptions {
uint32_t preloaded_class_threshold = 10;
uint32_t image_class_threshold = 10;
uint32_t image_class_clean_threshold = 5;
uint32_t method_threshold = 10;
SafeMap<std::string, uint32_t>
special_packages_thresholds;
std::set<std::string> preloaded_classes_denylist;
};预加载阈值默认是 10%,含义是类的 origin package 注解数量占最大聚合数的比例达到阈值。它不是方法调用次数,也不是单次耗时。生成脚本默认再给 android 和 com.android.systemui 设置 1% special-package 阈值,让关键包使用的类更容易入选。
源码文件:art/profman/boot_image_profile.cc
static bool IncludeItemInProfile(uint32_t max_aggregation_count,
uint32_t item_threshold,
const FlattenProfileData::ItemMetadata& metadata,
const BootImageOptions& options) {
float item_percent = metadata.GetAnnotations().size() /
static_cast<float>(max_aggregation_count);
for (const auto& annotIt : metadata.GetAnnotations()) {
const auto& thresholdIt = options.special_packages_thresholds.find(
annotIt.GetOriginPackageName());
if (thresholdIt != options.special_packages_thresholds.end()) {
if (item_percent >= (thresholdIt->second / 100.f)) {
return true;
}
}
}
return item_percent >= (item_threshold / 100.f);
}
static bool IncludeInPreloadedClasses(
const std::string& class_name,
uint32_t max_aggregation_count,
const FlattenProfileData::ItemMetadata& metadata,
const BootImageOptions& options) {
bool denylisted = options.preloaded_classes_denylist.find(
class_name) != options.preloaded_classes_denylist.end();
return !denylisted && IncludeItemInProfile(
max_aggregation_count,
options.preloaded_class_threshold,
metadata, options);
}阈值降低会扩大类表,优先照顾关键包;阈值升高会保留跨更多包复用的类。denylist 是硬条件,即使一个类覆盖率足够高也不能进入输出。
3.3 输出顺序
profman 把候选放入 SafeMap<std::string, FlattenProfileData::ItemMetadata>,而 SafeMap 等价于 std::map。输出按类名 key 迭代,不按 hotness、加载耗时或 package 权重排序。
// art/profman/boot_image_profile.cc
SafeMap<std::string, FlattenProfileData::ItemMetadata>
preloaded_classes;候选通过阈值和 denylist 后,以类名作为 map key 写入;最终输出直接遍历同一个 map:
for (const auto& it : preloaded_classes) {
preloaded_content += ClassToProfileFormat(
it.first, it.second,
options.append_package_use_list) + "\n";
}因此当前实现里的“优先级”体现在入选阈值,不体现在 Zygote 加载顺序。手工把高频类移动到文件顶部不会改变候选集合,还可能被依赖类的静态初始化提前加载所抵消。若要研究顺序,必须用每个类名对应的 trace 做独立实验,不能把它当成现行生成策略。
4. Denylist边界
源码文件:frameworks/base/config/preloaded-classes-denylist
android.content.AsyncTaskLoader$LoadTask
android.net.ConnectivityThread$Singleton
android.os.FileObserver
android.provider.MediaStore
android.widget.Magnifier
com.android.internal.jank.InteractionJankMonitor$InstanceHolder
java.util.concurrent.ThreadLocalRandom该 tag 的 denylist 共 12 条。配置说明将它们定义为“具有 app-specific behavior、不应在 Zygote 初始化的类”。常见风险包括启动线程、建立进程级 singleton、缓存调用进程环境或提前固定随机/媒体/网络状态。这里首先是正确性边界,其次才是内存优化。
host 测试把 denylist 作为资源逐行读取,为每个类启动独立 app_process,要求类存在但尚未初始化。
源码文件:frameworks/base/tools/preload-check/src/com/android/preload/check/PreloadCheck.java
@Test
public void testDenyList() throws Exception {
StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(
new InputStreamReader(getClass().getResourceAsStream(
"/preloaded-classes-denylist")))) {
String s;
while ((s = br.readLine()) != null) {
s = s.trim();
if (s.startsWith("#") || s.isEmpty()) {
continue;
}
try {
run("com.android.preload.check.NotInitialized", s);
} catch (Throwable t) {
if (sb.length() > 0) {
sb.append('\n');
}
sb.append(t.getMessage());
}
}
}
if (sb.length() > 0) {
throw new RuntimeException(sb.toString());
}
}device helper 使用 Class.forName(name, false, null) 获取 boot classpath class,再读取 ART 的 class status,确认状态未达到 initialized。测试输入是完整 denylist,断言是每个独立进程最终打印 OK;它证明这些类没有被 Zygote 提前初始化,但不证明类表中其他类一定值得预加载。
profman 还有另一层测试:构造一个在多个 profile 中都出现的 denylisted java.lang.Thread,设置 100% preload threshold,期望输出仍只有满足阈值且不在 denylist 的 java.lang.Object。这验证了 denylist 优先于覆盖率。
5. 运行时成本
源码文件:frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.startsWith("#") || line.equals("")) {
continue;
}
Trace.traceBegin(Trace.TRACE_TAG_DALVIK, line);
try {
Class.forName(line, true, null);
count++;
} catch (ClassNotFoundException e) {
if (line.contains("$$Lambda$")) {
if (LOGGING_DEBUG) {
missingLambdaCount++;
}
} else {
Log.w(TAG, "Class not found for preloading: " + line);
}
} catch (UnsatisfiedLinkError e) {
Log.w(TAG, "Problem preloading " + line + ": " + e);
} catch (Throwable t) {
Log.e(TAG, "Error preloading " + line + ".", t);
if (t instanceof Error) {
throw (Error) t;
} else if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new RuntimeException(t);
}
}
Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
}initialize=true 会执行 <clinit>,所以成本不仅是读 dex metadata。每个成功类都可能创建字符串、数组、缓存、Binder proxy 或 native state;这些对象由 Zygote heap 持有,fork 后才成为共享候选。
失败语义也不同:
- 文件不存在:记录错误并跳过整个类阶段,后续资源预加载仍继续;
- 普通缺失类或 native library 缺失:记录 warning,继续下一类;
- 静态初始化抛出
Error/RuntimeException:重新抛出,Zygote 初始化失败; - 读取中断:记录 IOException,但 finally 仍填充 dex cache 并恢复权限。
每个类名都有 DALVIK trace section,适合定位单类 <clinit>;但父类、字段类型和静态依赖可能在当前行之前已经被其他类隐式加载,单类 section 不能直接等同于“该类独占成本”。
lazy preload 也只是移动生效时机。--preload-default 命令由 ZygoteConnection 调用同一个 ZygoteInit.lazyPreload();它不会缩小类表或减少总初始化工作,还不能交给 USAP 处理。优化前必须明确是希望缩短 Zygote 启动,还是希望降低首次 fork 请求延迟。
6. COW收束
类加载完成不等于已经形成适合 fork 的共享堆。Zygote 随后填充 dex cache、执行 GC/finalization;第一次 fork 前 ART 还会做 full GC、trim 和 Zygote space compaction。
源码文件:art/runtime/gc/heap.cc
if (!HasZygoteSpace()) {
CollectGarbageInternal(collector::kGcTypeFull,
kGcCauseBackground, false, GC_NUM_ANY);
non_moving_space_->Trim();
}第一次进入时,ART 随后切换 post-fork linear alloc,并冻结当前 intern/class table:
runtime->SetupLinearAllocForPostZygoteFork(self);
runtime->GetInternTable()->AddNewTable();
runtime->GetClassLinker()->MoveClassTableToPreZygote();移动 GC 配置下,当前空间再按 bin 进行 compaction:
ZygoteCompactingCollector zygote_collector(
this, is_running_on_memory_tool_);
zygote_collector.BuildBins(non_moving_space_);
zygote_collector.Run(kGcCauseCollectorTransition,
false);full GC 删除预加载期间的临时垃圾,trim 归还尾部页面,class/intern table 被切分成 pre/post-Zygote 区域,compaction 再建立 Zygote space。若只比较 Class.forName 时间,就会漏掉类表扩大导致的 PostZygoteInitGC 和第一次 preFork 成本。
boot image 还使用 dirty-image-objects 把预计会写脏的对象集中到相同页面。
源码文件:art/dex2oat/linker/image_writer.cc
Bin bin = Bin::kRegular;
if (kBinObjects) {
ObjPtr<mirror::Class> klass =
object->GetClass<kVerifyNone, kWithoutReadBarrier>();
if (klass->IsStringClass<kVerifyNone>()) {
bin = Bin::kString;
} else if (dirty_objects_.find(object) != dirty_objects_.end()) {
bin = Bin::kKnownDirty;
} else if (klass->IsClassClass()) {
bin = Bin::kClassVerified;
ObjPtr<mirror::Class> as_klass =
object->AsClass<kVerifyNone>();
if (as_klass->IsVisiblyInitialized<kVerifyNone>()) {
bin = Bin::kClassInitialized;
}
}
}类表决定哪些类和静态对象会在 fork 前存在;dirty-image-objects 决定 boot image 中易脏对象如何聚合。二者共同影响 COW,但 owner 不同,不能用“删类”代替页面布局优化。
7. 对照实验
一轮可解释的优化应固定设备、build variant、profile 场景、启动终点和应用集合,再只改变候选类表及由它生成的 boot image。
7.1 生成候选
源码脚本:art/tools/boot-image-profile-generate.sh
# 运行位置:完成 envsetup/lunch 的 AOSP 根目录。
# 输入:boot.zip、denylist 和多份执行过关键用户流程的 android.prof。
# 输出:candidate/boot-image-profile.txt、preloaded-classes、art-profile。
art/tools/boot-image-profile-generate.sh \
candidate \
out/dist/boot.zip \
frameworks/base/config/preloaded-classes-denylist \
profiles/home.prof \
profiles/camera.prof \
profiles/settings.prof \
--boot-image-profman-arg --preloaded-class-threshold=10
# 输出候选规模及相对当前类表的新增/删除项,不把总行数当最终结论。
awk '!/^#/ && NF' candidate/preloaded-classes | wc -l
diff -u frameworks/base/config/preloaded-classes \
candidate/preloaded-classes > candidate/preloaded-classes.diff || true生成失败时先检查 profile 是否与 boot.zip 中 dex 匹配、denylist 路径是否存在,以及 ANDROID_BUILD_TOP 是否已设置。阈值不是越高越好;应先观察新增/删除类属于哪些关键路径,再构建候选镜像。
7.2 时间与内存
# 输入:刷入基线或候选完整构建后的冷启动事件。
# 输出:同一Zygote PID的preload起止时间;重复多轮再比较分布。
adb logcat -b events -v threadtime -d \
| rg 'boot_progress_preload_(start|end)'
# 输入:已经运行相同用户流程、具有相同目标进程集合的设备。
# 输出:目标进程相对其Zygote的boot image private dirty对象报告。
art/imgdiag/run_imgdiag.py \
com.android.systemui com.android.settings \
--zygote zygote64 \
--host-out-dir imgdiag-candidate
# 从报告中提取每个进程的private dirty页和对象,和基线同名进程比较。
rg -n 'total private dirty pages|Application dirty entries' \
imgdiag-baseline imgdiag-candidate时间改善但 private dirty 增加,说明工作可能从 Zygote 移到了每个 child;private dirty 降低但 preload/first fork 显著变慢,则需要评估进程数量能否摊销成本。进程集合不一致时,均值也不可直接比较。
8. 验证边界
源码测试:art/profman/profile_assistant_test.cc 的 TestBootImageProfile 构造四个 origin package 的 class/method profile,设置 clean、dirty、method、preloaded 和 special-package 阈值,并把 java.lang.Thread 放入 denylist。测试执行真正的 profman,最后断言 boot profile 和 preloaded classes 文件与期望完全相同。
源码文件:art/profman/profile_assistant_test.cc
std::vector<std::string> expected_preloaded_data = {
DescriptorToDot(kDirtyClass)
};测试把阈值、special package 和 denylist 作为真实 profman 参数:
args.push_back("--preloaded-class-threshold=" +
std::to_string(kPreloadedThreshold));
args.push_back(
"--special-package=" + kSpecialPackage + ":" +
std::to_string(kSpecialThreshold));
args.push_back("--profile-file=" + profile.GetFilename());
args.push_back("--out-profile-path=" + out_profile.GetFilename());
args.push_back("--out-preloaded-classes-path=" +
out_preloaded_classes.GetFilename());
args.push_back("--apk=" + core_dex);
args.push_back("--dex-location=" + core_dex);
args.push_back("--preloaded-classes-denylist=" +
preloaded_class_denylist.GetFilename());执行完成后读取输出文件并与上面的期望逐字节比较:
ASSERT_EQ(ExecAndReturnCode(args, &error), 0) << error;
ASSERT_TRUE(android::base::ReadFileToString(
out_preloaded_classes.GetFilename(),
&output_preloaded_contents));
ASSERT_EQ(output_preloaded_contents,
expected_preloaded_content);这个测试证明阈值和 denylist 的组合筛选生效,也证明输出内容是确定的;它没有衡量真实设备上的启动时间和 COW 收益。
PreloadCheck.testDenyList() 则从另一个方向验证运行时状态:输入是当前 denylist,动作是在 Zygote child 中以 initialize=false 查类,断言是所有类仍未初始化。它能发现“生成器排除了类,但其他预加载路径又把它隐式初始化”的问题;这正是只 diff 文本文件无法覆盖的条件路径。
检查自己是否理解这套机制,可以回答两个问题:把 preloaded-class-threshold 从 10 提到 30 后,哪些类会被删除、special package 为什么仍可能保留低覆盖类;以及只 push 新类表不重建 boot image 时,为什么 Zygote trace 看似改变,AOT clinit check 和 private dirty 结论却仍不可信。
