后台 dexopt
本文默认读者已经知道 dexopt reason、compiler filter 和 profile 的基本作用;如果这些概念还不熟悉,可以先读 dexopt 流程总览 和 ART profiles。本文解决的问题是:一次后台 dexopt 怎样从系统启动时的注册,经过 JobScheduler 约束,进入 ART Service 的批量优化,最终完成取消、重调度和清理。
后台 dexopt 不是“每天把所有 APK 编译一次”。Android 17 会先选择近期活跃包;存储紧张时可能先降级不活跃包;主批次完成后,还可能为 profile 尚未及时合并的包执行补充批次。任务停止也不是杀掉线程,而是通过同一个 CancellationSignal 向各层传播取消状态。
1. 启动注册
后台任务的第一个入口仍在 PMS,但 PMS 只决定“何时注册”,不拥有 job 的运行状态。
源码文件:frameworks/base/services/core/java/com/android/server/pm/DexOptHelper.java,符号:initializeArtManagerLocal
public static void initializeArtManagerLocal(
@NonNull Context systemContext, @NonNull PackageManagerService pm) {
ArtManagerLocal artManager = new ArtManagerLocal(systemContext);
artManager.addDexoptDoneCallback(false /* onlyIncludeUpdates */, Runnable::run,
pm.getDexOptHelper().new DexoptDoneHandler());
LocalManagerRegistry.addManager(ArtManagerLocal.class, artManager);
sArtManagerLocalIsInitialized = true;
systemContext.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
context.unregisterReceiver(this);
artManager.scheduleBackgroundDexoptJob();
}
}, new IntentFilter(Intent.ACTION_LOCKED_BOOT_COMPLETED));
}这里注册的是一次性广播接收器。收到 ACTION_LOCKED_BOOT_COMPLETED 后,接收器先注销自己,再调用 scheduleBackgroundDexoptJob()。因此该广播不会直接执行 dexopt,也不要求此时设备已经空闲;它只是等 JobSchedulerService 可用后提交任务约束。
源码文件:art/libartservice/service/java/com/android/server/art/ArtManagerLocal.java,符号:scheduleBackgroundDexoptJob
public @ScheduleStatus int scheduleBackgroundDexoptJob() {
return mInjector.getBackgroundDexoptJob().schedule(
BackgroundDexoptJob.JobType.BG_DEXOPT);
}ArtManagerLocal 在这里是门面。任务 ID、约束、正在运行的 Future、取消信号和统计时间都属于 BackgroundDexoptJob。调试时若只停在 ArtManagerLocal,看不到任务真正的状态所有者。
2. 两类任务
BackgroundDexoptJob 实际承载两类任务。二者复用运行器,但调度条件和结束策略并不相同。
源码文件:art/libartservice/service/java/com/android/server/art/BackgroundDexoptJob.java,符号:JobType
public enum JobType {
BG_DEXOPT(27873780, ReasonMapping.REASON_BG_DEXOPT),
POST_UNATTENDED_REBOOT(
27873782, ReasonMapping.REASON_POST_UNATTENDED_REBOOT);
private final int jobId;
private final @NonNull String reason;
}| 属性 | BG_DEXOPT | POST_UNATTENDED_REBOOT |
|---|---|---|
| reason | bg-dexopt | post-ur |
| 周期 | 一天 | 一次性 |
| 设备空闲 | 必须 | 不要求 |
| 充电、电量不低 | 必须 | 必须 |
| 最小延迟 | 无额外设置 | 重启后 10 分钟 |
| 完成后 cleanup | 执行 | 不执行 |
| 取消结果主动重调度 | 是 | 否 |
源码文件:art/libartservice/service/java/com/android/server/art/BackgroundDexoptJob.java,符号:schedule
JobInfo.Builder builder =
new JobInfo.Builder(jobType.getJobId(),
new ComponentName(JOB_PKG_NAME,
BackgroundDexoptJobService.class.getName()))
.setRequiresCharging(true)
.setRequiresBatteryNotLow(true);
if (jobType == JobType.BG_DEXOPT) {
builder.setRequiresDeviceIdle(true).setPeriodic(JOB_INTERVAL_MS);
} else {
Utils.check(jobType == JobType.POST_UNATTENDED_REBOOT);
builder.setMinimumLatency(Duration.ofMinutes(10).toMillis());
}“无人值守重启后优化”不要求 idle,是因为它利用无人值守重启后的窗口尽快恢复编译状态;但源码仍设置充电和电量不低,并额外等待十分钟,避开刚启动阶段的资源竞争。
3. 约束边界
pm.dexopt.disable_bg_dexopt 只禁止调度,不禁止显式调用 start()。这是调试后台任务时最容易误判的边界。
源码文件:art/libartservice/service/java/com/android/server/art/BackgroundDexoptJob.java,符号:schedule
if (SystemProperties.getBoolean(
"pm.dexopt.disable_bg_dexopt", false /* def */)) {
AsLog.i("Job is disabled by system property "
+ "'pm.dexopt.disable_bg_dexopt'");
return ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP;
}ART 允许 system_server 通过 ScheduleBackgroundDexoptJobCallback 修改 JobInfo.Builder,例如改变优先级或取消 battery-not-low 条件。不过有一项明确禁止:不能设置 requiresStorageNotLow。
JobInfo info = builder.build();
if (info.isRequireStorageNotLow()) {
throw new IllegalStateException(
"'setRequiresStorageNotLow' must not be set");
}原因不是后台优化忽略存储,而是存储策略属于 ART 批次内部:它可以在空间不足时跳过优化或降级不活跃包。如果让 JobScheduler 因低存储完全不启动任务,降级和清理也失去了运行机会。
4. JobService 分派
源码文件:frameworks/base/core/res/AndroidManifest.xml,符号:BackgroundDexoptJobService
<service android:name="com.android.server.art.BackgroundDexoptJobService"
android:permission="android.permission.BIND_JOB_SERVICE" >
</service>只有 JobScheduler 能绑定这个 service。它是所有 ART job 的系统回调入口,不只服务后台 dexopt。
源码文件:art/libartservice/service/java/com/android/server/art/BackgroundDexoptJobService.java,符号:getJob
static ArtServiceJobInterface getJob(int jobId) {
if (Arrays.stream(BackgroundDexoptJob.JobType.values())
.anyMatch(t -> t.getJobId() == jobId)) {
return LocalManagerRegistry.getManager(ArtManagerLocal.class)
.getBackgroundDexoptJob();
} else if (jobId == PreRebootDexoptJob.JOB_ID
&& SdkLevel.isAtLeastV()) {
return LocalManagerRegistry.getManager(ArtManagerLocal.class)
.getPreRebootDexoptJob();
}
throw new IllegalArgumentException("Unknown job ID " + jobId);
}未知 job ID 会抛出异常,不会默认落到后台 dexopt。onStartJob 和 onStopJob 都先经过这个分派,再调用对应 job 对象,因此开始与停止看到的是同一个状态所有者。
5. 单任务所有权
后台 dexopt 可能运行很久,源码为它创建了独占的单线程 executor,避免占住 system_server 中其他已知线程池。
源码文件:art/libartservice/service/java/com/android/server/art/BackgroundDexoptJob.java,符号:字段与 start
@GuardedBy("this")
@Nullable private CompletableFuture<Result> mRunningJob = null;
@GuardedBy("this")
@Nullable private CancellationSignal mCancellationSignal = null;
private final ThreadPoolExecutor mExecutor =
new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>());
public synchronized CompletableFuture<Result> start(
@NonNull JobType jobType) {
if (mRunningJob != null) {
AsLog.i("Job is already running");
return mRunningJob;
}
mCancellationSignal = new CancellationSignal();
mLastStopReason = Optional.empty();
mRunningJob = new CompletableFuture().supplyAsync(() -> {
try {
return run(jobType, mCancellationSignal);
} catch (RuntimeException e) {
AsLog.wtf("Fatal error", e);
return new FatalErrorResult();
} finally {
synchronized (this) {
mRunningJob = null;
mCancellationSignal = null;
}
}
}, mExecutor);
return mRunningJob;
}这里建立了两个不变量:同一时刻最多有一个后台 job;重复 start() 返回同一个 Future,而不是排队启动第二个 job。无论正常完成还是抛出未预期异常,finally 都在锁内清空 Future 和取消信号,下一次任务才能取得所有权。
6. 包集合
任务开始后不会直接遍历所有已安装包。ArtManagerLocal.dexoptPackages() 先让 ReasonMapping 根据 reason 建立默认包列表。
源码文件:art/libartservice/service/java/com/android/server/art/ReasonMapping.java,符号:getDefaultPackagesForReason
Stream<PackageInfo> packages = snapshot.getPackageStates().values().stream()
.filter(pkgState -> Utils.canDexoptPackage(
pkgState, appHibernationManager))
.map(pkgState -> new PackageInfo(pkgState,
Utils.getPackageLastActiveTime(pkgState,
mInjector.getDexUseManager(),
mInjector.getUserManager()),
Flags.hybridPreRebootDexopt()
? mInjector.getDexUseManager()
.calculateDecayedPackageScore(
pkgState.getPackageName(), now)
: 0D));第一层先排除不能 dexopt 的包,包括已由休眠机制处理产物的包。每个候选包再附上最后活跃时间;特定 feature flag 开启时还会计算衰减后的使用分数。
case ReasonMapping.REASON_INACTIVE ->
packages.filter(pkgInfo -> pkgInfo.lastActiveTime() <= thresholdTimeMs)
.sorted(Comparator
.<PackageInfo>comparingDouble(pkgInfo -> pkgInfo.score())
.thenComparingLong(pkgInfo -> pkgInfo.lastActiveTime()));
default -> {
Comparator<PackageInfo> comparator = Comparator
.<PackageInfo>comparingDouble(PackageInfo::score)
.thenComparingLong(PackageInfo::lastActiveTime)
.reversed();
yield packages
.filter(pkgInfo -> pkgInfo.lastActiveTime() > thresholdTimeMs)
.sorted(comparator);
}bg-dexopt 进入 default 分支,只保留活跃时间晚于阈值的包,并按分数、最后活跃时间降序排列。inactive 恰好相反,用于寻找可降级的长期不活跃包。系统属性 pm.dexopt.downgrade_after_inactive_days 同时参与这两组集合的边界计算。
7. 三个批次
一次 bg-dexopt 最多产生三个 pass,顺序固定为 downgrade、main、supplementary。普通 boot reason 不会进入 downgrade 和 supplementary 分支。
源码文件:art/libartservice/service/java/com/android/server/art/ArtManagerLocal.java,符号:dexoptPackagesWithParams
if (reason.equals(ReasonMapping.REASON_BG_DEXOPT)) {
DexoptResult downgradeResult = maybeDowngradePackages(
snapshot,
new HashSet<>(params.getPackages()),
cancellationSignal, dexoptExecutor,
progressCallbackExecutor,
progressCallbacks != null
? progressCallbacks.get(ArtFlags.PASS_DOWNGRADE)
: null);
if (downgradeResult != null) {
dexoptResults.put(ArtFlags.PASS_DOWNGRADE, downgradeResult);
}
}
DexoptResult mainResult = mInjector.getDexoptHelper().dexopt(
snapshot, params.getPackages(), params.getDexoptParams(),
cancellationSignal, dexoptExecutor,
progressCallbackExecutor,
progressCallbacks != null
? progressCallbacks.get(ArtFlags.PASS_MAIN) : null);
dexoptResults.put(ArtFlags.PASS_MAIN, mainResult);降级批次把主批次包集合放入 excludedPackages,保证近期活跃并准备优化的包不会同时被降级。主批次始终产生结果,即使包集合为空或所有包都被跳过,也以 PASS_MAIN 进入结果映射。
源码文件:art/libartservice/service/java/com/android/server/art/ArtManagerLocal.java,符号:maybeDexoptPackagesSupplementaryPass
List<String> packageNames = mainResult.getPackageDexoptResults().stream()
.filter(packageResult -> packageResult
.getDexContainerFileDexoptResults().stream()
.anyMatch(fileResult ->
DexFile.isProfileGuidedCompilerFilter(
fileResult.getActualCompilerFilter())
&& fileResult.getStatus()
== DexoptResult.DEXOPT_SKIPPED))
.map(PackageDexoptResult::getPackageName)
.toList();
DexoptParams dexoptParams = mainParams.toBuilder()
.setFlags(ArtFlags.FLAG_FORCE_MERGE_PROFILE,
ArtFlags.FLAG_FORCE_MERGE_PROFILE)
.build();补充批次不是重试所有失败包。它只挑选实际使用 profile-guided filter 且主批次状态为 DEXOPT_SKIPPED 的包,然后加上 FLAG_FORCE_MERGE_PROFILE 再执行。这解决的是 profile 尚未满足普通 merge 条件导致的跳过,不是通用失败重试。
8. 存储策略
源码文件:art/libartservice/service/java/com/android/server/art/ArtManagerLocal.java,符号:shouldDowngrade
private boolean shouldDowngrade() {
try {
return mInjector.getStorageManager()
.getAllocatableBytes(StorageManager.UUID_DEFAULT)
< DOWNGRADE_THRESHOLD_ABOVE_LOW_BYTES;
} catch (IOException e) {
AsLog.e("Failed to check storage. Assuming storage not low", e);
return false;
}
}阈值比较使用 StorageManager.getAllocatableBytes(),常量 DOWNGRADE_THRESHOLD_ABOVE_LOW_BYTES 为 500,000,000 字节。读取存储状态发生 I/O 异常时,ART 选择“假定存储不低”,即跳过 downgrade;它不会因为无法判断空间就批量降级应用。
源码文件:art/libartservice/service/java/com/android/server/art/ArtManagerLocal.java,符号:maybeDowngradePackages
List<String> packages = mInjector.getReasonMapping()
.getDefaultPackagesForReason(snapshot,
ReasonMapping.REASON_INACTIVE)
.stream()
.filter(pkg -> !excludedPackages.contains(pkg))
.toList();
DexoptParams params = new DexoptParams.Builder(
ReasonMapping.REASON_INACTIVE).build();
return mInjector.getDexoptHelper().dexopt(
snapshot, packages, params, cancellationSignal,
executor, progressCallbackExecutor, progressCallback);降级仍然走统一的 DexoptHelper.dexopt(),compiler filter 由 inactive reason 的系统属性决定,通常是较轻的编译模式。它不是简单删除所有产物,也不会处理主批次已选择的活跃包。
9. 取消与重调度
JobScheduler 发现充电、空闲或电量约束不再满足时,会调用 onStopJob()。ART 先保存 stop reason,再取消共享信号。
源码文件:art/libartservice/service/java/com/android/server/art/BackgroundDexoptJob.java,符号:onStopJob、cancel
public boolean onStopJob(@NonNull JobParameters params) {
synchronized (this) {
mLastStopReason = Optional.of(params.getStopReason());
}
cancel();
return true;
}
public synchronized void cancel() {
if (mRunningJob == null) {
AsLog.i("Job is not running");
return;
}
mCancellationSignal.cancel();
AsLog.i("Job cancelled");
}cancel() 是非阻塞操作:它只改变 CancellationSignal,实际 dexopt worker 在检查信号后产生 DEXOPT_CANCELLED。onStopJob() 返回 true,让 JobScheduler 按默认退避策略重新尝试。
源码文件:art/libartservice/service/java/com/android/server/art/BackgroundDexoptJob.java,符号:onStartJob
wantsReschedule = result instanceof CompletedResult
&& ((CompletedResult) result).isCancelled();
jobService.jobFinished(params, wantsReschedule);完成回调还有更窄的判断:只有周期性的 BG_DEXOPT 产生 CompletedResult 且其中任一 pass 最终状态为 DEXOPT_CANCELLED,才向 jobFinished 传 wantsReschedule=true。FatalErrorResult 和 POST_UNATTENDED_REBOOT 的取消结果都不会走这个主动重调度分支。
10. 收尾清理
源码文件:art/libartservice/service/java/com/android/server/art/BackgroundDexoptJob.java,符号:run
try (var snapshot = mInjector.getPackageManagerLocal()
.withFilteredSnapshot()) {
dexoptResultByPass = mInjector.getArtManagerLocal()
.dexoptPackages(snapshot, jobType.getReason(),
cancellationSignal, Runnable::run,
progressCallbacks);
}
if (jobType == JobType.BG_DEXOPT
&& !cancellationSignal.isCanceled()) {
try (var snapshot = mInjector.getPackageManagerLocal()
.withFilteredSnapshot()) {
long freedBytes = mInjector.getArtManagerLocal()
.cleanup(snapshot);
AsLog.i(String.format("Freed %d bytes", freedBytes));
}
cleanupLegacyDexoptFiles();
}cleanup 有三个关键条件:只属于 BG_DEXOPT;任务不能已取消;必须重新获取包快照。第二个快照不能复用 dexopt 前的快照,因为长时间编译期间可能安装或删除了包,使用旧快照清理会依据过时的包集合判断产物归属。
清理被安排在 dexopt 之后还有统计原因:提前清理会改变回调中 getSizeBeforeBytes 的语义。代价是极低存储时本轮可能失去部分优化机会,源码接受这个取舍,并把机会留给下一次运行。
11. 测试反证
源码文件:art/libartservice/service/javatests/com/android/server/art/BackgroundDexoptJobTest.java,符号:testStartAlreadyRunning
Future<Result> future1 = mBackgroundDexoptJob.start(
POST_UNATTENDED_REBOOT);
Future<Result> future2 = mBackgroundDexoptJob.start(BG_DEXOPT);
assertThat(future1).isSameInstanceAs(future2);
verify(mArtManagerLocal, times(1)).dexoptPackages(
any(), any(), any(), any(), any());测试先让第一次 dexopt 阻塞,再启动另一种 job。断言两个调用返回同一个 Future,且 dexoptPackages 只执行一次。这反向验证了“跨 JobType 也只能有一个运行实例”,但它不证明底层每个包只能串行 dexopt;包级并发由 reason 对应的 concurrency 决定。
源码文件:art/libartservice/service/javatests/com/android/server/art/BackgroundDexoptJobTest.java,符号:testStartBgDexopt、testStartPostUr
when(mPackageManagerLocal.withFilteredSnapshot())
.thenReturn(mSnapshot1)
.thenReturn(mSnapshot2);
Utils.getFuture(mBackgroundDexoptJob.start(BG_DEXOPT));
verify(mArtManagerLocal).cleanup(same(mSnapshot2));
Utils.getFuture(mBackgroundDexoptJob.start(POST_UNATTENDED_REBOOT));
verify(mArtManagerLocal, never()).cleanup(any());这组测试区分了两个 job 的结束路径:普通后台任务使用第二个快照清理,post-UR 不清理。它证明的是 job runner 的调用约束,不覆盖 ArtManagerLocal.cleanup() 内部具体删除哪些文件。
源码文件:art/libartservice/service/javatests/com/android/server/art/BackgroundDexoptJobTest.java,符号:testWantsRescheduleTrue
DexoptResult mainResult = createDexoptResultWithStatus(
DexoptResult.DEXOPT_CANCELLED);
mDexoptResultByPass.put(ArtFlags.PASS_MAIN, mainResult);
mBackgroundDexoptJob.onStartJob(mJobService, mJobParameters);
verify(mJobService).jobFinished(
any(), eq(true) /* wantsReschedule */);测试把 main pass 构造成取消结果,并断言周期任务要求重调度。相邻测试还验证:正常完成、fatal error 和 post-UR 取消均传 false。因此不能把所有失败或停止都描述为“稍后自动重试”。
12. 现场定位
遇到“后台 dexopt 没运行”时,可以按所有权顺序定位,而不是先猜 compiler filter:
- 检查
pm.dexopt.disable_bg_dexopt是否阻止了 schedule;显式 start 不受它影响。 - 在 JobScheduler 状态中确认 job ID
27873780是否存在,以及 idle、charging、battery-not-low 哪项未满足。 - 确认
BackgroundDexoptJobService收到的是已知 job ID,mRunningJob是否已有任务占用。 - 检查默认包列表是否因 hibernation 或 inactive threshold 为空。
- 分别读取 downgrade、main、supplementary pass 的最终状态,不要只看总任务完成。
- 若任务被停止,结合
JobParameters.getStopReason()与DEXOPT_CANCELLED判断是否应当重调度。 - 若 dexopt 已完成但空间没有变化,确认 cleanup 是否因 post-UR 或 cancellation 被跳过。
读完源码后,应能从 LOCKED_BOOT_COMPLETED 复述到 jobFinished(),并解释三件事:为什么存储不足不能成为 JobScheduler 约束、为什么重复 start 不会排队第二个任务、为什么 cleanup 必须使用新的包快照。这三点分别对应调度策略、任务所有权和资源生命周期。
