作者:字節跳動終端技術——郭海洋
背景
對於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(寬松的),0表示enforcing(嚴格的)
簡單的說就是,當在Android 10 且targetSdkVersion>=29的機器上的App進程上執行拼接的dex2oat命令的時候,是由untrusted_app ****觸發dex2oat_exec , 而由於untrusted_app的規則限制,導致其觸發dex2oat_exec的execute權限被拒絕。
下面簡單總結一下:
- 限制1:
Android 10+系統刪除了在構建ClassLoader時觸發dex2oat的相關代碼,來限制從應用進程觸發dex2oat的入口。 - 限制2:
Android 10+系統的相關SELinux規則變更,限制targetSdkVersion>=29的時候從應用進程觸發dex2oat。
現在通過查閱相關代碼和SELinux規則以及使用代碼驗證,真正的見識到了限制到底是什么樣子的,又是如何生效的,以及真真切切的感受到它的威力......
那既然知道限制是什么以及限制如何生效的了,那是否可以繞過呢?
限制能否繞過?
通過上面對限制的了解,可以先大膽的假設:
targetSdkVersion設置小於29- 偽裝應用進程為系統進程
- 關閉
Android系統的SELinux檢測 - 修改規則移除限制
下面開始小心求證,上述假設是否可行?
對於假設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反注冊的流程:
無法復制加載中的內容
而且通過查看源碼發現,觸發此處的方式其實有兩種:
- 方式一:使用
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處理。 - 方式二:使用
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等各終端都有深入研究。
就是現在!客戶端/前端/服務端/端智能算法/測試開發 面向全球范圍招聘!一起來用技術改變世界,感興趣請聯系chenxuwei.cxw@bytedance.com,郵件主題簡歷-姓名-求職意向-期望城市-電話。
