关于android:Android性能优化之Android-10-dex2oat实践

10次阅读

共计 14482 个字符,预计需要花费 37 分钟才能阅读完成。

作者:字节跳动终端技术——郭陆地

背景

对于 Android App 的性能优化来说,形式办法以及工具都有很多,而 dex2oat 作为其中的一员,却可能不被公众所熟知。它是 Android 官网利用于运行时,针对 dex 进行 编译优化 的程序,通过对 dex 进行一系列的指令优化、编译机器码等操作,晋升 dex 加载速度代码运行速度,从而晋升装置速度、启动速度、以及利用应用过程中的晦涩度,最终晋升用户日常的应用体验。

它的适用范围也比拟广,能够用于 Primary ApkSecondary Apk 惯例场景 插件场景 。(Primary Apk 是指的 惯例场景 下的 主包 base.apk)或者 插件场景 下的 宿主包 Secondary Apk 是指的 惯例场景 下的 自行加载的包 (.apk) 或者 插件场景 下的 插件包(.apk))。

而随着 Android 零碎版本的更迭,发现本来能够在 利用过程 上触发 dex2oat 编译的形式,却在 targetSdkVersion>=29Android 10+的零碎上,不再容许应用。其起因是零碎在 targetSdkVersion=29 的时候,对此做了限度,不容许 利用过程 上触发 dex2oat 编译(Android 运行时 (ART) 不再从利用过程调用 dex2oat。这项变更意味着 ART 将仅承受系统生成的 OAT 文件)(OATdex2oat 后的产物)。

那以后是否会受到这个限度的影响呢?

2020 年的时候 Android 11 零碎正式公布,各大利用市场就开始限度 ApptargetSdkVersion>=29,而 Android 11 零碎距今曾经公布一年之久,也就意味着,现如今 ApptargetSdkVersion>=29是不可避免的。而且随着新 Android 设施的一直迭代,越来越多的用户,应用上了携带新零碎的新机器,使得 Android 10+ 零碎的占有量逐渐减少,目前为止 Android 10+ 零碎的占有量约占整体的 30%~40% 左右,也就是说这部分机器将会受到这个限度的影响。

那这个限度有什么影响呢?

这个限度的要害是,不容许 利用过程 上触发 dex2oat 编译,换句话说就是并不影响零碎本身去触发 dex2oat 编译,那么限度的影响也就是,影响那些须要通过 利用过程 去触发 dex2oat 编译的场景。

对于 Primary ApkSecondary Apk,它们在 惯例场景 插件场景 下,零碎都会收集其运行时的 热点代码 并用于 dex2oat 进行 编译优化 。此处触发dex2oat 编译是零碎行为,并不受限于上述限度。但触发此处 dex2oat 编译的条件是比拟刻薄的,它要求设施必须处于闲暇状态且要连贯电源,而且其校验的距离是一天。

在上述条件下,由零碎触发的 dex2oat 编译,基本上很难触发,从而导致 dex 加载速度 降落 80% 以上,代码运行速度 降落 11% 以上,使得利用的 ANR 率晋升、晦涩度降落,最终影响用户的日常应用体验。

对于之前来说改良计划就是通过 利用过程 触发 dex2oat 编译来补救零碎触发 dex2oat 编译的有余,而现在因限度会导致局部机器无奈失效。

如何能力让用户领会到 dex2oat 带来的体验晋升呢?问题又如何解决呢?

上面通过摸索,一步步的迫近假相,解决问题~

摸索

摸索之前,先明确下外围点,本次摸索的指标就是为了让用户领会到 dex2oat 带来的体验晋升,其最大的妨碍就是零碎触发 dex2oat 的编译条件太刻薄,导致难以触发,之前的成功实践就是基于 App 维度 手动触发 dex2oat 编译来补救零碎触发 dex2oat 的编译的有余。

而当初仍需摸索的起因就是,本来的成功实践,目前在某些机器上曾经受限,为了实现指标,解决掉现有的问题,自然而然的想法就是,限度到底是什么?限度是如何失效的?是否能够绕过?

