本文作者:熊大

导言

在热修正和插件化场景中会触及动态加载 dex,要使它们中代码的履行速度与装置的 APK 适当,需要对它们进行正确的优化。依据以往的经历,在热修正场景中,错误的办法导致 dex 没有得到优化时,修正后 App 的发动速度比修正前慢 50%。本文将在下面的部分介绍在 Android 5.0 以来的各体系版别中对动态加载的 dex 进行优化的办法及原理。

Android 5

从 Android 5.0 开始,体系引进了预先编译机制(AOT),在运用装置时,运用 dex2oat 东西将 dex 编译为可履行文件。

此刻能够经过 DexFile.loadDex 来触发 dex2oat 优化 dex,其调用进程如下:

DexFile.loadDex -> new DexFile -> DexFile.openDexFile -> DexFile.openDexFileNative -> DexFile_openDexFileNative -> ClassLinker::OpenDexFilesFromOat -> ClassLinker::CreateOatFileForDexLocation -> ClassLinker::GenerateOatFile

能够看到在 ClassLinker::GenerateOatFile 函数中会履行 dex2oat 指令来优化 dex。

// art/runtime/class_linker.cc(android-5.0.2)
bool ClassLinker::GenerateOatFile(const char* dex_filename,
                                  int oat_fd,
                                  const char* oat_cache_filename,
                                  std::string* error_msg) {
  // ...
  std::vector<std::string> argv;
  argv.push_back(dex2oat);
  argv.push_back("--runtime-arg");
  argv.push_back("-classpath");
  argv.push_back("--runtime-arg");
  argv.push_back(Runtime::Current()->GetClassPathString());
  Runtime::Current()->AddCurrentRuntimeFeaturesAsDex2OatArguments(&argv);
  if (!Runtime::Current()->IsVerificationEnabled()) {
    argv.push_back("--compiler-filter=verify-none");
  }
  if (Runtime::Current()->MustRelocateIfPossible()) {
    argv.push_back("--runtime-arg");
    argv.push_back("-Xrelocate");
  } else {
    argv.push_back("--runtime-arg");
    argv.push_back("-Xnorelocate");
  }
  if (!kIsTargetBuild) {
    argv.push_back("--host");
  }
  argv.push_back(boot_image_option);
  argv.push_back(dex_file_option);
  argv.push_back(oat_fd_option);
  argv.push_back(oat_location_option);
  const std::vector<std::string>& compiler_options = Runtime::Current()->GetCompilerOptions();
  for (size_t i = 0; i < compiler_options.size(); ++i) {
    argv.push_back(compiler_options[i].c_str());
  }
  return Exec(argv, error_msg);
}

所以,可用 DexFile.loadDex 进行 dex 优化。

dex 优化编年史

Android 7

从 Android 7.0 开始,为处理 AOT 带来的装置时间长和占用空间大等问题,体系引进了配置文件引导型编译,结合 AOT 和 即时编译(JIT)一起运用:

  1. 运用装置时不再进行 AOT 编译
  2. 在运用的运转进程中,对未编译的代码进行解释,将履行的办法信息记载到配置文件中,并对经常履行的办法进行 JIT 编译
  3. 当设备搁置和充电时,依据生成的配置文件对常用代码进行 AOT 编译

配置文件引导型编译跟曾经的 AOT 编译的一个首要区别是履行 dex2oat 时运用的编译过滤器不同,前者运用 speed-profile,而后者运用 speed。dex2oat 的一切编译过滤器界说在 compiler_filter.h 中,在不同体系版别中类型会有变化,首要有以下 4 种:

  • verify:仅运转 dex 代码验证
  • quicken:运转 dex 代码验证,并优化一些 dex 指令,以取得更好的解释器性能(Android 8 引进,Android 12 移除)
  • speed:运转 dex 代码验证,并对一切办法进行 AOT 编译
  • speed-profile:运转 dex 代码验证,并对配置文件中列出的办法进行 AOT 编译

编译过滤器会影响 dex 优化的作用,回到前面给出的优化办法 DexFile.loadDex,其在新体系版别中会运用优化所需的编译过滤器吗?DexFile.loadDex 在 Android 7.0 上调用进程如下:

