dex2oat的原理及慢的原因

时间:2021-04-10 17:01:12

Overview

众所周知,Android的程序是可以用java来写的,如果可以运行java语言,那么就一定需要一个虚拟机。同时,google为了避开商业纠纷,采用了自己在dalvik虚拟机。

虚拟机的功能是解释执行代码文件。就好比

dex2oat的原理及慢的原因

java语言在android的运行流程如下所示:

 

dex2oat的原理及慢的原因

 

dex2oat的原理

虚拟机的发生展经历了从初期的dalvik,到中期的dalvik,以及后期的ART。但是市面上的APK文件早已已经全球流行。为了能够让这些APK不加改动的在所有虚拟机上面运行,google采用了类似适配器模式。即在让虚拟运行之前多一道工序。就是dexopt。dexopt在dalvik虚拟机上的目标是将dex文件优化成odex文件,可以让虚拟机更加优化的执行。

 

在ART虚拟机中,dexopt将dex文件优化成二进制格式的文件,从可以让ART虚拟机执行。dexopt会调用dex2oat进行。而dex2oat的任务是将原来的dex文件做预先的翻译,从而可以加快APP运行的时间。但是由于某些APP比较复杂,所以优化的时间就比较长。优化是以dex文件中的method为单位。dex2oat在优化时,会根据需要优化一定量的method。也就是说并不是优化的method都会被翻译成oat模式。根据优化的method的量的多少,可以分为如下的几种模式:

 

dex2oat的原理及慢的原因


N版本中dex2oat的原理及模式

N版本当中强化了JIT模式。JIT模式是Just in time 的简称。意思是在运行的时候,根据method有使用频率来决定是否要对某一个方法进行优化。虚拟机会统计每一个方法被执行的次数。如果某一个方法执行的次数比较多,达到一定的阈值,就会将升级为hot method,并将其记录在一个profile当中。在系统空闲并且在充电的时候,只将这些方法进行优化。在运行的时候,也会对这些方法进行优化,以方便在运行的时候使用。

dex2oat的原理及慢的原因

JIT的动作原理如下图所示:

dex2oat的原理及慢的原因


如何判断dex2oat采用的相关参数

dex2oat的参数会在代码中通过如下的函数进行打印:

  • L版本

L版本会无条件打印

 timings.StartTiming("dex2oat Setup");
1436 LOG(INFO) << CommandLine();

  • M和N版本

         在M和N当中会有条件的打印不同的内容。例如在会在debug版本中打印非常详细的dex2oat参数,而在非debug版本中打印部分内容。

  if (kIsDebugBuild || dex2oat.IsImage() || dex2oat.IsHost() || !kIsTargetBuild) {
2144 LOG(INFO) << CommandLine();
2145 } else {
2146 LOG(INFO) << StrippedCommandLine();
2147 }
  • 如可查看dex2oat 的Log

   

 01-01 04:28:03.471265  5654  5654 I dex2oat : Starting dex2oat.

01-01 04:28:37.548439  5654  5654 I dex2oat : dex2oat took 34.077s (threads: 4) arena alloc=23MB java alloc=14MB native alloc=88MB free=5MB 01-01

其中:dex2oat tokk 34.077s表示 dex2oat的耗时情况,(threads:4)表示,一共用了4个线程做这件事情。

 

01-01 08:34:27.063 I/dex2oat ( 7714): /system/bin/dex2oat --runtime-arg -classpath 
--runtime-arg --instruction-set=arm --instruction-set-features=default --runtime-arg -Xrelocate --boot-image=/system/framework/boot.art
--dex-file=/data/data/com.tencent.mm/app_dex/secondary-2.dex.jar --oat-fd=62 --oat-location=/data/data/com.tencent.mm/app_cache/secondary-2.dex.dex
--runtime-arg -Xms64m --runtime-arg -Xmx512m --compiler-filter=speed

会版本合适,可以通过会打印出较多的dex2oat参数。