限度是什么?

目前对于限度的了解,应该仅限于背景中的形容,那 Google 官网 是怎么说的呢?

Android 运行时 (ART) 不再从利用过程调用 dex2oat。这项变更意味着 ART 将仅承受系统生成的 OAT 文件。(Android 运行时只承受系统生成的 OAT 文件)

通过 Google 官网 的形容大抵能够了解为,本来 ART 会从利用过程调用 dex2oat,当初不再从利用过程调用dex2oat 了,从而使得利用过程没有机会触发 dex2oat,从而达到限度App 维度 触发 dex2oat 的目标。

但问题的确有这么简略嘛?

通过比照 Android 9Android 10 的代码时发现,Android 9在构建 ClassLoader 的时候会触发 dex2oat,然而 Android 10 上相干代码曾经被移除,此处同Google 官网 的说法统一。

但如果限度仅仅如此的话,能够依照本来 ART 从利用过程调用 dex2oat 的形式,而后手动从利用过程调用就能够了。

因为 Android` `10 相干代码曾经移除,所以查看下 Android 9 的代码,看下之前是如何从利用过程调用 dex2oat 的,相干代码链接:https://android.googlesource….,通过查看代码能够看出,是通过拼接 dex2oat 的命令来触发执行的,依照如上代码,拼接 dex2oat 命令的伪代码如下:

//step1 拼接命令

List<String> commandAndParams = new ArrayList<>();

commandAndParams.add("dex2oat");

if (Build.VERSION.SDK_INT >= 24) {commandAndParams.add("--runtime-arg");

    commandAndParams.add("-classpath");

    commandAndParams.add("--runtime-arg");

    commandAndParams.add("&");

}

commandAndParams.add("--instruction-set=" + getCurrentInstructionSet());

// verify-none|interpret-only|verify-at-runtime|space|balanced|speed|everything|time

// 编译模式,不同的模式,影响最终的运行速度和磁盘大小的占用

if (mode == Dex2OatCompMode.FASTEST_NONE) {commandAndParams.add("--compiler-filter=verify-none");

} else if (mode == Dex2OatCompMode.FASTER_ONLY_VERIFY) {

    // 疾速编译

    if (Build.VERSION.SDK_INT > 25) {commandAndParams.add("--compiler-filter=quicken");

    } else {commandAndParams.add("--compiler-filter=interpret-only");

    }

} else if (mode == Dex2OatCompMode.SLOWLY_ALL) {

    // 全量编译

    commandAndParams.add("--compiler-filter=speed");

}

// 源码门路(apk or dex 门路)commandAndParams.add("--dex-file=" + sourceFilePath);

//dex2oat 产物门路

commandAndParams.add("--oat-file=" + optimizedFilePath);



String[] cmd= commandAndParams.toArray(new String[commandAndParams.size()]);



//step2 执行命令

Runtime.getRuntime().exec(cmd)

将上述拼接的 dex2oat 命令在 Android` `9 机器的 App 过程触发执行,的确失去合乎预期的 dex2oat 产物,并能够失常加载和应用,阐明命令拼接的是 OK 的,而后将上述命令在 Android 10targetSdkVersion>=29 机器的 App 过程触发执行,发现并没有失去 dex2oat 产物,并且失去如下日志:

type=1400 audit(0.0:569): avc: denied {execute} for name="dex2oat" dev="dm-2" ino=222 scontext=u:r:untrusted_app:s0:c12,c257,c512,c768 tcontext=u:object_r:dex2oat_exec:s0 tclass=file permissive=0

这个日志阐明了什么呢?

能够看到日志信息里有 avc: denied 关键词,阐明此操作受 SELinux 规定管控,并被回绝。

在进行日志剖析之前,先补充一下 SELinux 的相干常识,上面是 Google 官网 的阐明:

Android 应用平安增强型 Linux (SELinux) 对所有过程强制执行强制访问控制 (MAC),甚至包含以 Root/ 超级用户权限运行的过程(Linux 性能)