DexFile.loadDex -> new DexFile -> DexFile.openDexFile -> DexFile.openDexFileNative -> DexFile_openDexFileNative -> OatFileManager::OpenDexFilesFromOat -> OatFileAssistant::MakeUpToDate -> OatFileAssistant::GenerateOatFile

仍然会在 OatFileAssistant::GenerateOatFile 函数中履行 dex2oat 指令,运用的编译过滤器是在调 OatFileAssistant::MakeUpToDate 函数时传入的 speed,所以 DexFile.loadDex 在 Android 7.0 上仍然适用。

// art/runtime/oat_file_manager.cc(android-7.0.0)
CompilerFilter::Filter OatFileManager::filter_ = CompilerFilter::Filter::kSpeed;
std::vector<std::unique_ptr<const DexFile>> OatFileManager::OpenDexFilesFromOat(
    const char* dex_location,
    const char* oat_location,
    jobject class_loader,
    jobjectArray dex_elements,
    const OatFile** out_oat_file,
    std::vector<std::string>* error_msgs) {
  // ...
  if (!oat_file_assistant.IsUpToDate()) {
    // ...
    switch (oat_file_assistant.MakeUpToDate(filter_, /*out*/ &error_msg)) {
      // ...
    }
  }
  // ...        
}

Android 8

在 Android 8.0 中,DexFile.loadDex 的调用进程根本不变,但编译过滤器改为经过 GetRuntimeCompilerFilterOption 函数得到。

// art/runtime/oat_file_assistant.cc(android-8.0.0)
OatFileAssistant::MakeUpToDate(bool profile_changed, std::string* error_msg) {
  CompilerFilter::Filter target;
  if (!GetRuntimeCompilerFilterOption(&target, error_msg)) {
    return kUpdateNotAttempted;
  }
  // ...
}

GetRuntimeCompilerFilterOption 函数优先取当时 Runtime 的发动参数 --compiler-filter 指定的编译过滤器,如不存在,则用默认的 quicken

// art/runtime/oat_file_assistant.cc(android-8.0.0)
static bool GetRuntimeCompilerFilterOption(CompilerFilter::Filter* filter,
                                           std::string* error_msg) {
  // ...
  *filter = OatFileAssistant::kDefaultCompilerFilterForDexLoading;
  for (StringPiece option : Runtime::Current()->GetCompilerOptions()) {
    if (option.starts_with("--compiler-filter=")) {
      const char* compiler_filter_string = option.substr(strlen("--compiler-filter=")).data();
      if (!CompilerFilter::ParseCompilerFilter(compiler_filter_string, filter)) {
        // ...
        return false;
      }
    }
  }
  return true;
}
// art/runtime/oat_file_assistant.h(android-8.0.0)
class OatFileAssistant {
 public:
  // The default compile filter to use when optimizing dex file at load time if they
  // are out of date.
  static const CompilerFilter::Filter kDefaultCompilerFilterForDexLoading =
      CompilerFilter::kQuicken;
  // ...      
};

Runtime 的发动参数 --compiler-filter 的值由设置的体系特点 vold.decryptdalvik.vm.dex2oat-filter 决议:

  1. 如果 vold.decrypt 特点值等于 trigger_restart_min_framework1,则为 assume-verified
  2. 否则为 dalvik.vm.dex2oat-filter 特点值
// frameworks/base/core/jni/AndroidRuntime.cpp(android-8.0.0)
int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote)
{
    // ...
    property_get("vold.decrypt", voldDecryptBuf, "");
    bool skip_compilation = ((strcmp(voldDecryptBuf, "trigger_restart_min_framework") == 0) ||
                             (strcmp(voldDecryptBuf, "1") == 0));
    // ...
    if (skip_compilation) {
        addOption("-Xcompiler-option");
        addOption("--compiler-filter=assume-verified");
        // ...
    } else {
        parseCompilerOption("dalvik.vm.dex2oat-filter", dex2oatCompilerFilterBuf,
                            "--compiler-filter=", "-Xcompiler-option");
    }
}

