从Tinker加载dex补丁看动态加载插件过程

本篇文章基于Android Q 和 Tinker 1.9.14.7。

经过前面的分析,已经初步了解了Tinker的整个执行流程,对整个脉络有了清晰的认识。那么本篇文章就来深挖一个点,从加载dex补丁看Tinker动态加载插件过程。

首先来回顾Tinker加载dex补丁过程

整个dex补丁加载过程分两部分:

  • 如果系统ota升级后,因为odex失效了,所以Tinker自己会触发一次dex加载,而这个加载过程在Q之前是会走dex2oat编译的,Q开始如果odex失效,当前loadDex过程不进行dex2oat编译,转而走解释模式执行。另外,这里区分了版本,对25及以上的版本使用PathClassLoader来加载,而低版本直接使用DexFile.loadDex。

  • dex补丁包加载实际上是通过hook的方式将修复后的dex插入到的DexPathList的Elements[]中最前面去,从而替代之前的基准包,替代原理是双亲委派和类缓存保证了相同类不能被重复加载,前面加载过了后面的dex就失效了,从而达到替换的目的。

那么本篇文章主要分析如下三点问题:

  • PathClassLoader与DexFile.loadDex加载类的区别
  • dex加载过程
  • Apk加载过程与Tinker动态加载dex补丁包整体顺序流程

一、PathClassLoader与DexFile.loadDex加载类的区别

PathClassLoader初始化流程

流程主要包含两个部分:

  • PathClassLoader经过一系列初始化,最终由DexPathList触发DexFile.loadDex来加载dex。

  • 通过report将插件路径上报到DexManager统一管理。

乍一看,这两种方式本质上只有有个上报的差别,那么这个上报到底有什么意义?另外,在之前老版本Tinker中,这里统一是由DexFile.loadDex来加载的,之后为什么新增PathClassLoader加载方式?所以就很有必要研究下上报是干了什么。

在ActivityThread handleBindApplication会通过LoadedApk进行Apk加载操作,在这里会设置BaseDexClassLoader的reporter:

if (SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false)) {
    BaseDexClassLoader.setReporter(DexLoadReporter.getInstance());
}

看看这个系统属性:

cepheus:/ $ getprop | grep "dalvik.vm.usejitprofiles"

[dalvik.vm.usejitprofiles]: [true]   开关默认是打开的

下面来研究下整个上报过程:

上报简单流程

最终dexPath归DexManager统一管理。系统有个jobService:BackgroundDexService它在SystemServer执行startOtherServices时触发schedule,在idle场景下触发插件和主apk包的dex2oat编译,简单流程如下图所示:


上报插件被编译流程

也就是说,如果上报了,当前插件会被DexManager统一管理,在idle状态下系统会通过一个jobService触发对所有已安装app的主apk以及插件的dex2oat的编译,通常方式为speed-profile。在Q之前,因为DexFile.loadDex本身会走编译流程,所以也不需要上报之后统一编译,而Q之后DexFile.loadDex不走编译流程了,一旦判断没有有效odex,则走解释执行。所以依赖上报让系统帮忙来对插件做编译。

注:使用dex2oat进行AOT编译的compile filter:

  • verify:只运行 DEX 代码验证。
  • quicken:运行 DEX 代码验证,并优化一些 DEX 指令,以获得更好的解释器性能。
  • speed-profile:运行 DEX 代码验证,并对配置文件中列出的方法进行 AOT 编译。
  • speed:运行 DEX 代码验证,并对所有方法进行 AOT 编译。

另外 附上系统触发dex2oat编译时机:

路径 描述 编译方式 编译内容
Install 应用安装触发的编译 speed-profile 主apk
ota 系统升级触发的编译 verify 主apk
loadDex 动态加载插件触发的编译 quicken 插件
postboot 开机1分钟后,jobService触发的编译 verify 主apk
idle&charge 同时满足充电、idle状态且24小时内只触发一次 speed-profile 主apk 和 插件

从这表中也可以明显看出,Q上取消了load dexFile的插件编译,那么现在剩下的唯一的原生的编译场景为idle&charge了。

二、dex加载过程

那么,我们接下来详细分析下DexFile.loadDex的整体逻辑。