简略说,SELinux就是 Android 零碎以过程维度对其进行强制访问控制的管理体系。SELinux是依附配置的规定对过程进行束缚拜访权限。

上面回归正题,剖析下日志。

日志细节剖析如下:

  • type=1400:示意SYSCALL
  • denied {`execute` } :示意 执行权限 被回绝;
  • scontext=u:r:`untrusted_app`:s0:c12,c257,c512,c768:示意主体的平安上下文,其中 untrusted_appsource type
  • tcontext=u:object_r:`dex2oat_exec`:s0:示意指标资源的平安上下文,其中 dex2oat_exectarget type
  • tclass=file:示意指标资源的 class 类型
  • permissive=0:以后的 SELLinux 模式,1示意 permissive(宽松的),0 示意enforcing(严格的)

简略的说就是,当在 Android 10targetSdkVersion>=29 的机器上的 App 过程上执行拼接的 dex2oat 命令的时候,是由 untrusted_app * 触发dex2oat_exec ,* 而因为untrusted_app 的规定限度,导致其触发 dex2oat_execexecute权限被回绝。

上面简略总结一下:

  1. 限度 1:Android 10+零碎删除了在构建 ClassLoader 时触发 dex2oat 的相干代码,来限度从 利用过程 触发 dex2oat 的入口。
  2. 限度 2:Android 10+零碎的相干 SELinux 规定变更,限度 targetSdkVersion>=29 的时候从 利用过程 触发dex2oat

当初通过查阅相干代码和 SELinux 规定以及应用代码验证,真正的见识到了限度到底是什么样子的,又是如何失效的,以及真真切切的感触到它的威力 ……

那既然晓得限度是什么以及限度如何失效的了,那是否能够绕过呢?

限度是否绕过?

通过上面对限度的理解,能够先大胆的假如:

  1. targetSdkVersion设置小于29
  2. 假装利用过程为零碎过程
  3. 敞开 Android 零碎的 SELinux 检测
  4. 批改规定移除限度

上面开始小心求证,上述假如是否可行?

对于 假如 1 来说,如果全局设置 targetSdkVersion 小于 29 的话,则会影响 App 后续在利用商店的上架,如果部分设置 targetSdkVersion 小于 29 的话,不仅难以批改且机会难以把握,dex2oat是独自的过程进行编译操作的,不同的过程对其进行触发编译的时候,会将过程的 targetSdkVersion 信息作为参数传给它,用于它外部逻辑的判断,而过程信息是存在于零碎过程的。

对于 假如 2 来说,目前还没相干的已知操作能够做到相似成果 …

对于 假如 3 来说,Android零碎的确也提供了敞开 SELinux 检测的办法,然而须要 Root 权限。

对于 假如 4 来说,如果全局批改规定,须要从新编译系统,才能够失效,如果部分批改规定(内存中批改),此处所需的权限也比拟高,也无权操作。

所以,从目前来看,绕过根本不可行了 …

那怎么办?限度绕不过来,指标无奈达成了 …

或者谜底就在谜面上,既然 Android 零碎限度只能应用系统生成的,那咱们就用系统生成的?

只须要让零碎能够感知到咱们的操作,能够依据咱们提供的操作去生成,能够由咱们去管制生成的机会以及成果,这样不如同在 利用过程 触发 dex2oat 有一样的成果了嘛?

那如何操作呢?

借助零碎的能力?

零碎是否提供了能够供 利用过程 触发零碎行为,而后由零碎触发 dex2oat 的形式?

通过查阅 Android 的官网文档以及相干代码发现能够通过如下形式进行操作(强制编译):

  • 基于配置文件编译:adb shell cmd package compile -m speed-profile -f my-package
  • 全面编译:adb shell cmd package compile -m speed -f my-package

上述命令不仅反对抉择编译模式 (speed-profile or speed),而且还能够抉择特定的App 进行操作(my-package)。