经过 adb 用 getprop 指令检查体系特点值,可知 vold.decrypt 的特点值不等于 trigger_restart_min_framework1,且 dalvik.vm.dex2oat-filter 特点不存在,所以 DexFile.loadDex 终究运用的编译过滤器为 quicken,达不到 dex 优化的要求。

无法完成对 dex 的一切办法进行 AOT 编译,能够退而求其次,经过创立 BaseDexClassLoader 或其子类目标,让动态加载的 dex 跟装置的运用一样,初始只做根本优化,随着代码的运转,常用代码会被 AOT 编译。

BaseDexClassLoader 的结构办法会顺次履行以下 2 个进程来别离完成根本优化和对常用代码进行 AOT 编译:

  1. 创立 DexPathList 目标
  2. 履行 DexLoadReporter.report 办法
// libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java(android-8.0.0)
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
        String librarySearchPath, ClassLoader parent) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
    if (reporter != null) {
        reporter.report(this.pathList.getDexPaths());
    }
}

首先,创立 DexPathList 目标会触发创立 DexFile 目标,进而会如前文所述运用编译过滤器 quicken 履行根本优化,调用进程如下:

new DexPathList -> DexPathList.makeDexElements -> DexPathList.loadDexFile -> new DexFile

在介绍为什么 DexLoadReporter.report 办法能够让动态加载的 dex 能被 AOT 编译之前,先看看 BaseDexClassLoader.reporter 的来源。在运用发动进程中,体系会依据 dalvik.vm.usejitprofiles 特点值来决议是否将 DexLoadReporter 单例设给 BaseDexClassLoader 的静态变量 reporter,经过 getprop 指令检查可知 dalvik.vm.usejitprofiles 特点值为 true,所以 BaseDexClassLoader.reporter 的值为 DexLoadReporter 单例。

// frameworks/base/core/java/android/app/ActivityThread.java(android-8.0.0)
private void handleBindApplication(AppBindData data) {
    // ...
    if (SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false)) {
        BaseDexClassLoader.setReporter(DexLoadReporter.getInstance());
    }
    // ...
}

DexLoadReporter.report 办法经过履行如下 2 步来完成动态加载的 dex 会被体系履行基于配置文件的 AOT 编译:

  1. PackageManagerService 注册 dex 运用信息,使体系在履行后台 dex 优化时能取得动态加载的 dex 信息进行优化
  2. VMRuntime 注册记载履行的办法信息的配置文件,使动态加载的 dex 中的办法被履行时也会被记载
// frameworks/base/core/java/android/app/DexLoadReporter.java(android-8.0.0)
public void report(List<String> dexPaths) {
    // ...
    // Notify the package manager about the dex loads unconditionally.
    // The load might be for either a primary or secondary dex file.
    notifyPackageManager(dexPaths);
    // Check for secondary dex files and register them for profiling if
    // possible.
    registerSecondaryDexForProfiling(dexPaths);
}

notifyPackageManager 办法经过如下调用进程后,将 dex 信息注册到 PackageDexUsage 中,并写入到 /data/system/package-dex-usage.list 文件中。

DexLoadReporter.notifyPackageManager -> PackageManagerService.notifyDexLoad -> DexManager.notifyDexLoad -> PackageDexUsage.record -> PackageDexUsage.maybeWriteAsync

registerSecondaryDexForProfiling 办法会以 dex 文件途径加 .prof 后缀作为途径创立配置文件,并将其注册到 VMRuntime 中。在履行进程中会判别 dex 文件是否是 secondary dex 文件,即非装置的 APK 文件,判别办法为 dex 文件是否位于运用的 data 目录中,所以需要将动态加载的 dex 放在运用的 data 目录中。

最后来剖析下体系履行后台 dex 优化的流程,看看经过创立 BaseDexClassLoader 或其子类目标的办法注册到 PackageDexUsage 中的 dex 能否被优化。体系会在发动时向 JobScheduler 注册后台 dex 优化使命,调用进程如下:

SystemServer.run -> SystemServer.startOtherServices -> BackgroundDexOptService.schedule

后台 dex 优化使命会在设备闲暇且充电时履行,使命履行时间间隔至少 1 天。

