Skip to content

后台 dexopt

追踪后台 dexopt 从系统启动、任务约束、批次执行到取消重调度和空间清理的完整控制流。

基于android-17.0.0_r1
AndroidPMSDexOptARTJobScheduler

后台 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

java
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

java
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

java
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_DEXOPTPOST_UNATTENDED_REBOOT
reasonbg-dexoptpost-ur
周期一天一次性
设备空闲必须不要求
充电、电量不低必须必须
最小延迟无额外设置重启后 10 分钟
完成后 cleanup执行不执行
取消结果主动重调度是否

源码文件:art/libartservice/service/java/com/android/server/art/BackgroundDexoptJob.java,符号:schedule

java
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

java
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。

java
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

xml
<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

java
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

java
@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

java
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 开启时还会计算衰减后的使用分数。

java
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

java
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

java
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

java
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

java
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

java
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

java
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

java
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

java
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

java
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

java
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:

  1. 检查 pm.dexopt.disable_bg_dexopt 是否阻止了 schedule;显式 start 不受它影响。
  2. 在 JobScheduler 状态中确认 job ID 27873780 是否存在,以及 idle、charging、battery-not-low 哪项未满足。
  3. 确认 BackgroundDexoptJobService 收到的是已知 job ID,mRunningJob 是否已有任务占用。
  4. 检查默认包列表是否因 hibernation 或 inactive threshold 为空。
  5. 分别读取 downgrade、main、supplementary pass 的最终状态,不要只看总任务完成。
  6. 若任务被停止,结合 JobParameters.getStopReason() 与 DEXOPT_CANCELLED 判断是否应当重调度。
  7. 若 dexopt 已完成但空间没有变化,确认 cleanup 是否因 post-UR 或 cancellation 被跳过。

读完源码后,应能从 LOCKED_BOOT_COMPLETED 复述到 jobFinished(),并解释三件事:为什么存储不足不能成为 JobScheduler 约束、为什么重复 start 不会排队第二个任务、为什么 cleanup 必须使用新的包快照。这三点分别对应调度策略、任务所有权和资源生命周期。