Android性能優化之Android 10+ dex2oat實踐


作者:字節跳動終端技術——郭海洋

背景

對於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.com/platform/art/+/refs/tags/android-9.0.0_r52/runtime/oat_file_assistant.cc#698,通過查看代碼可以看出,是通過拼接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、 運維、產品經理、項目經理以及運營角色,提供一站式整體研發解決方案,助力企業研發模式升級,降低企業研發綜合成本。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM