乐趣区

关于android:dex-优化编年史

本文作者:熊大

引言

在热修复和插件化场景中会波及动静加载 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 优化。

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 失去跟装置的利用一样的优化成果。

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 进行成果跟装置的利用一样的优化。

小结

本文在剖析零碎相干实现的根底上,介绍了在 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!

退出移动版