目录

dex 和 mmap

背景

最近依葫芦画瓢搞了一个 dex2oat 的优化实验,用于冷启动场景

全量编译的情况下,劣化 400ms,部分编译的情况下有大概 100ms(数据在逐渐缩小)

dex2oat 的现状

其实在 Android 10 以后,之前的 dex2oat 命令行已经失效了,目前能有效触发 dex2oat 的只能依靠 jit 强制编译命令

可以查阅文档
https://source.android.com/devices/tech/dalvik/jit-compiler#force-compilation-of-a-specific-package

但是,通过这次提交以后
https://android-review.googlesource.com/c/platform/frameworks/base/+/1699071

Android12 上进行 dex2oat 的路已经被封死了

当然,因为国内的一些 ROM 的独特性···可能存在一些未被封死的情况

我用小米手机测试了一下,运行 dex2oat 会秒返回 success(正常应该会有个几十秒的等待时间),应该并没有真实的执行,因为手机并不是 root 的,无法确定有没有 odex 文件的变化,这个可以后续找个 root 手机实锤一下

Baseline Profiles

不过!现在有了个神奇的 Baseline Profiles 的功能,https://developer.android.com/topic/performance/baselineprofiles

看起来似乎是可以让你本地跑出一个 profile 文件,然后和 apk 一起打包,根据这个 profile 更快的帮你进行 dex2oat(不然可能需要收集很久的热点代码)

目前还没有进行尝试,主要是 profile 的生成似乎需要 AGP7.1.0 的支持,感觉国内大厂的 AGP 版本都停留在了 4.x

dex2oat 有优化效果么

因为系统也会帮忙做 dex2oat 操作,只是可能需要一些"闲时"的环境,导致 dex2oat 可能不是那么及时。

所以,我们手动强制 dex2oat,优化效果当然是有的,但是也有一些前提。

https://juejin.cn/post/7016531198516133902

这里有一篇字节的 dex2oat 的优化,后面的数据是说,在 Dex 加载方面 和 运行效率方面 是有优势的

Dex 加载速度提升存疑

Dex 加载的速度提升,我并没有试过,我感觉他们可能是测试的动态加载 Dex 的数据,比如插件的加载等等。

因为,apk 在安装的时候也会执行一次 dex2oat,进行 class 的预校验。

后续在 classloader 创建和加载 dex 的时候,先不说以何种方式加载(后续说明),单说 dex 大小本身并没有变化,哪里来的速度提升呢?

所以,猜测是插件的加载数据,因为插件没有 PMS 的安装过程,也不会走安装时的 quicken 模式的 dex2oat,所以会有一些 verify 上的收益?

运行时的效率的话,当然是提升了,但是对于冷启动场景,反而会有一些 负面效果

冷启动的劣化

这是一个反直觉的事情,但是也是我们线上的实验结果(P90 数据,劣化了 400ms),同时 baseline profiles 的 notes 中也提示

Broad rules that compile too much of the application can slow down startup due to increased disk access. You should test the performance of your Baseline Profiles
使用太宽的规则进行编译,可能会由于更多的磁盘访问导致启动变慢

为什么会有更多的磁盘访问呢?因为 dex2oat 以后产生的 oat(其实命名为 base.odex) 文件会大很多。

之前亲测一个 115MB(也可能是70MB,记不清楚了···) 左右的 APK,全量编译以后的 odex 为 260MB+。我们也知道内存操作 200MB 的数据是快到离谱的。但是,如果从磁盘来读呢?无疑,是很慢的,所以拖垮了启动速度。

但是,稍微想想,不知道你会不会有个疑问?

odex 文件,或者说 class 是怎么加载的?启动时一次性载入内存?还是懒加载?

考虑到计算机的一切都是懒加载的,应该是懒加载的。

可是,如果想实现,从一个磁盘文件到内存中懒加载,且指哪读哪,怎么实现呢?

当然是 mmap

所以后面就来看看源码中,classloader 的创建 和 dex 加载的事情,顺便验证这一个观点。

结论

在 PathClassLoader 创建之时,odex 文件已经被 mmap 进内存。但是因为 mmap 自身是 天然的懒加载,只是分配了虚拟内存,并没有真实的 IO,当你访问到具体的内容时,触发缺页中断,进而才有 IO 操作。

所以,在后续的 findClass 的过程,mmap 会多次因为缺页中断进行 IO,直到加载完所有需要加载的数据

所以,这才是类加载是懒加载的真实原因

可能也是冷启动过程,全量编译反而更慢的原因

到这里本篇已经结束,后续只是繁琐的源码寻找过程。

源码探寻

Java 层

PathClassLoader 是一个经常遇到的类加载器,继承自 BaseDexClassLoader,下面是一坨代码,核心其实就是想说:

在 classloader 创建之时,已经开始了默默的加载操作(只是想 mmap),一直到 openDexFileNative,进入到了 native 层。

流程大致是,DexPathList 的创建 -> makeDexElements -> loadDexFile -> openDexFileNative

// libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public BaseDexClassLoader(String dexPath,
          String librarySearchPath, ClassLoader parent, ClassLoader[] sharedLibraryLoaders,
          ClassLoader[] sharedLibraryLoadersAfter,
          boolean isTrusted) {
      super(parent);
      // Setup shared libraries before creating the path list. ART relies on the class loader
      // hierarchy being finalized before loading dex files.
      this.sharedLibraryLoaders = sharedLibraryLoaders == null
              ? null
              : Arrays.copyOf(sharedLibraryLoaders, sharedLibraryLoaders.length);
      this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);

      this.sharedLibraryLoadersAfter = sharedLibraryLoadersAfter == null
              ? null
              : Arrays.copyOf(sharedLibraryLoadersAfter, sharedLibraryLoadersAfter.length);
      // Run background verification after having set 'pathList'.
      this.pathList.maybeRunBackgroundVerification(this);

      reportClassLoaderChain();
  }

// libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
DexPathList(ClassLoader definingContext, String dexPath,
          String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
      
      // 略
      this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                          suppressedExceptions, definingContext, isTrusted);
}

private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
      // 略
       loadDexFile(file, optimizedDirectory, loader, elements);
}

// libcore/dalvik/src/main/java/dalvik/system/DexFile.java
private static Object openDexFile(String sourceName, String outputName, int flags,
        ClassLoader loader, DexPathList.Element[] elements) throws IOException {
    // Use absolute paths to enable the use of relative paths when testing on host.
    return openDexFileNative(new File(sourceName).getAbsolutePath(),
                              (outputName == null)
                                  ? null
                                  : new File(outputName).getAbsolutePath(),
                              flags,
                              loader,
                              elements);
}

native 层

接着上面的 openDexFileNative,会进入 native 层,最终 mmap 的地方我找了好久

最终发现其实只要调用了 oat_file_info.getFile() 其实就已经触发了 mmap

大致流程为:
openDexFileNative -> OpenDexFilesFromOat -> GetOptimizationStatus -> GetFile -> Open -> OpenOatFile -> Load -> ElfFileOpen

此处 getOptimizationsStatus 是我找到的最靠前的方法。mmap 最早发生在何处可能不太准确,但是不影响结论——使用 mmap 来达到懒加载。

native 侧代码如下

// art/runtime/native/dalvik_system_DexFile.cc
static jobject DexFile_openDexFileNative(JNIEnv* env,
                                         jclass,
                                         jstring javaSourceName,
                                         jstring javaOutputName ATTRIBUTE_UNUSED,
                                         jint flags ATTRIBUTE_UNUSED,
                                         jobject class_loader,
                                         jobjectArray dex_elements) {
  ScopedUtfChars sourceName(env, javaSourceName);
  if (sourceName.c_str() == nullptr) {
    return nullptr;
  }

  std::vector<std::string> error_msgs;
  const OatFile* oat_file = nullptr;
  std::vector<std::unique_ptr<const DexFile>> dex_files =
      Runtime::Current()->GetOatFileManager().OpenDexFilesFromOat(sourceName.c_str(),
                                                                  class_loader,
                                                                  dex_elements,
                                                                  /*out*/ &oat_file,
                                                                  /*out*/ &error_msgs);
  return CreateCookieFromOatFileManagerResult(env, dex_files, oat_file, error_msgs);
}

// art/runtime/oat_file_manager.cc
std::vector<std::unique_ptr<const DexFile>> OatFileManager::OpenDexFilesFromOat(
    const char* dex_location,
    jobject class_loader,
    jobjectArray dex_elements,
    const OatFile** out_oat_file,
    std::vector<std::string>* error_msgs) {
  // 略

  // 这里是我发现的可能是最早的 mmap 的地方
  oat_file_assistant->GetOptimizationStatus(
      &odex_location, &compilation_filter, &compilation_reason, &odex_status);

  Runtime::Current()->GetAppInfo()->RegisterOdexStatus(
      dex_location,
      compilation_filter,
      compilation_reason,
      odex_status);
}

// art/runtime/oat_file_assistant.cc
void OatFileAssistant::GetOptimizationStatus(
  std::string* out_odex_location,
  std::string* out_compilation_filter,
  std::string* out_compilation_reason,
  std::string* out_odex_status) {
OatFileInfo& oat_file_info = GetBestInfo();
const OatFile* oat_file = GetBestInfo().GetFile();

if (oat_file == nullptr) {
  *out_odex_location = "error";
  *out_compilation_filter = "run-from-apk";
  *out_compilation_reason = "unknown";
  // This mostly happens when we cannot open the oat file.
  // Note that it's different than kOatCannotOpen.
  // TODO: The design of getting the BestInfo is not ideal,
  // as it's not very clear what's the difference between
  // a nullptr and kOatcannotOpen. The logic should be revised
  // and improved.
  *out_odex_status = "io-error-no-oat";
  return;

  // 略
}

// art/runtime/oat_file.cc
//  Open -> OpenOatFile -> Load -> ElfFileOpen  均在 oat_file 中 
bool ElfOatFile::ElfFileOpen(File* file,
                             bool writable,
                             bool executable,
                             bool low_4gb,
                             /*inout*/MemMap* reservation,
                             /*out*/std::string* error_msg) {
  ScopedTrace trace(__PRETTY_FUNCTION__);
  elf_file_.reset(ElfFile::Open(file,
                                writable,
                                /*program_header_only=*/ true,
                                low_4gb,
                                error_msg));
  if (elf_file_ == nullptr) {
    DCHECK(!error_msg->empty());
    return false;
  }
  bool loaded = elf_file_->Load(file, executable, low_4gb, reservation, error_msg);
  DCHECK(loaded || !error_msg->empty());
  return loaded;
}

// 后续的 Open 方法中会调用 setup,进而调用 MemMap::MapFile 进而调用 mmap