通过运行上述命令发现的确能够在 targetSdkVersion>=29Android 10+的零碎上编译出对应的 dex2oat 产物,且能够失常加载应用!!!

然而上述命令仅反对 Primary Apk 并不反对Secondary Apk,感觉它的性能还不止于此,还能够持续开掘一下这个命令的后劲,上面看下这个命令的实现。

剖析之前须要先确定命令对应的代码实现,这里应用了个小技巧,通过成心输错命令,发现最终解体的地位在 PackageManagerShellCommand,而后通过debug 源码,梳理了一下残缺的代码调用流程,细节如下。

为了不便了解,上面将代码的调用流程应用时序图形容进去。

下图为 Primary Apk 的编译流程:

无奈复制加载中的内容

在梳理 Primary Apk 的编译流程的时候,发现代码中也有解决 Secondary Apk 的办法,上面梳理流程如下:

无奈复制加载中的内容

而后依据其代码,梳理其编译命令为:adb shell cmd package compile -m speed -f --secondary-dex my-package

至此,咱们曾经失去了一种能够借助命令使零碎触发 dex2oat 编译的形式,且能够反对 Primary ApkSecondary Apk

还有一些细节须要留神,Primary Apk的命令传入的是 App 的包名,Secondary Apk的命令传入的也是包名,那哪些 Secondary Apk 会参加编译呢?

这就波及到 Secondary Apk 的注册了,只有注册了的 Secondary Apk 才会参加编译。

上面是 Secondary Apk 注册的流程:

无奈复制加载中的内容

对于 Secondary Apk 来说只注册不反注册也不行,因为对于 Secondary Apk 来说,每次编译仅想编译新增的或者未被编译过的,对于曾经编译过的,是不想其仍参加编译,所以这些曾经编译过的,就须要进行反注册。

上面是 Secondary Apk 反注册的流程:

无奈复制加载中的内容

而且通过查看源码发现,触发此处的形式其实有两种:

  1. 形式一 :应用adb shell cmd package + 命令。例如adb shell cmd package compile -m quicken com.bytedance.demo,其含意就是触发runCompile 办法,而后指定编译模式为 quicken,指定编译的包名为com.bytedance.demo,因为没有指定是Secondary,所以依照Primary 编译。而后其底层通过 socket+binder 实现通信,最终交由 PackageManagerBinder解决。
  2. 形式二 :应用PackageManagerBinder,并设定 code=SHELL_COMMAND_TRANSACTION,而后将命令以数组的模式封装到data 内即可。

对于形式一来说,依赖 adb 的实现,底层通信须要依赖socket + binder,而对于形式二来说,底层通信间接应用binder,相比来说更高效,所以最终抉择第二种形式。

上面简略的总结一下。

在得悉限度无奈被绕过后,就想到是否能够使得 利用过程 能够触发零碎行为,而后由零碎触发 dex2oat,而后通过查阅官网文档找到对应的adb 命令 能够满足诉求,不过此时仅看到 Primary Apk 的相干实现,而后持续通过查看代码验证其流程,找到 Secondary Apk 的相干实现,而后依据理论场景的须要,又持续查看代码,找到注册 Secondary Apk 和反注册 Secondary Apk 的办法,而后通过比照 adb 命令 的实现和 binder 的实现差别,最终选用 binder 的实现形式,来实现上述操作。

既然摸索曾经实现,那么上面就依据摸索的后果,实现落地实际,并验证其成果。

实际

操作

示例代码如下:

// 执行疾速编译

@Override

public void dexOptQuicken(String pluginPackageName, int version) {

    //step1:如果没有初始化则初始化

    maybeInit();

    //step2: 将 apk 门路进行注册到 PMS

    registerDexModule(pluginPackageName, version);

    //step3: 应用 binder 触发疾速编译

    dexOpt(COMPILE_FILTER_QUICKEN, pluginPackageName, version);

    //step4: 将 apk 门路反注册到 PMS

    unregisterDexModule(pluginPackageName, version);

}



// 执行全量编译

@Override

