天天看點

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

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

對于Android App的性能優化來說,方式方法以及工具都有很多,而dex2oat作為其中的一員,卻可能不被大衆所熟知。

作者:位元組跳動終端技術——郭海洋

背景

對于

Android App

的性能優化來說,方式方法以及工具都有很多,而

dex2oat

作為其中的一員,卻可能不被大衆所熟知。它是

Android

官方應用于運作時,針對

dex

進行

編譯優化

的程式,通過對

dex

進行一系列的指令優化、編譯機器碼等操作,提升

dex加載速度

代碼運作速度

,進而提升安裝速度、啟動速度、以及應用使用過程中的流暢度,最終提升使用者日常的使用體驗。

它的适用範圍也比較廣,可以用于

Primary Apk

Secondary Apk

正常場景

插件場景

。(

Primary Apk

是指的

正常場景

下的

主包

base.apk

)或者

插件場景

下的

宿主包

Secondary Apk

是指的

正常場景

下的

自行加載的包

(

.apk

)或者

插件場景

下的

插件包

(

.apk

))。

而随着

Android

系統版本的更疊,發現原本可以在

應用程序

上觸發

dex2oat

編譯的方式,卻在

targetSdkVersion>=29

Android 10+

的系統上,不再允許使用。其原因是系統在

targetSdkVersion=29

的時候,對此做了限制,不允許

應用程序

上觸發

dex2oat

編譯(Android 運作時 (ART) 不再從應用程序調用 dex2oat。這項變更意味着 ART 将僅接受系統生成的 OAT 檔案)(

OAT

dex2oat

後的産物)。

那目前是否會受到這個限制的影響呢?

2020

年的時候

Android 11

系統正式釋出,各大應用市場就開始限制

App

targetSdkVersion>=29

,而

Android 11

系統距今已經釋出一年之久,也就意味着,現如今

App

targetSdkVersion>=29

是不可避免的。而且随着新

Android

裝置的不斷疊代,越來越多的使用者,使用上了攜帶新系統的新機器,使得

Android 10+

系統的占有量逐漸增加,目前為止

Android 10+

系統的占有量約占整體的

30%~40%

左右,也就是說這部分機器将會受到這個限制的影響。

那這個限制有什麼影響呢?

這個限制的關鍵是,不允許

應用程序

上觸發

dex2oat

編譯,換句話說就是并不影響系統自身去觸發

dex2oat

編譯,那麼限制的影響也就是,影響那些需要通過

應用程序

去觸發

dex2oat

編譯的場景。

對于

Primary Apk

Secondary 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 9

Android 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 10

targetSdkVersion>=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_app

    source type

  • tcontext=u:object_r:``dex2oat_exec``:s0

    :表示目标資源的安全上下文,其中

    dex2oat_exec

    target type

  • tclass=file

    :表示目标資源的

    class

    類型
  • permissive=0

    :目前的

    SELLinux

    模式,

    1

    表示

    permissive

    (寬松的), 表示

    enforcing

    (嚴格的)

簡單的說就是,當在

Android 10

targetSdkVersion>=29

的機器上的

App

程序上執行拼接的

dex2oat

指令的時候,是由

untrusted_app

****觸發

dex2oat_exec

, 而由于

untrusted_app

的規則限制,導緻其觸發

dex2oat_exec

execute

權限被拒絕。

下面簡單總結一下:

  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>=29

Android 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 Apk

Secondary 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

    完成通信,最終交由

    PackageManager

    Binder

    處理。
  2. 方式二:使用

    PackageManager

    Binder

    ,并設定

    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 ClassLoader

Dex

加載的耗時。

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>=29

Android10+

上的限制,效果較為明顯,

dex

加載速度提升

80%

以上,場景運作速度提升

11%

以上。

關于位元組終端技術團隊

位元組跳動終端技術團隊(Client Infrastructure)是大前端基礎技術的全球化研發團隊(分别在北京、上海、杭州、深圳、廣州、新加坡和美國山景城設有研發團隊),負責整個位元組跳動的大前端基礎設施建設,提升公司全産品線的性能、穩定性和工程效率;支援的産品包括但不限于抖音、今日頭條、西瓜視訊、飛書、懂車帝等,在移動端、Web、Desktop等各終端都有深入研究。

就是現在!用戶端/前端/服務端/端智能算法/測試開發 面向全球範圍招聘!一起來用技術改變世界,感興趣請聯系[email protected],郵件主題履歷-姓名-求職意向-期望城市-電話。

火山引擎應用開發套件 MARS 是位元組跳動終端技術團隊過去九年在抖音、今日頭條、西瓜視訊、飛書、懂車帝等 App 的研發實踐成果,面向移動研發、前端開發、QA、 運維、産品經理、項目經理以及營運角色,提供一站式整體研發解決方案,助力企業研發模式更新,降低企業研發綜合成本。