天天看点

Android P(9.0) Debug 版本关闭DEXPREOPT后每次开机都DEXOPT导致开机很久

最近开发Android P(9.0), 关闭WITH_DEXPREOPT(不关闭的话,编译framework或者services生成的jar包不能直接push到机器,影响开发效率,不知到有没有方法解决这个问题)后,发现每次开机都要很久。

我们使用log大法分析一下

第一步,看Log

10:12:30.474  1338  1338 I  SystemServer: UpdatePackagesIfNeeded
...
10:12:40.544  1338  1338 I  PackageManager.DexOptimizer: Running dexopt (dexoptNeeded=1) on: /system/priv-app/FusedLocation/FusedLocation.apk pkg=com.android.location.fused isa=arm64 dexoptFlags=public targetFilter=verify oatDir=null classLoaderContext=PCL[/system/framework/com.android.location.provider.jar]
10:12:40.546   974  1363 V  installed: DexInv: --- BEGIN '/system/priv-app/FusedLocation/FusedLocation.apk' ---
10:12:40.613  1709  1709 I  dex2oat: /system/bin/dex2oat --input-vdex-fd=-1 --output-vdex-fd=19 --compiler-filter=verify --classpath-dir=/system/priv-app/FusedLocation --class-loader-context=PCL[/system/framework/com.android.location.provider.jar] --generate-mini-debug-info --compact-dex-level=none --compilation-reason=boot
10:12:40.741  1709  1709 I  dex2oat: dex2oat took 131.017ms (122.126ms cpu) (threads: 8) arena alloc=0B (0B) java alloc=132KB (135984B) native alloc=2MB (2499928B) free=1654KB (1694376B)
10:12:40.783   974  1363 V  installed: DexInv: --- END '/system/priv-app/FusedLocation/FusedLocation.apk' (success) ---
...
10:12:45.738  1338  1338 D  SystemServerTiming: UpdatePackagesIfNeeded took to complete: 15264ms      

从开机log看出,用了15s,实际情况是应用越多耗时越久

为什么每次都做这个优化,现有log信息看不出来,那就查代码吧

第二步,看代码

1 找到入口

/frameworks/base/services/java/com/android/server/SystemServer.java

if (!mOnlyCore) {
    traceBeginAndSlog("UpdatePackagesIfNeeded");
    try {
        mPackageManagerService.updatePackagesIfNeeded();
    } catch (Throwable e) {
        reportWtf("update packages", e);
    }
    traceEnd();
}      

显然mOnlyCore为false,因为log里打印了UpdatePackagesIfNeeded2 再看PMS

public void updatePackagesIfNeeded() {
    enforceSystemOrRoot("Only the system can request package update");

    // We need to re-extract after an OTA.
    boolean causeUpgrade = isUpgrade();

    // First boot or factory reset.
    // Note: we also handle devices that are upgrading to N right now as if it is their
    //       first boot, as they do not have profile data.
    boolean causeFirstBoot = isFirstBoot() || mIsPreNUpgrade;

    // We need to re-extract after a pruned cache, as AoT-ed files will be out of date.
    boolean causePrunedCache = VMRuntime.didPruneDalvikCache();

    if (!causeUpgrade && !causeFirstBoot && !causePrunedCache) {
        return;
    }
    
    List<PackageParser.Package> pkgs;
    synchronized (mPackages) {
        pkgs = PackageManagerServiceUtils.getPackagesForDexopt(mPackages.values(), this);
    }

    final long startTime = System.nanoTime();
    final int[] stats = performDexOptUpgrade(pkgs, mIsPreNUpgrade /* showDialog     */,
                causeFirstBoot ? REASON_FIRST_BOOT : REASON_BOOT,
                false /* bootComplete */);
    ...
}      

这个函数先收集需要做优化的pkgs,然后将pkgs交给函数performDexOptUpgrade

收集pkg前,有一个判断,

!causeUpgrade && !causeFirstBoot && !causePrunedCache

1

如果不是升级,并且不是第一个开机,并且没有删减cache,就直接返回

显然cache被删减了,为了证明我的推测,添加了log,push service.jar,重启,

果然

1297  1297 I  PackageManager: causeUpgrade=false,causeFirstBoot=false,causePrunedCache=true

1

为了进一步确定,对比了7.0的Android,这三个变量都是false

3 进入ART

VMRuntime.didPruneDalvikCache()是java库的函数,VMRuntime.java对应的native文件是