public void dexOptSpeed(String pluginPackageName, int version) {

    //step1:如果没有初始化则初始化

    maybeInit();

    //step2: 将 apk 门路进行注册到 PMS

    registerDexModule(pluginPackageName, version);

    //step3: 应用 binder 触发全量编译

    dexOpt(COMPILE_FILTER_SPEED, pluginPackageName, version);

    //step4: 将 apk 门路反注册到 PMS

    unregisterDexModule(pluginPackageName, version);

}

实现

 /**

 * Try To Init (Build Base env)

 */

private void maybeInit() {if (mContext == null || mPmBinder != null) {return;}



    PackageManager packageManager = mContext.getPackageManager();



    Field mPmField = safeGetField(packageManager, "mPM");

    if (mPmField == null) {return;}



    mPmObj = safeGetValue(mPmField, packageManager);

    if (!(mPmObj instanceof IInterface)) {return;}



    IInterface mPmInterface = (IInterface) mPmObj;

    IBinder binder = mPmInterface.asBinder();

    if (binder != null) {mPmBinder = binder;}

}



 /**

 * DexOpt (Add Retry Function)

 */

private void dexOpt(String compileFilter, String pluginPackageName, int version) {String tempFilePath = PluginDirHelper.getTempSourceFile(pluginPackageName, version);

    String tempCacheDirPath = PluginDirHelper.getTempDalvikCacheDir(pluginPackageName, version);

    String tempOatDexFilePath = tempCacheDirPath + File.separator + PluginDirHelper.getOatFileName(tempFilePath);

    File tempOatDexFile = new File(tempOatDexFilePath);



    for (int retry = 1; retry <= MAX_RETRY_COUNT; retry++) {execCmd(buildDexOptArgs(compileFilter), null);

        if (tempOatDexFile.exists()) {break;}

    }

}



 /**

 * Register DexModule(dex path) To PMS

 */

private void registerDexModule(String pluginPackageName, int version) {if (pluginPackageName == null || mContext == null) {return;}



    String originFilePath = PluginDirHelper.getSourceFile(pluginPackageName, version);

    String tempFilePath = PluginDirHelper.getTempSourceFile(pluginPackageName, version);

    safeCopyFile(originFilePath, tempFilePath);



    String loadingPackageName = mContext.getPackageName();

    String loaderIsa = getCurrentInstructionSet();

    notifyDexLoad(loadingPackageName, tempFilePath, loaderIsa);

}



 /**

 * Register DexModule(dex path) To PMS By Binder

 */

private void notifyDexLoad(String loadingPackageName, String dexPath, String loaderIsa) {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {

        //deal android 11\12

        realNotifyDexLoadForR(loadingPackageName, dexPath, loaderIsa);

    } else {

        //deal android 10

        realNotifyDexLoad(loadingPackageName, dexPath, loaderIsa);

    }

}



 /**

 * Register DexModule(dex path) To PMS By Binder for R+

 */

private void realNotifyDexLoadForR(String loadingPackageName, String dexPath, String loaderIsa) {if (mPmObj == null || loadingPackageName == null || dexPath == null || loaderIsa == null) {return;}

    Map<String, String> maps = Collections.singletonMap(dexPath, "PCL[]");

    safeInvokeMethod(mPmObj, "notifyDexLoad",

            new Object[]{loadingPackageName, maps, loaderIsa},

            new Class[]{String.class, Map.class, String.class});

}



 /**

 * Register DexModule(dex path) To PMS By Binder for Q

 */

private void realNotifyDexLoad(String loadingPackageName, String dexPath, String loaderIsa) {if (mPmObj == null || loadingPackageName == null || dexPath == null || loaderIsa == null) {return;}

    List<String> classLoadersNames = Collections.singletonList("dalvik.system.DexClassLoader");

    List<String> classPaths = Collections.singletonList(dexPath);

    safeInvokeMethod(mPmObj, "notifyDexLoad",

            new Object[]{loadingPackageName, classLoadersNames, classPaths, loaderIsa},

            new Class[]{String.class, List.class, List.class, String.class});

}



 /**

 * UnRegister DexModule(dex path) To PMS

 */