之前先来了解下编译相关文件:

  • .dex 编译的源文件,符合android虚拟机规范的字节码文件,APK中包含一个或多个classes.dex。

  • .vdex 存储预先验证的dex文件,在有效期内能减少不必要的dex文件验证。

  • .odex 是dex2oat对dex编译后的优化产物,节省从apk中加载dex过程和编译优化过程,执行速度比解释执行快,但是该文件有有效期,失效需要重新编译

  • .art 对热点函数进行对应类的预加载,编译过程会触发预加载,预加载后的类可以直接map到内存中使用,不需要从dex中加载。

简单说就是:

  • .vdex 优化dex文件的合法性验证。
  • .odex 优化dex文件加载。
  • .art 优化类加载。
loadDex流程

这里DexFile就是dex文件的一个封装对象,这里很多细节不一一铺开,直接到核心方法OpenDexFilesFromOat,Q之前这个方法会通过MakeUpToDate来做编译,而Q上去掉了。

这里简单梳理下Q上OpenDexFilesFromOat到底做了些什么:

std::vector> OatFileManager::OpenDexFilesFromOat(
    const char* dex_location,
    jobject class_loader,
    jobjectArray dex_elements,
    const OatFile** out_oat_file,
    std::vector* error_msgs) {
  ScopedTrace trace(__FUNCTION__);
  CHECK(dex_location != nullptr);
  CHECK(error_msgs != nullptr);
  // Verify we aren't holding the mutator lock, which could starve GC if we
  // have to generate or relocate an oat file.
  Thread* const self = Thread::Current();
  Locks::mutator_lock_->AssertNotHeld(self);
  Runtime* const runtime = Runtime::Current();
  std::unique_ptr context;
  // If the class_loader is null there's not much we can do. This happens if a dex files is loaded
  // directly with DexFile APIs instead of using class loaders.
  if (class_loader == nullptr) {
    LOG(WARNING) << "Opening an oat file without a class loader. "
                << "Are you using the deprecated DexFile APIs?";
    context = nullptr;
  } else {
    context = ClassLoaderContext::CreateContextForClassLoader(class_loader, dex_elements);
  }
  //初始化OatFileAssistant
  OatFileAssistant oat_file_assistant(dex_location,
                                      kRuntimeISA,
                                      !runtime->IsAotCompiler(),
                                      only_use_system_oat_files_);
  // Get the oat file on disk.
  //获取oat file
  std::unique_ptr oat_file(oat_file_assistant.GetBestOatFile().release());
  VLOG(oat) << "OatFileAssistant(" << dex_location << ").GetBestOatFile()="
           << reinterpret_cast(oat_file.get())
            << " (executable=" << (oat_file != nullptr ? oat_file->IsExecutable() : false) << ")";
  const OatFile* source_oat_file = nullptr;
  CheckCollisionResult check_collision_result = CheckCollisionResult::kPerformedHasCollisions;
  std::string error_msg;
  if ((class_loader != nullptr || dex_elements != nullptr) && oat_file != nullptr) {
    // Prevent oat files from being loaded if no class_loader or dex_elements are provided.
   // This can happen when the deprecated DexFile.(String) is called directly, and it
   // could load oat files without checking the classpath, which would be incorrect.
   // Take the file only if it has no collisions, or we must take it because of preopting.
   //odex冲突检测
   //是否提供class_loader或dex_elements,同时检测odex文件路径等。
   check_collision_result = CheckCollision(oat_file.get(), context.get(), /*out*/ &error_msg);
    //odex冲突检测通过
   bool accept_oat_file = AcceptOatFile(check_collision_result);
    if (!accept_oat_file) {
       //如果没通过,那么原始dex文件存在也给通过
     // Failed the collision check. Print warning.
     if (runtime->IsDexFileFallbackEnabled()) {
        if (!oat_file_assistant.HasOriginalDexFiles()) {
          // We need to fallback but don't have original dex files. We have to
         // fallback to opening the existing oat file. This is potentially
         // unsafe so we warn about it.
         accept_oat_file = true;
          LOG(WARNING) << "Dex location " << dex_location << " does not seem to include dex file. "
                      << "Allow oat file use. This is potentially dangerous.";
        } else {
          // We have to fallback and found original dex files - extract them from an APK.
         // Also warn about this operation because it's potentially wasteful.
         LOG(WARNING) << "Found duplicate classes, falling back to extracting from APK : "
                      << dex_location;
          LOG(WARNING) << "NOTE: This wastes RAM and hurts startup performance.";
        }
      } else {
        // TODO: We should remove this. The fact that we're here implies -Xno-dex-file-fallback
       // was set, which means that we should never fallback. If we don't have original dex
       // files, we should just fail resolution as the flag intended.
       if (!oat_file_assistant.HasOriginalDexFiles()) {
          accept_oat_file = true;
        }
        LOG(WARNING) << "Found duplicate classes, dex-file-fallback disabled, will be failing to "
                       " load classes for " << dex_location;
      }
      LOG(WARNING) << error_msg;
    }
    if (accept_oat_file) {
      VLOG(class_linker) << "Registering " << oat_file->GetLocation();
      //source_oat_file 赋值为oat_file , 并且oat_files_中添加 oat_file
     source_oat_file = RegisterOatFile(std::move(oat_file));
      *out_oat_file = source_oat_file;
    }
  }
  std::vector> dex_files;
  // Load the dex files from the oat file.
  if (source_oat_file != nullptr) {
    bool added_image_space = false;
    if (source_oat_file->IsExecutable()) {
      ScopedTrace app_image_timing("AppImage:Loading");
      // We need to throw away the image space if we are debuggable but the oat-file source of the
     // image is not otherwise we might get classes with inlined methods or other such things.
     std::unique_ptr image_space;
      if (ShouldLoadAppImage(check_collision_result,
                             source_oat_file,
                             context.get(),
                             &error_msg)) {
        //加载.art文件
       image_space = oat_file_assistant.OpenImageSpace(source_oat_file);
      }
      if (image_space != nullptr) {
        ScopedObjectAccess soa(self);
        StackHandleScope<1> hs(self);
        Handle h_loader(
            hs.NewHandle(soa.Decode(class_loader)));
        // Can not load app image without class loader.
       if (h_loader != nullptr) {
          std::string temp_error_msg;
          // Add image space has a race condition since other threads could be reading from the
         // spaces array.
         {
            ScopedThreadSuspension sts(self, kSuspended);
            gc::ScopedGCCriticalSection gcs(self,
                                            gc::kGcCauseAddRemoveAppImageSpace,
                                            gc::kCollectorTypeAddRemoveAppImageSpace);
            ScopedSuspendAll ssa("Add image space");
            runtime->GetHeap()->AddSpace(image_space.get());
          }
          {
            ScopedTrace trace2(StringPrintf("Adding image space for location %s", dex_location));
            //去.art里找dexfile 对应的热点函数类,map到内存,并加入到class table
           added_image_space = runtime->GetClassLinker()->AddImageSpace(image_space.get(),
                                                                         h_loader,
                                                                         dex_elements,
                                                                         dex_location,
                                                                         /*out*/&dex_files,
                                                                         /*out*/&temp_error_msg);
          }
          if (added_image_space) {
            // Successfully added image space to heap, release the map so that it does not get
           // freed.
           image_space.release();  // NOLINT b/117926937
           // Register for tracking.
           for (const auto& dex_file : dex_files) {
              dex::tracking::RegisterDexFile(dex_file.get());
            }
          } else {
            LOG(INFO) << "Failed to add image file " << temp_error_msg;
            dex_files.clear();
            {
              ScopedThreadSuspension sts(self, kSuspended);
              gc::ScopedGCCriticalSection gcs(self,
                                              gc::kGcCauseAddRemoveAppImageSpace,
                                              gc::kCollectorTypeAddRemoveAppImageSpace);
              ScopedSuspendAll ssa("Remove image space");
              runtime->GetHeap()->RemoveSpace(image_space.get());
            }
            // Non-fatal, don't update error_msg.
         }
        }
      }
    }
    if (!added_image_space) {
      DCHECK(dex_files.empty());
      //尝试从odex中加载dex
     dex_files = oat_file_assistant.LoadDexFiles(*source_oat_file, dex_location);
      // Register for tracking.
     for (const auto& dex_file : dex_files) {
        dex::tracking::RegisterDexFile(dex_file.get());
      }
    }
    if (dex_files.empty()) {
      error_msgs->push_back("Failed to open dex files from " + source_oat_file->GetLocation());
    } else {
      // Opened dex files from an oat file, madvise them to their loaded state.
      for (const std::unique_ptr& dex_file : dex_files) {
         OatDexFile::MadviseDexFile(*dex_file, MadviseState::kMadviseStateAtLoad);
       }
    }
  }
  // Fall back to running out of the original dex file if we couldn't load any
  // dex_files from the oat file.
  if (dex_files.empty()) {
    //前面都没加载dexfile,那么直接尝试加载原始dex文件。
   if (oat_file_assistant.HasOriginalDexFiles()) {
      if (Runtime::Current()->IsDexFileFallbackEnabled()) {
        static constexpr bool kVerifyChecksum = true;
        const ArtDexFileLoader dex_file_loader;
        if (!dex_file_loader.Open(dex_location,
                                  dex_location,
                                  Runtime::Current()->IsVerificationEnabled(),
                                  kVerifyChecksum,
                                  /*out*/ &error_msg,
                                  &dex_files)) {
          LOG(WARNING) << error_msg;
          error_msgs->push_back("Failed to open dex files from " + std::string(dex_location)
                                + " because: " + error_msg);
        }
      } else {
        error_msgs->push_back("Fallback mode disabled, skipping dex files.");
      }
    } else {
      error_msgs->push_back("No original dex files found for dex location "
         + std::string(dex_location));
    }
  }
  if (Runtime::Current()->GetJit() != nullptr) {
    ScopedObjectAccess soa(self);
    Runtime::Current()->GetJit()->RegisterDexFiles(
        dex_files, soa.Decode(class_loader));
  }
  return dex_files;
}