上面的Log中,可以得出很多信息来:
1.被优化的dex文件信息,可以通过 --dex-file获取,形如上面我们就知道优化的是微信里面的内容,因为--dex-file参数的dex文件位于
com.tencent.mm目录下面的。
2.优化之后oat存储的位置,可以看到--oat-location其是保存在包名下面的app_cache目录下面的。
3.是对插件优化还是对APK本身优化。在L的时候所有的PMS优化的内容都会放在data/dalvik-cache目录下面,所以如果oat放的位置是data/
dalvik-cache目录下面,那么就是优化主APK,否则就是优化插件的。
而在M和N当中,优化的结果会放在自己的包下面,但是不会是这种类似app_cache这种,根据这个可以判断是对插件的优化。
4.优化采用的模式
根据--compiler-filter可以判断采用的是拿种模式。

如果我们关注的信息没有打印出来,可以修改上面打印这些Log的条件。

引起的问题及原因

一个字:慢!

两个字:很慢!

目前遇到的大多数问题都是和慢相关的。慢的主要原因如下:

  • 工作量

dex2oat发生的时候,会将原文件中的dex文件抽出来,逐个指令的判断,然后进行翻译,并生成大量的中间内容。并占用所有有CPU(目前的策略是有多少个核,就启动多少个线程)

  • CPU

       如果CPU越多,则启动的线程数越多,这样的话,就会比较快做晚。

  • EMMC

     如果dex文件比较大,会产生大量的中间内容,这些在memory当中是保存不下的,所以采用了swap机制,会将一些内容交换到EMMC当中,而且有大量的读操作,同时会将结果保存至emmc当中,所以emmc的性能也是非常关键的。

  •  memory

     如上面所述,memory越少,越容易发生交换,所以就会引起IO上的瓶颈。

  • patch

     google之前有一个优化,就是在dex2oat做完的时候,不进行析构,因为析构会将交换出去的内容再读进来,从而导致耗时较长。

   M版本的patch为:  

     ALPS02736915

   N版本当中,暂时没有,如果有遇到慢的问题,可以向我司申请。

Solution 1:在PMS中进行白名单处理

遇到慢的问题的时候,先从上面几个方面找原因,如果无法解决,那么就只好调整APP的安装策略。例如原来为speed模式,就改为interpreter-only模式。

 

参考代码如下 :

 

  • 在PMS中修改
         在L和M版本不适用。
         N版本中经过层层调用,会调用到如下的函数,因此需要修改如下的文件:
private int performDexOptLI(PackageParser.Package pkg, String[] sharedLibraries,
153 String[] targetInstructionSets, boolean checkProfiles, String targetCompilerFilter) {



在这函数中,可以判断传递下来的pkg是否是我们需要的package,如果是的话,将targetCompilerFilter设置为speed-profile。

speed-profile会在安装的时候采用interperter-only,然后,运行一段时间之后,会将那些常用的方法优化成为speed模式。也就是说是有选择性的优化。

if (isProfileGuidedFilter && isUsedByOtherApps(pkg)) {
168 checkProfiles = false;
169
170 targetCompilerFilter = getNonProfileGuidedCompilerFilter(targetCompilerFilter);
171 if (DexFile.isProfileGuidedCompilerFilter(targetCompilerFilter)) {
172 throw new IllegalStateException(targetCompilerFilter);
173 }
174 isProfileGuidedFilter = false;
175 }
176
177 // If we're asked to take profile updates into account, check now.
178 boolean newProfile = false;
//mtk add begin
if(pkg.contains("com.tencent.mm")){
targetCompilerFilter="interpret-only";

}
//mtk add end

Solution 2:在Installd中进行白名单处理

  • 在commands.cpp中修改:

适用于:L、M、N

     如前面讨论PMS实质上是通过commands.cpp中的dex2oat函数进行的,因此 ,可以在commands.cpp中添加白名单机制。

实现的思路为:

通过 property来增加白名单
adb shell setprop ro.mtk.dex2oat_white_list Facebook: 注意Facebook是apk的名字,后面不用带后缀,但是大小写一定要匹配。

如果需要增加到开机启动当中,可以如下修改
在device.mk中加入.PRODUCT_PROPERTY_OVERRIDES += ro.mtk.dex2oat_white_list=facebook:Facebook:youtube:skype:twitter:

 多个APK名字之间用冒号隔开。

[添加source code]

在 \frameworks\native\cmds\installd\commands.c添加红色部分:

static int shouldUseInterpretonly(const char* input_file_name);

static void run_dex2oat(int zip_fd, int oat_fd, const char* input_file_name,
const char* output_file_name, int swap_fd, const char *pkgname, const char *instruction_set,
bool vm_safe_mode)
{

......

//适当的时候,进行调用

if(shouldUseInterpretonly(input_file_name))
{
strcpy(dex2oat_compiler_filter_arg, "--compiler-filter=interpret-only");
have_dex2oat_compiler_filter_flag = true;   //关键是设置这个参数的值
ALOGW("%s is in whitelist from property so set interpret-only",input_file_name);
}

ALOGV("Running %s in=%s out=%s\n", DEX2OAT_BIN, input_file_name, output_file_name);

.....

}

 

/**

*函数功能,根据input的file name判断,当前来优化的APK名字判断是否在白名单当中。

*白名单来自于property

*/

static int shouldUseInterpretonly(const char* input_file_name)
{
char prop_buf[PROPERTY_VALUE_MAX];
memset(prop_buf,0,PROPERTY_VALUE_MAX);
bool have_whitelist = property_get("ro.mtk.dex2oat_white_list", prop_buf, NULL) > 0;
if(!have_whitelist)
return false;
char *str = prop_buf;
char appname[128],*ptrname = appname;
memset(appname,0,128);

while(*str)
{
if(*str != ':')
{
*ptrname = *str;
ptrname ++;
str++;
}
else{

str++;

if(*appname != 0)
{
if(strstr(input_file_name,appname)) //found
{
return 1; 
}
else
{
memset(appname,0,sizeof(appname));
ptrname = appname;
}

}
}

}

return 0;

}

 

Solution 3:在dex2oat.cc中进行白名单处理

  • 在dex2oat.cc中修改

     由于插件不会走PMS和installd这一路,所以,只能想办法在dex2oat中进行拦截。在这里面需要解决的问题是:如何判断来优化的就是我们关注的插件。

    •    通过判断dex2oat进程的父pid来判断是插件

如果是插件进行的dex2oat,有一个特点就是这个进程的父进程不是installd。所以可以根据一点进行判断。但是有一个特点,会将所有的插件全部优化成interpreter-only。

以N版本为例,建议的代码如下:

/*
*调整编译的模式,规则:如果当前进程的父进程是installd,不做修改,否则修改为interpreter-only。
*因为插件类的,都是进程本来fork的dex2oat.
*only for device end
*
*/
void adjustCompilerOptions(const CompilerOptions* compiler_options){

int ppid = getppid(); //获取当前进程的parent pid
char cmdline_path[256];
char contents[256];
memset(cmdline_path,0,sizeof(cmdline_path));
memset(contents,0,sizeof(contents));
snprintf(cmdline_path,"/proc/%d/cmdline");
FILE *fd = fopen(cmdline_path,"r"); // 打开proc/pid/cmdline文件,以读取对应ppid进程的名字

if(fd == NULL){
LOG(ERROR) < return;
}

fgets(contents,sizeof(contents),fd); //读取文件中的内容
fclose(fd);
if(strstr(contents,"installd")){ //判断是否是installd
return;
}

Log(INFO)<<"parent is not installd,may be is a plugin adjust it to interpret-only";
compiler_options->SetCompilerFilter(kInterpretOnly); //如果是的话,强行调整编译模式为interpreteronly模式


}

在ParseArgs函数中对dex2oat的模式进行调整:
ProcessOptions(parser_options.get());

// Insert some compiler things.
InsertCompileOptions(argc, argv);

if(!IsHost()) //注意一定要判断非host端,否则会有问题
adjustCompilerOptions(compiler_options_.get());

}

    •      根据dex文件名字或者oatlocation名字来判断

 

        以L和M版本为例:

  L版本:
/art/dex2oat/dex2oat.cc

方法1:

// mtk add begin

if ((!oat_filename_.empty() && (oat_filename_.find("facebook") != std::string::npos))||(!zip_location_.empty() && (zip_location_.find("facebook") !=std::string::npos))||((!oat_location_.empty()) && oat_location_.find("facebook") != std::string::npos)){  // mtk modify 2016-06-06

                                    compiler_filter_string = "interpret-only";

                                    LOG(INFO) <<" This apk is in whitelist from property so set interpret-only ";

            }