private void unregisterDexModule(String pluginPackageName, int version) {if (pluginPackageName == null || mContext == null) {return;}



    String originDir = PluginDirHelper.getSourceDir(pluginPackageName, version);

    String tempDir = PluginDirHelper.getTempSourceDir(pluginPackageName, version);

    safeCopyDir(tempDir, originDir);



    String tempFilePath = PluginDirHelper.getTempSourceFile(pluginPackageName, version);

    safeDelFile(tempFilePath);



    reconcileSecondaryDexFiles();}



 /**

 * Real UnRegister DexModule(dex path) To PMS (By Binder)

 */

private void reconcileSecondaryDexFiles() {execCmd(buildReconcileSecondaryDexFilesArgs(), null);

}



 /**

 * Process CMD (By Binder)(Have system permissions)*/

private void execCmd(String[] args, Callback callback) {Parcel data = Parcel.obtain();

    Parcel reply = Parcel.obtain();

    data.writeFileDescriptor(FileDescriptor.in);

    data.writeFileDescriptor(FileDescriptor.out);

    data.writeFileDescriptor(FileDescriptor.err);

    data.writeStringArray(args);

    data.writeStrongBinder(null);



    ResultReceiver resultReceiver = new ResultReceiverCallbackWrapper(callback);

    resultReceiver.writeToParcel(data, 0);



    try {mPmBinder.transact(SHELL_COMMAND_TRANSACTION, data, reply, 0);

        reply.readException();} catch (Throwable e) {//Report info} finally {data.recycle();

        reply.recycle();}

}



 /**

 * Build dexOpt args

 *

 *  @param compileFilter compile filter

 *  @return cmd args

 */

private String[] buildDexOptArgs(String compileFilter) {

    return buildArgs("compile", "-m", compileFilter, "-f", "--secondary-dex",

            mContext == null ? "" : mContext.getPackageName());

}



 /**

 * Build ReconcileSecondaryDexFiles Args

 *

 *  @return cmd args

 */

private String[] buildReconcileSecondaryDexFilesArgs() {return buildArgs("reconcile-secondary-dex-files", mContext == null ? "" : mContext.getPackageName());

}



 /**

 * Get the InstructionSet through reflection

 */

private String getCurrentInstructionSet() {

    String currentInstructionSet;

    try {Class vmRuntimeClazz = Class.forName("dalvik.system.VMRuntime");

        currentInstructionSet = (String) MethodUtils.invokeStaticMethod(vmRuntimeClazz,

                "getCurrentInstructionSet");

    } catch (Throwable e) {currentInstructionSet = "arm64";}

    return currentInstructionSet;

}

验证

Android 10+ dex2oat 计划兼容状况

上面是针对本计划兼容性验证的后果:

指标版本 零碎版本 手机品牌 Register Dex Module Dex Opt UnRegister Dex Module 手机型号
Target29 Android 10 Vivo – Yes – Yes – Yes Vivo IQOO
Target29 Android 10 Oppo – Yes – Yes – Yes Oppo R15
Target29 Android 10 MI – Yes – Yes – Yes MI 8
Target29 Android 10 华为 – Yes – Yes – Yes 华为 nova 7
Target29 Android 11 Vivo – Yes – Yes – Yes Vivo V20
Target29 Android 11 Oppo – Yes – Yes – Yes Oppo PDPM00(Oppo Android 11 对 Rom 进行了批改,目前暂不反对)
Target29 Android 11 MI – Yes – Yes – Yes MI M2011K2C
Target29 Android 11 华为 – Yes – Yes – Yes 无此机器
Target29 Android 12 Piexl – Yes – Yes – Yes 本地真机
Target30 Android 10 Vivo – Yes – Yes – Yes Vivo S1
Target30 Android 10 Oppo – Yes – Yes – Yes Oppo Find X
Target30 Android 10 MI – Yes – Yes – Yes MI 8
Target30 Android 10 华为 – Yes – Yes – Yes 华为 P20
Target30 Android 11 Vivo – Yes – Yes – Yes Vivo V2046A
Target30 Android 11 Oppo – Yes – Yes – Yes Oppo PDPM00(Oppo Android 11 对 Rom 进行了批改,目前暂不反对)
Target30 Android 11 MI – Yes – Yes – Yes MI M2011K2C
Target30 Android 11 华为 – Yes – Yes – Yes 无此机器
Target30 Android 12 Piexl – Yes – Yes – Yes 本地真机

