
對于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:
系統删除了在建構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等各終端都有深入研究。
就是現在!用戶端/前端/服務端/端智能算法/測試開發 面向全球範圍招聘!一起來用技術改變世界,感興趣請聯系[email protected],郵件主題履歷-姓名-求職意向-期望城市-電話。
火山引擎應用開發套件 MARS 是位元組跳動終端技術團隊過去九年在抖音、今日頭條、西瓜視訊、飛書、懂車帝等 App 的研發實踐成果,面向移動研發、前端開發、QA、 運維、産品經理、項目經理以及營運角色,提供一站式整體研發解決方案,助力企業研發模式更新,降低企業研發綜合成本。