这里代码挺长,但是总结起来就加载dex文件到内存,并且初始化好DexFile返回。而加载dex文件的逻辑是:

.art > odex > 从原始dex文件

.art在编译期做了热点函数对应类的预加载,保存了内存image,如果它有则通过classlinker.AddImageSpace直接将image 加载到内存,省略类加载过程,直接加入class table。找不到退而求其次,执行oat_file_assistant.LoadDexFiles,从odex加载dex,因为odex中保存了一个优化后的dex,都不行最后尝试从原始dex文件中加载,如果此时vdex还有效,则能绕过dex验证过程,否则需要重新验证。

三、Apk加载过程与Tinker动态加载dex补丁包整体顺序流程

dex补丁包加载是在TinkerApplication的onBaseContextAttached中通过TinkerLoader.tryLoad触发的,而TinkerApplication是继承Application的。

那么在应用启动流程中,application初始化流程是怎样的:

应用冷启动ActivityThread初始化以及attach Application过程

这里借用我之前文章一张图,虽然是android 4.4的,但是基本流程没有什么变化。

handleBindApplication : 通过getPackageInfo初始化LoadApk,初始化Instrumentation,通过makeApplication加载主apk, 然后初始化Application类,最后通过Instrumentation执行Application的onCreate回调。

也就是说,当tinker热修复合成patch包,应用程序进程被kill重启之后,走冷启动流程(冷启动详细流程可以参考之前文章:深入剖析应用启动流程),先通过LoadedApk加载之前出现bug的基准apk,然后加载application并初始化,初始化过程走application的onBaseContextAttached生命周期,Tinker通过TinkerLoader.tryLoad来走补丁包加载,加载原理便是hook DexPathList 将修复的合成dex添加到Elements[]头部。