// mtk add end

if (compiler_filter_string = nullptr){

      compiler_filter_string = "speed";

}

 注意:CTS测项用的包不能使用interpret-only模式,因为一些CTS测项是性能相关的,如果改为interpret模式,会导致CTS不过。之前有客户为了追求更快的安装速度,将所有的包都改为interpret-only的,结果CTS就不过了,针对这种情况,可以把CTS的包再放到speed模式的白名单当中。cts的关键字为test.

 

方法2(若上述方法无效):

1420 if (num_methods <= compiler_options_->GetNumDexMethodsThreshold()) {
1421 compiler_options_->SetCompilerFilter(CompilerOptions::kSpeed);
1422 VLOG(compiler) << "Below method threshold, compiling anyways";
1423 }
1424 }
1425
1426//mtk_add_begin
1427 static constexpr size_t kMinCompileDexSize = 4;
1428 if (!image_ && dex_files_.size() > kMinCompileDexSize) {
1429 compiler_options_->SetCompilerFilter(CompilerOptions::kInterpretOnly);
1430 LOG(INFO) << "Enable Whitelist Rules. Current Dex File Sizes:" << dex_files_.size();
1431 }
1432//mtk_add_end
1433
1434 return true;
1435 }
1436
1437 // Create and invoke the compiler driver. This will compile all the dex files.
1438 void Compile() {
1439 TimingLogger::ScopedTiming t("dex2oat Compile", timings_);
1440 compiler_phases_timings_.reset(new CumulativeLogger("compilation times"))

 

或者如下的代码:

 [SOLUTION L版本]

    在device.mk中加入PRODUCT_PROPERTY_OVERRIDES += ro.mtk.dex2oat_white_list=com.tencent.mm: (注意 包名后又冒号“:”
 
[添加source code](抱歉不能排版)
 /art/dex2oat/dex2oat.cc添加红色部分:
 
 #ifdef HAVE_ANDROID_OS
 extern "C"{
                  static int shouldUseInterpretonly(const char* filename){
                       char prop_buf[92];
                       memset(prop_buf,0,92);
                       bool have_whitelist = property_get("ro.mtk.dex2oat_white_list", prop_buf, NULL) > 0;
                       if(!have_whitelist)
                       return false;
                       char *str = prop_buf;
                       char appname[128],*ptrname = appname;
                       memset(appname,0,128);

                       while(*str)
{
         if(*str != ':'){
                                     *ptrname = *str;
                                      ptrname ++;
                                      str++;
              } else{
                                       str++;
                                      if(*appname != 0){
                                                   if(strstr(filename,appname)) //found
                                                     {
                                                              return 1;
                                                      } else{
                                                              memset(appname,0,sizeof(appname));
                                                              ptrname = appname;
                                                      }
                                         }
                         }
}

return 0;
}
}
#endif

static int dex2oat(int argc, char** argv) {

          std::string dex_filename; //mtk_add
           ......
         if (option.starts_with("--dex-file=")) {
                    dex_filenames.push_back(option.substr(strlen("--dex-file=")).data());
                    dex_filename = option.substr(strlen("--dex-file=")).data(); //mtk_add
         } else if......

         
         if (compiler_filter_string == nullptr) {
         if (instruction_set == kMips64) {
         // TODO: fix compiler for Mips64.
                     compiler_filter_string = "interpret-only";
         } else if (image) {
                     compiler_filter_string = "speed";
         } else {
                    #if ART_SMALL_MODE
                    compiler_filter_string = "interpret-only";
        #else
          #ifdef HAVE_ANDROID_OS
                 if(shouldUseInterpretonly(zip_location.c_str())){
                              compiler_filter_string = "interpret-only";
                              LOG(INFO) <<" This apk is in whitelist from property so set interpret-only";
                  }else if(shouldUseInterpretonly(dex_filename.c_str())){
                              compiler_filter_string = "interpret-only";
                              LOG(INFO) <<" This jar is in whitelist from property so set interpret-only";
                   }else{
          #endif
                               compiler_filter_string = "speed";
           #ifdef HAVE_ANDROID_OS
                    }
        #endif
#endif
    }
}

CHECK(compiler_filter_string != nullptr);
.......

}