art/runtime/native/dalvik_system_VMRuntime.cc

1

对应的函数是

static jboolean VMRuntime_didPruneDalvikCache(JNIEnv* env ATTRIBUTE_UNUSED,
                                              jclass klass ATTRIBUTE_UNUSED) {
  return Runtime::Current()->GetPrunedDalvikCache() ? JNI_TRUE : JNI_FALSE;
}      

继续跟踪,art/runtime/runtime.h

bool GetPrunedDalvikCache() const {
    return pruned_dalvik_cache_;
}      

发现只是读这个变量值,那这个值在哪里设置的呢

搜索art目录,发现,只有一个地方设置了这个值,就是删减cache的时候,cache是DalvikCache

art/runtime/gc/space/image_space_fs.h

static void PruneDalvikCache(InstructionSet isa) {
  CHECK_NE(isa, InstructionSet::kNone);
  // Prune the base /data/dalvik-cache.
  // Note: GetDalvikCache may return the empty string if the directory doesn't
  // exist. It is safe to pass "" to DeleteDirectoryContents, so this is okay.
  impl::DeleteDirectoryContents(GetDalvikCache("."), false);
  // Prune /data/dalvik-cache/<isa>.
  impl::DeleteDirectoryContents(GetDalvikCache(GetInstructionSetString(isa)), false);

  // Be defensive. There should be a runtime created here, but this may be called in a test.
  if (Runtime::Current() != nullptr) {
    Runtime::Current()->SetPrunedDalvikCache(true);
  }
}      

这个函数调用的地方有几个,而且都在art/runtime/gc/space/image_space.cc,并且有log,只需要在文件最前面定义

#define LOG_TAG "art"就能打开log.

编译make libart -j8, push, 重启

1006  1006 W  art: Failed execv(/system/bin/patchoat --input-image-location=/system/framework/boot.art --output-image-directory=/data/dalvik-cache/arm --instruction-set=arm --verify) because non-0 exit status Preemptively pruning the dalvik cache.

1

找到了删减cache的代码位置

std::unique_ptr<ImageSpace> ImageSpace::CreateBootImage(const char* image_location,
                                                        const InstructionSet image_isa,
                                                        bool secondary_image,
                                                        std::string* error_msg) {
...
bool verified = VerifyImage(image_location, dalvik_cache.c_str(), image_isa, &local_error_msg);
    // If we prune for space at a secondary image, we may end up in a crash loop with the _exit
    // path.
    bool check_space = CheckSpace(dalvik_cache, &local_error_msg);
    if (!verified || !check_space) {
      // Note: it is important to only prune for space on the primary image, or we will hit the
      //       restart path.
      LOG(WARNING) << local_error_msg << " Preemptively pruning the dalvik cache.";
      PruneDalvikCache(image_isa);
      ...
}      

再次添加Log, 发现verified失败了。而7.0没有VerifyImage.

VerfifyImage是执行patchoat

VerifyImage: /system/bin/patchoat --input-image-location=/system/framework/boot.art --output-image-directory=/data/dalvik-cache/arm --instruction-set=arm --verify

1

打开patchoat的log,编译make patchoat -j8

最后发现是打不开文件boot.art.rel

Failed to open image relocation file /system/framework/arm64/boot.art.rel

1

而关闭DEXPREOPT是没有这个文件的。

最后解决方法:

USER版本打开PREOPT,即编译时优化

在device下对应机型的.mk文件中加入以下代码

ifeq ($(TARGET_BUILD_VARIANT), user)
  WITH_DEXPREOPT := true
  DONT_DEXPREOPT_PREBUILTS := false
else
  WITH_DEXPREOPT := false
  DONT_DEXPREOPT_PREBUILTS := true
endif      
在art/runtime/gc/space/image_space.cc加入以下代码
#ifdef ART_TARGET_ANDROID
#include "cutils/properties.h"
#endif      
bool verified = VerifyImage(image_location, dalvik_cache.c_str(), image_isa, &local_error_msg);

// 以下为加入的代码,如果是usedebug,认为verify通过
#ifdef ART_TARGET_ANDROID
    char build_type_value[PROPERTY_VALUE_MAX];
    if (property_get("ro.build.type", build_type_value, "") > 0) {
        std::string build_type(build_type_value);
        if (build_type.compare("userdebug") == 0) {
            verified = true;
            LOG(INFO) << "dont't verify secondary images if is userdebug";
        }
    }
#endif