写在最后的注意点:

Android Q之前,DexFile.loadDex会先判断是否需要走编译逻辑,如果要走,则需要同步等待编译完才能执行后续流程,因此如果合成后的patch包tinker_classN.apk太大的话,dex补丁加载过程会出现明显卡顿,这个问题系统层不太好去规避。

举个比较典型的场景,比如系统ota升级之后,首次启动app加载热修复包,必然会走编译,因为ota升级之后odex失效需要重新编译。那么系统层要想优化主要有两个思路:1)预编译;2)不同步等待编译,先走解释模式;

1)预编译:在ota升级过程中或者升级完重启手机过程中找个时机去将插件编译一下,这有个问题就是:tinker_classN.apk存放的目录是在data/data/packageName 应用私有目录下,这个目录在锁屏解锁前,内的文件都是按FBE加密的,系统没法获取有效解密文件。但是如果锁屏解锁之后再做预加载显然有点晚了。

2)不同步等待编译,先走解释模式: 这种便是google 在Android Q上的优化,但是需要配合dexpath上报,来让系统在idle时进行插件编译,这也是原生唯一能编译插件的地方,但是这种场景触发太不及时了,因此可以增加一些场景来及时触发插件编译,保证下次应用使用更快。

你可能感兴趣的:(从Tinker加载dex补丁看动态加载插件过程)