// frameworks/base/services/core/java/com/android/server/pm/BackgroundDexOptService.java(android-8.0.0)
public static void schedule(Context context) {
    JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
    // ...
    js.schedule(new JobInfo.Builder(JOB_IDLE_OPTIMIZE, sDexoptServiceName)
                .setRequiresDeviceIdle(true)
                .setRequiresCharging(true)
                .setPeriodic(IDLE_OPTIMIZATION_PERIOD)
                .build());
    // ...
}

体系履行后台 dex 优化使命的调用进程如下:

BackgroundDexOptService.onStartJob -> BackgroundDexOptService.runIdleOptimization -> BackgroundDexOptService.idleOptimization

BackgroundDexOptService.idleOptimization 办法中,会依据 dalvik.vm.dexopt.secondary 特点值决议是否对 secondary dex 进行优化,运用 getprop 指令检查可知该特点值为 true,所以后台 dex 优化的目标包括 secondary dex。

// frameworks/base/services/core/java/com/android/server/pm/BackgroundDexOptService.java(android-8.0.0)
private int idleOptimization(PackageManagerService pm, ArraySet<String> pkgs, Context context) {
    // ...
    if (SystemProperties.getBoolean("dalvik.vm.dexopt.secondary", false)) {
        // ...
        result = optimizePackages(pm, pkgs, lowStorageThreshold, /*is_for_primary_dex*/ false,
                sFailedPackageNamesSecondary);
    }
    return result;
}

后续对 secondary dex 进行优化的调用进程如下,终究经过从 ServiceManager 获取的 installd 服务供给的 dexopt 接口履行 dex 优化。

BackgroundDexOptService.optimizePackages -> PackageManagerService.performDexOptSecondary -> DexManager.dexoptSecondaryDex -> DexManager.dexoptSecondaryDex -> PackageDexOptimizer.dexOptSecondaryDexPath -> PackageDexOptimizer.dexOptSecondaryDexPathLI -> Installer.dexopt

DexManager.dexoptSecondaryDex 办法中,会先从 PackageDexUsage 获取已注册的 dex 信息,然后履行 dex 优化,所以只有已注册到 PackageDexUsage 中的 dex 能被优化。

// frameworks/base/services/core/java/com/android/server/pm/dex/DexManager.java(android-8.0.0)
public boolean dexoptSecondaryDex(String packageName, String compilerFilter, boolean force) {
    // ...
    PackageUseInfo useInfo = getPackageUseInfo(packageName);
    // ...
    for (Map.Entry<String, DexUseInfo> entry : useInfo.getDexUseInfoMap().entrySet()) {
        // ...
        int result = pdo.dexOptSecondaryDexPath(pkg.applicationInfo, dexPath,
                dexUseInfo.getLoaderIsas(), compilerFilter, dexUseInfo.isUsedByOtherApps());
        // ...
    }
    // ...
}
public PackageUseInfo getPackageUseInfo(String packageName) {
    return mPackageDexUsage.getPackageUseInfo(packageName);
}

前文说到注册 dex 时会将 dex 信息写入文件,且在体系发动创立 PackageManagerService 目标时会读取文件中的 dex 信息,调用进程如下:

new PackageManagerService -> DexManager.load -> DexManager.loadInternal -> PackageDexUsage.read

所以即使从注册 dex 到本次体系生命周期结束都没满意履行后台 dex 优化条件,但下次体系发动后,曾经注册的 dex 还能够在满意履行条件时被优化。

installd 服务运转于 installd 守护进程中,该进程在体系发动时由 init 进程发动,并在发动时创立 installd 服务实例注册到 ServiceManager 中。installd 服务的 dexopt 接口经过如下调用进程后,终究会履行 dex2oat 指令。

InstalldNativeService::dexopt -> android::installd::dexopt -> run_dex2oat

经过以上剖析,能够确认创立 BaseDexClassLoader 或其子类目标能够让动态加载的 dex 得到跟装置的运用一样的优化作用。

dex 优化编年史

Android 10