目前来看,对于手机品牌来说,该计划均能够兼容,仅 Oppo 且 Android 11 的机器上,因为对 Rom 进行了批改限度,导致此款机器不兼容。

兼容成果还算良好。

Android 10+ 优化前后 Dex 加载速度比照

上面针对高中低端的机器上,验证下优化前后 Dex 加载速度的差别:

机器性能 机器型号 包大小 优化前均匀耗时 优化后均匀耗时 缩小耗时占总耗时百分比
低端机 Piexl 2 1.9m 269.5ms 12ms 95.5%
中端机 Vivo S1 1.9m 159ms 8.8ms 94%
高端机 MI 8 1.9m 48.3ms 6.5ms 86%

对于 Dex 加载 耗时的统计,是采纳统计首次 new ClassLoaderDex加载的耗时。

Dex 加载 耗时同 包大小 属于 正相干 ,包越大,加载耗时越多;同 机器性能 属于 负相关,机器性能越好,加载耗时越少。

通过上述数据能够看出,优化前后耗时差距还是非常明显的,机器性能越差优化越显著。

Dex 加载 速度优化显著。

Android 10+ 优化前后场景运行耗时比照

上面针对高中低端的机器上,验证下优化前后场景运行速度的差别:

机器性能 机器型号 优化前均匀耗时 优化后均匀耗时 缩小耗时占总耗时百分比
低端机 Piexl 2 45ms 36ms 20%
中端机 Vivo S1 36.75ms 31.23ms 13.6%
高端机 MI 8 13ms 11.5ms 11.5%

对于场景运行耗时的统计,是采纳对场景启动前后打点,而后计算时间差。

因为非全量编译对运行速度影响较小,上述数据为未优化同全量编译优化的比照数据。

场景耗时 场景复杂度 属于 正相干 ,场景复杂度越高,场景耗时越多;同 机器性能 属于 负相关,机器性能越好,场景耗时越少。

通过上述数据能够看出,优化后对运行速度还是有质的晋升的,且会随场景复杂度的晋升,带来更大的晋升。

总结

最终,通过假借零碎之手来触发 dex2oat 的形式,绕过 targetSdkVersion>=29Android10+上的限度,成果较为显著,dex加载速度晋升 80% 以上,场景运行速度晋升 11% 以上。

对于字节终端技术团队

字节跳动终端技术团队 (Client Infrastructure) 是大前端根底技术的全球化研发团队(别离在北京、上海、杭州、深圳、广州、新加坡和美国山景城设有研发团队),负责整个字节跳动的大前端基础设施建设,晋升公司全产品线的性能、稳定性和工程效率;反对的产品包含但不限于抖音、今日头条、西瓜视频、飞书、懂车帝等,在挪动端、Web、Desktop 等各终端都有深入研究。

就是当初!客户端/前端/服务端/端智能算法/测试开发 面向寰球范畴招聘!一起来用技术扭转世界,感兴趣请分割 chenxuwei.cxw@bytedance.com,邮件主题简历 - 姓名 - 求职意向 - 冀望城市 - 电话。

字节跳动利用开发套件 MARS 是字节跳动终端技术团队过来九年在抖音、今日头条、西瓜视频、飞书、懂车帝等 App 的研发实际成绩,面向挪动研发、前端开发、QA、运维、产品经理、项目经理以及经营角色,提供一站式整体研发解决方案,助力企业研发模式降级,升高企业研发综合老本。

正文完
 0