创立 BaseDexClassLoader 或其子类目标,在 Android 10 及以上体系中,仍然能在体系履行后台 dex 优化时对动态加载的 dex 进行优化,但从 Android 10 开始,体系引进了 class loader context,要求有必要创立 PathClassLoaderDexClassLoaderDelegateLastClassLoader 的目标,能够选择用 PathClassLoader

// frameworks/base/services/core/java/com/android/server/pm/dex/DexManager.java(android-10.0.0)
/*package*/ void notifyDexLoadInternal(ApplicationInfo loadingAppInfo,
        List<String> classLoaderNames, List<String> classPaths, String loaderIsa,
        int loaderUserId) {
    // ...
    String[] classLoaderContexts = DexoptUtils.processContextForDexLoad(
            classLoaderNames, classPaths);
    // ...
    for (String dexPath : dexPathsToRegister) {
        // ...
        if (searchResult.mOutcome != DEX_SEARCH_NOT_FOUND) {
            // ...
            if (classLoaderContexts != null) {
                // ...
                if (mPackageDexUsage.record(searchResult.mOwningPackageName,
                        dexPath, loaderUserId, loaderIsa, isUsedByOtherApps, primaryOrSplit,
                        loadingAppInfo.packageName, classLoaderContext)) {
                    mPackageDexUsage.maybeWriteAsync();
                }
            }
        } else {
            // ...
        }
        // ...
    }
}
// frameworks/base/services/core/java/com/android/server/pm/dex/DexoptUtils.java(android-10.0.0)
/*package*/ static String[] processContextForDexLoad(List<String> classLoadersNames,
        List<String> classPaths) {
    // ...
    for (int i = 1; i < classLoadersNames.size(); i++) {
        if (!ClassLoaderFactory.isValidClassLoaderName(classLoadersNames.get(i))
            || classPaths.get(i) == null) {
            return null;
        }
        // ...
    }
    // ...
}
public static boolean isValidClassLoaderName(String name) {
    // This method is used to parse package data and does not accept null names.
    return name != null && (isPathClassLoaderName(name) || isDelegateLastClassLoaderName(name));
}

从 Android 10 开始,创立 DexFile 目标不再会触发履行 dex2oat 指令,所以创立 PathClassLoader 目标已无法完成在初始时对 dex 进行根本优化。

同时,从 Android 10 开始,SELinux 增加了对运用履行 dex2oat 指令的限制,所以也无法经过 ProcessBuilderRuntime 履行 dex2oat 指令来对 dex 进行根本优化。在 file_contexts 文件中界说了 dex2oat 东西的安全上下文,指定了只有具有 dex2oat_exec 的权限的进程才能履行 dex2oat。

# system/sepolicy/private/file_contexts(android-10.0.0)
/system/bin/dex2oat(d)?     u:object_r:dex2oat_exec:s0

seapp_contexts 文件中指定了不同 targetSdkVersion 对应的进程安全上下文类型,例如当运用的 targetSdkVersion 大于等于 29 时,其进程安全上下文类型为 untrusted_app。

# system/sepolicy/private/seapp_contexts(android-10.0.0)
user=_app minTargetSdkVersion=29 domain=untrusted_app type=app_data_file levelFrom=all
user=_app minTargetSdkVersion=28 domain=untrusted_app_27 type=app_data_file levelFrom=all
user=_app minTargetSdkVersion=26 domain=untrusted_app_27 type=app_data_file levelFrom=user
user=_app domain=untrusted_app_25 type=app_data_file levelFrom=user

可经过 ps -Z 指令来检查进程的安全上下文,结果跟规矩指定的一致:

  • targetSdkVersion = 28:u:r:untrusted_app_27:s0:c101,c259,c512,c768
  • targetSdkVersion = 29:u:r:untrusted_app:s0:c101,c259,c512,c768

不同进程安全上下文类型所具有的权限界说在跟类型对应的文件中,与 targerSdkVersion 小于 29 的运用对应的权限规矩文件中声明了运用进程有 dex2oat_exec 类型的读和履行权限,而与 targerSdkVersion 大于等于 29 的运用对应的文件中没有对 dex2oat_exec 类型的权限的声明,所以在 Android 10 及以上体系中,当运用的 targetSdkVersion 大于等于 29 时,无法在运用进程中履行 dex2oat 指令。

# system/sepolicy/private/untrusted_app_27.te(android-10.0.0)
# The ability to invoke dex2oat. Historically required by ART, now only
# allowed for targetApi<=28 for compat reasons.
allow untrusted_app_27 dex2oat_exec:file rx_file_perms;
userdebug_or_eng(`auditallow untrusted_app_27 dex2oat_exec:file rx_file_perms;')
# system/sepolicy/private/untrusted_app.te(android-10.0.0)
typeattribute untrusted_app coredomain;
app_domain(untrusted_app)
untrusted_app_domain(untrusted_app)
net_domain(untrusted_app)
bluetooth_domain(untrusted_app)

PMS 经过 aidl 供给了 performDexOptSecondary 接口,可对 secondary dex 进行优化,且能指定编译过滤器,可用来完成初始时的根本优化。该接口经过 Binder 的 shell command 办法对外露出,调用进程如下:

Binder.onTransact -> Binder.shellCommand -> PackageManagerService.onShellCommand -> ShellCommand.exec -> PackageManagerShellCommand.onCommand -> PackageManagerShellCommand.runCompile -> PackageManagerService.performDexOptSecondary

所以可经过反射获取 PMS 的 Binder 接口实例,然后用对应的 transation code 来调 Binder 的 shell command 接口,传入调 performDexOptSecondary 接口所需的参数的办法来让 PMS 履行对 secondary dex 的优化。

fun performDexOptSecondaryByShellCommand(context: Context) {
  runCatching {
    val pm = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String::class.java).invoke(null, "package") as? IBinder
    var data: Parcel? = null
    var reply: Parcel? = null
    val lastIdentity = Binder.clearCallingIdentity()
    try {
      data = Parcel.obtain()
      reply = Parcel.obtain()
      data.writeFileDescriptor(FileDescriptor.`in`)
      data.writeFileDescriptor(FileDescriptor.out)
      data.writeFileDescriptor(FileDescriptor.err)
      val args = arrayOf("compile", "-f", "--secondary-dex", "-m", if (Build.VERSION.SDK_INT >= 31) "verify" else "speed-profile", context.packageName)
      data.writeStringArray(args)
      data.writeStrongBinder(null)
      ResultReceiver(Handler(Looper.getMainLooper())).writeToParcel(data, 0)
      val shellCommandTransaction: Int = '_'.toInt() shl 24 or ('C'.toInt() shl 16) or ('M'.toInt() shl 8) or 'D'.toInt()
      pm?.transact(shellCommandTransaction, data, reply, 0)
      reply.readException()
    } finally {
      reply?.recycle()
      data?.recycle()
      Binder.restoreCallingIdentity(lastIdentity)
    }
  }.onFailure { it.printStackTrace() }
}

初始时对 dex 进行根本优化与运用装置对应,运用的编译过滤器也应保持一致,运用装置场景运用的编译过滤器由 pm.dexopt.install 体系特点指定,其值为 speed-profile,在 Android 12 及以上体系中,可用新引进的 pm.dexopt.install-bulk-secondary 特点的值 verify

综上,可结合创立 PathClassLoader 目标和调 PMS 供给的 performDexOptSecondary 接口来对动态加载的 dex 进行作用跟装置的运用一样的优化。

dex 优化编年史

小结

本文在剖析体系相关完成的基础上,介绍了在 Android 5.0 以来的各体系版别中完成对动态加载的 dex 进行优化,使履行速度与装置的 APK 适当的办法:

  1. 体系版别大于等于 5.0 且小于 8.0:运用 DexFile.loadDex
  2. 体系版别大于等于 8.0 且小于 10:创立 PathClassLoader 目标
  3. 体系版别大于等于 10:创立 PathClassLoader 目标,并经过 PMS Binder 的 shell command 调 performDexOptSecondary 接口

参考资料

  • ART
  • Android SELinux 概念
  • Android 各版别源码

本文发布自网易云音乐技能团队,文章未经授权制止任何形式的转载。咱们常年招收各类技能岗位,如果你预备换工作,又恰好喜欢云音乐,那就加入咱们 grp.music-fe(at)corp.netease.com!