背景
在Android上,Java/Kotlin代码会编译为DEX字节码,在运行期由虚拟机解释执行。但是,字节码解释执行的速度比较慢。所以,通常虚拟机会在解释模式基础上做一些必要的优化。
在Android 5,Google采用的策略是在应用安装期间对APP的全量DEX进行AOT优化。AOT优化(Ahead of time),就是在APP运行前就把DEX字节码编译成本地机器码。虽然运行效率相比DEX解释执行有了大幅提高,但由于是全量AOT,就会导致用户需要等待较长的时间才能打开应用,对于磁盘空间的占用也急剧增大。
于是,为了避免过早的资源占用,从Android 7开始便不再进行全量AOT,而是JIT+AOT的混合编译模式。JIT(Just in time),就是即时优化,也就是在APP运行过程中,实时地把DEX字节码编译成本地机器码。具体方式是,在APP运行时分析运行过的热代码,然后在设备空闲时触发AOT,在下次运行前预编译热代码,提升后续APP运行效率。
但是热代码代码收集需要比较长周期,在APP升级覆盖安装之后,原有的预编译的热代码失效,需要再走一遍运行时分析、空闲时AOT的流程。在单周迭代的研发模式下问题尤为明显。
因此,从Android 9 开始,Google推出了Cloud Profiles技术。它的原理是,在部分先安装APK的用户手机上,Google Play Store收集到热点代码,然后上传到云端并聚合。这样,对于后面安装的用户,Play Store会下发热点代码配置进行预编译,这些用户就不需要进行运行时分析,大大提前了优化时机。不过,这个收集聚合下发过程需要几天时间,大部分用户还是没法享受到这个优化。
最终,在2022年Google推出了 Baseline Profiles (https://developer.android.com/topic/performance/baselineprofiles/overview?hl=zh-cn)技术。它允许开发者内置自己定义的热点代码配置文件。在APP安装期间,系统提前预编译热点代码,大幅提升APP运行效率。
不过,Google官方的Baseline Profiles存在以下局限性:
Baseline Profile 需要使用 AGP 7 及以上的版本,公司内各大APP的版本都还比较低,短期内并不可用 安装时优化依赖Google Play,国内无法使用
为此,我们开发了一套定制化的Baseline Profiles优化方案,可以适用于全版本AGP。同时通过与国内主流厂商合作,推进支持了安装时优化生效。
方案探索与实现
我们先来看一下官方Baseline Profile安装时优化的流程:
这里面主要包含3个步骤:
热点方法收集,通过本地运行设备或者人工配置,得到可读格式的基准配置文本文件(baseline-prof.txt) 编译期处理,将基准配置文本文件转换成二进制文件,打包至apk内(baseline.prof和baseline.profm),另外Google Play服务端还会将云端profile与baseline.prof聚合处理。 安装时,系统会解析apk内的baseline.prof二进制文件,根据版本号,做一些转换后,提前预编译指定的热点代码为机器码。
热点方法收集
官方文档(https://developer.android.com/topic/performance/baselineprofiles/create-baselineprofile)提到使用Jetpack Macrobenchmark库(https://developer.android.com/macrobenchmark) 和 BaselineProfileRule
自动收集热点方法。通过在Android Studio中引入Benchmark module,需要编写相应的Rule触发打包、测试等流程。
从下面源码可以看到,最终是通过profman命令可以收集到app运行过程中的热点方法。
private fun profmanGetProfileRules(apkPath: String, pathOptions: List<String>): String {
// When compiling with CompilationMode.SpeedProfile, ART stores the profile in one of
// 2 locations. The `ref` profile path, or the `current` path.
// The `current` path is eventually merged into the `ref` path after background dexopt.
val profiles = pathOptions.mapNotNull { currentPath ->
Log.d(TAG, "Using profile location: $currentPath")
val profile = Shell.executeScriptCaptureStdout(
"profman --dump-classes-and-methods --profile-file=$currentPath --apk=$apkPath"
)
profile.ifBlank { null }
}
...
return builder.toString()
}
所以,我们可以绕过Macrobenchmark库,直接使用profman命令,减少自动化接入成本。具体命令如下:
adb shell profman --dump-classes-and-methods
--profile-file=/data/misc/profiles/cur/0/com.ss.android.article.video/primary.prof
--apk=/data/app/com.ss.android.article.video-Ctzj32dufeuXB8KOhAqdGg==/base.apk
> baseline-prof.txt
生成的baseline-prof.txt文件内容如下:
PLcom/bytedance/apm/perf/b/f;->a(Lcom/bytedance/apm/perf/b/f;)Ljava/lang/String;
PLcom/bytedance/bdp/bdpbase/ipc/n$a;->a()Lcom/bytedance/bdp/bdpbase/ipc/n;
HSPLorg/android/spdy/SoInstallMgrSdk;->initSo(Ljava/lang/String;I)Z
HSPLorg/android/spdy/SpdyAgent;->InvlidCharJudge([B[B)V
Lanet/channel/e/a$b;
Lcom/bytedance/alliance/services/impl/c;
...
这些规则采用两种形式,分别指明方法和类。方法的规则如下所示:
[FLAGS][CLASS_DESCRIPTOR]->[METHOD_SIGNATURE]
FLAGS表示 H
、S
和 P
中的一个或多个字符,用于指示相应方法在启动类型方面应标记为 Hot
、Startup
还是 Post Startup
:
带有 H
标记表示相应方法是一种“热”方法,这意味着相应方法在应用的整个生命周期内会被调用多次。带有 S
标记表示相应方法在启动时被调用。带有 P
标记表示相应方法是与启动无关的热方法。
类的规则,则是直接指明类签名即可:
[CLASS_DESCRIPTOR]
不过这里是可读的文本格式,后续还需要进一步转为二进制才可以被系统识别。
另外,release包导出的是混淆后的符号,需要根据mapping文件再做一次反混淆才能使用。
编译期处理
在得到base.apk的基准配置文本文件(baseline-prof.txt)之后还不够,一些库里面
(比如androidx的库里https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:recyclerview/recyclerview/src/main/baseline-prof.txt)
也会自带baseline-prof.txt文件。所以,我们还需要把这些子library内附带的baseline-prof.txt取出来,与base.apk的配置一起合并成完整的基准配置文本文件。
接下来,我们需要把完整的配置文件转换成baseline.prof二进制文件。具体是由AGP 7.x内的 CompileArtProfileTask.kt
实现的 :
/**
* Task that transforms a human readable art profile into a binary form version that can be shipped
* inside an APK or a Bundle.
*/
abstract class CompileArtProfileTask: NonIncrementalTask() {
...
abstract class CompileArtProfileWorkAction:
ProfileAwareWorkAction<CompileArtProfileWorkAction.Parameters>() {
override fun run() {
val diagnostics = Diagnostics {
error -> throw RuntimeException("Error parsing baseline-prof.txt : $error")
}
val humanReadableProfile = HumanReadableProfile(
parameters.mergedArtProfile.get().asFile,
diagnostics
) ?: throw RuntimeException(
"Merged ${SdkConstants.FN_ART_PROFILE} cannot be parsed successfully."
)
val supplier = DexFileNameSupplier()
val artProfile = ArtProfile(
humanReadableProfile,
if (parameters.obfuscationMappingFile.isPresent) {
ObfuscationMap(parameters.obfuscationMappingFile.get().asFile)
} else {
ObfuscationMap.Empty
},
//need to rename dex files with sequential numbers the same way [DexIncrementalRenameManager] does
parameters.dexFolders.asFileTree.files.sortedWith(DexFileComparator()).map {
DexFile(it.inputStream(), supplier.get())
}
)
// the P compiler is always used, the server side will transcode if necessary.
parameters.binaryArtProfileOutputFile.get().asFile.outputStream().use {
artProfile.save(it, ArtProfileSerializer.V0_1_0_P)
}
// create the metadata.
parameters.binaryArtProfileMetadataOutputFile.get().asFile.outputStream().use {
artProfile.save(it, ArtProfileSerializer.METADATA_0_0_2)
}
}
}
这里的核心逻辑就是做了以下3件事:
读取baseline-prof.txt基准配置文本文件,下文用HumanReadableProfile表示 将HumanReadableProfile、proguard mapping文件、dex文件作为输入传给ArtProfile 由ArtProfile生成特定版本格式的baseline.prof二进制文件
ArtProfile类是在profgen子工程内实现的,其中有两个关键的方法:
构造方法:读取HumanReadableProfile、proguard mapping文件、dex文件作为参数,构造ArtProfile实例 save()方法:输出指定版本格式的baseline.prof二进制文件
至此,我们可以基于profgen开发一个gradle plugin,在编译构建流程中插入一个自定义task,将baseline-prof.txt转换成baseline.prof,并内置到apk的asset目录。
核心代码如下:
val packageAndroidTask =
variant.variantScope.taskContainer.packageAndroidTask?.get()
packageAndroidTask?.doFirst {
var dexFiles = collectDexFiles(variant.packageApplication.dexFolders)
dexFiles = dexFiles.sortedWith(DexFileComparator())
//基准配置文件的内存表示
var hrp = HumanReadableProfile("baseline-prof.txt")
var obfFile: File? = getObfFile(variant, proguardTask)
val apk = Apk(dexFiles, "")
val obf =
if (obfFile != null) ObfuscationMap(obfFile) else ObfuscationMap.Empty
val profile = ArtProfile(hrp!!, obf, apk)
val dexoptDir = File(variant.mergedAssets.first(), profDir)
if (!dexoptDir.exists()) {
dexoptDir.mkdirs()
}
val outFile = File(dexoptDir, "baseline.prof")
val metaFile = File(dexoptDir, "baseline.profm")
profile.save(outFile.outputStream(), ArtProfileSerializer.V0_1_0_P)
profile.save(metaFile.outputStream(), ArtProfileSerializer.METADATA_0_0_2)
}
自定义task主要包含以下几个步骤:
解压apk获取dex列表,按照一定规则排序(跟Android的打包规则有关,dex文件名和crc等信息需要和prof二进制文件内的对应上) 通过ObfuscationMap将baseline-prof.txt文件中的符号转换成混淆后的符号 通过ArtProfile按照一定格式转换成baseline.prof与baseline.profm二进制文件
其中有两个文件:
baseline.prof:包含热点方法id、类id信息的二进制编码文件 baseline.profm:用于高版本转码的二进制扩展文件
关于baseline.prof的格式,我们从ArtProfileSerializer.kt
的注释可以看到不同Android版本有不同的格式。Android 12 开始需要另外转码才能兼容,详见可以看这个issue:
安装期处理
在生成带有baseline.prof二进制文件的APK之后,再来看一下系统在安装apk时如何处理这个baseline.prof文件(基于Android 13源码分析)。本地测试通过adb install-multiple release.apk release.dm
命令执行安装,然后通过Android系统包管理子系统进行安装时优化。
Android系统包管理框架分为3层:
应用层:应用通过getPackageManager获取PMS的实例,用于应用的安装,卸载,更新等操作 PMS服务层:拥有系统权限,解析并记录应用的基本信息(应用名称,数据存放路径、关系管理等),最终通过binder系统层的installd系统服务进行通讯 Installd系统服务层:拥有root权限,完成最终的apk安装、dex优化
其中处理baseline.prof二进制文件并最终指导编译生成odex的主要路径如下:
InstallPackageHelper.java#installPackagesLI
InstallPackageHelper.java#executePostCommitSteps
ArtManagerService.java#prepareAppProfiles
Installer.java#prepareAppProfile
InstalldNativeService.cpp#prepareAppProfile
dexopt.cpp#prepare_app_profile
ProfileAssistant.cpp#ProcessProfilesInternal
PackageDexOptimizer.java#performDexOpt
PackageDexOptimizer.java#performDexOptLI
PackageDexOptimizer.java#dexOptPath
InstalldNativeService.cpp#dexopt
dexopt.cpp#dexopt
dex2oat.cc
在入口installPackagesLI函数中,经过prepare、scan、Reconcile、Commit 四个阶段后最终调用executePostCommitSteps完成apk安装、prof文件写入、dexopt优化:
private void installPackagesLI(List<InstallRequest> requests) {
//阶段1:prepare
prepareResult = preparePackageLI(request.mArgs, request.mInstallResult);
//阶段2:scan
final ScanResult result = scanPackageTracedLI(
prepareResult.mPackageToScan, prepareResult.mParseFlags,
prepareResult.mScanFlags, System.currentTimeMillis(),
request.mArgs.mUser, request.mArgs.mAbiOverride);
//阶段3:Reconcile
reconciledPackages = ReconcilePackageUtils.reconcilePackages(
reconcileRequest, mSharedLibraries,
mPm.mSettings.getKeySetManagerService(), mPm.mSettings);
//阶段4:Commit并安装
commitRequest = new CommitRequest(reconciledPackages,
mPm.mUserManager.getUserIds());
executePostCommitSteps(commitRequest);
}
executePostCommitSteps中,主要完成prof文件写入与dex优化:
private void executePostCommitSteps(CommitRequest commitRequest) {
for (ReconciledPackage reconciledPkg : commitRequest.mReconciledPackages.values()) {
final AndroidPackage pkg = reconciledPkg.mPkgSetting.getPkg();
final String packageName = pkg.getPackageName();
final String codePath = pkg.getPath();
//步骤1:prof文件写入
// Prepare the application profiles for the new code paths.
// This needs to be done before invoking dexopt so that any install-time profile
// can be used for optimizations.
mArtManagerService.prepareAppProfiles(pkg,
mPm.resolveUserIds(reconciledPkg.mInstallArgs.mUser.getIdentifier()),
/* updateReferenceProfileContent= */ true);
//步骤2:dex优化,在开启baseline profile优化之后compilation-reason=install-dm
final int compilationReason =
mDexManager.getCompilationReasonForInstallScenario(
reconciledPkg.mInstallArgs.mInstallScenario);
DexoptOptions dexoptOptions =
new DexoptOptions(packageName, compilationReason, dexoptFlags);
if (performDexopt) {
// Compile the layout resources.
if (SystemProperties.getBoolean(PRECOMPILE_LAYOUTS, false)) {
mViewCompiler.compileLayouts(pkg);
}
ScanResult result = reconciledPkg.mScanResult;
mPackageDexOptimizer.performDexOpt(pkg, realPkgSetting,
null /* instructionSets */,
mPm.getOrCreateCompilerPackageStats(pkg),
mDexManager.getPackageUseInfoOrDefault(packageName),
dexoptOptions);
}
// Notify BackgroundDexOptService that the package has been changed.
// If this is an update of a package which used to fail to compile,
// BackgroundDexOptService will remove it from its denylist.
BackgroundDexOptService.getService().notifyPackageChanged(packageName);
notifyPackageChangeObserversOnUpdate(reconciledPkg);
}
PackageManagerServiceUtils.waitForNativeBinariesExtractionForIncremental(
incrementalStorages);
}
prof文件写入
先来看下prof文件写入流程,主要流程如下图所示:
其入口在ArtManagerService.java``#``prepareAppProfiles
:
adb shell profman --dump-classes-and-methods
--profile-file=/data/misc/profiles/cur/0/com.ss.android.article.video/primary.prof
--apk=/data/app/com.ss.android.article.video-Ctzj32dufeuXB8KOhAqdGg==/base.apk
> baseline-prof.txt
0
其中dexMetadata是后缀为.dm的压缩文件,内部包含primary.prof、primary.profm文件,apk的baseline.prof、baseline.profm会在安装阶段转为成dm文件。
mInstaller.prepareAppProfile
最终会调用到dexopt.cpp#prepare_app_profile
中,通过fork一个子进程执行profman二进制程序,将dm文件、reference_profile文件(位于设备上固定路径,存储汇总的热点方法)、apk文件作为参数输入:
adb shell profman --dump-classes-and-methods
--profile-file=/data/misc/profiles/cur/0/com.ss.android.article.video/primary.prof
--apk=/data/app/com.ss.android.article.video-Ctzj32dufeuXB8KOhAqdGg==/base.apk
> baseline-prof.txt
1
实际上,就是执行了下面的profman命令:
adb shell profman --dump-classes-and-methods
--profile-file=/data/misc/profiles/cur/0/com.ss.android.article.video/primary.prof
--apk=/data/app/com.ss.android.article.video-Ctzj32dufeuXB8KOhAqdGg==/base.apk
> baseline-prof.txt
2
reference-profile-file-fd指向/data/misc/profile/ref/$package/primary.prof
文件,记录当前apk版本的热点方法,最终baseline.prof保存的热点方法信息需要写入到reference-profile文件。
profman二进制程序的代码如下:
adb shell profman --dump-classes-and-methods
--profile-file=/data/misc/profiles/cur/0/com.ss.android.article.video/primary.prof
--apk=/data/app/com.ss.android.article.video-Ctzj32dufeuXB8KOhAqdGg==/base.apk
> baseline-prof.txt
3
可以看到最后一行调用到profman的ProcessProfiles方法,它里面调用了ProfileAssistant.cpp#ProcessProfilesInternal[https://cs.android.com/android/platform/superproject/+/master:art/profman/profile_assistant.cc;l=30?q=ProcessProfilesInternal],核心代码如下:
adb shell profman --dump-classes-and-methods
--profile-file=/data/misc/profiles/cur/0/com.ss.android.article.video/primary.prof
--apk=/data/app/com.ss.android.article.video-Ctzj32dufeuXB8KOhAqdGg==/base.apk
> baseline-prof.txt
4
这里首先通过ProfileCompilationInfo的load方法,读取reference_profile二进制文件序列化加载到内存。再调用MergeWith方法将cur_profile二进制文件(也就是apk内的baseline.prof)合并到reference_profile文件中,最后调用Save方法保存。
再来看下ProfileCompilationInfo的类结构,可以发现与前面编译期处理提到的ArtProfile序列化格式是一致的。
adb shell profman --dump-classes-and-methods
--profile-file=/data/misc/profiles/cur/0/com.ss.android.article.video/primary.prof
--apk=/data/app/com.ss.android.article.video-Ctzj32dufeuXB8KOhAqdGg==/base.apk
> baseline-prof.txt
5
dex优化
分析完prof二进制文件处理流程之后,接着再来看dex优化部分。主要流程如下图所示:
dex优化的入口函数PackageDexOptimizer.java#performDexOptLI
,跟踪代码可以发现最终是通过调用dex2oat二进制程序:
adb shell profman --dump-classes-and-methods
--profile-file=/data/misc/profiles/cur/0/com.ss.android.article.video/primary.prof
--apk=/data/app/com.ss.android.article.video-Ctzj32dufeuXB8KOhAqdGg==/base.apk
> baseline-prof.txt
6
实际上是执行了如下命令:
adb shell profman --dump-classes-and-methods
--profile-file=/data/misc/profiles/cur/0/com.ss.android.article.video/primary.prof
--apk=/data/app/com.ss.android.article.video-Ctzj32dufeuXB8KOhAqdGg==/base.apk
> baseline-prof.txt
7
常规安装时不会带上dm-fd和install-dm参数,所以不会触发baseline profile相关优化。
dex2oat用于将dex字节码编译成本地机器码,相关的编译流程如下代码:
adb shell profman --dump-classes-and-methods
--profile-file=/data/misc/profiles/cur/0/com.ss.android.article.video/primary.prof
--apk=/data/app/com.ss.android.article.video-Ctzj32dufeuXB8KOhAqdGg==/base.apk
> baseline-prof.txt
8
这个流程包含:
解析命令行传入的参数 调用LoadProfile()加载profile热点方法文件,保存到profile_compilation_info_成员变量中 准备dex2oat环境,包括启动unstarted runtime、加载boot class path profile相关校验,主要检查profile_compilation_info_中的dex的crc与apk中dex的crc是否一致,方法数是否一致 调用DoCompilation正式开始编译
LoadProfile方法加载profile热点方法文件如下代码:
adb shell profman --dump-classes-and-methods
--profile-file=/data/misc/profiles/cur/0/com.ss.android.article.video/primary.prof
--apk=/data/app/com.ss.android.article.video-Ctzj32dufeuXB8KOhAqdGg==/base.apk
> baseline-prof.txt
9
LoadProfile方法,将之前生成的profile文件加载到内存,保存到profile_compilation_info_变量中。
接着调用Compile方法完成odex文件的编译生成,如下代码:
PLcom/bytedance/apm/perf/b/f;->a(Lcom/bytedance/apm/perf/b/f;)Ljava/lang/String;
PLcom/bytedance/bdp/bdpbase/ipc/n$a;->a()Lcom/bytedance/bdp/bdpbase/ipc/n;
HSPLorg/android/spdy/SoInstallMgrSdk;->initSo(Ljava/lang/String;I)Z
HSPLorg/android/spdy/SpdyAgent;->InvlidCharJudge([B[B)V
Lanet/channel/e/a$b;
Lcom/bytedance/alliance/services/impl/c;
...
0
profile_compilation_info_作为参数传给了CompilerDriver,在之后的编译过程中将用来判断是否编译某个方法和机器码重排。
CompilerDriver::Compile方法开始编译dex字节码,代码如下:
PLcom/bytedance/apm/perf/b/f;->a(Lcom/bytedance/apm/perf/b/f;)Ljava/lang/String;
PLcom/bytedance/bdp/bdpbase/ipc/n$a;->a()Lcom/bytedance/bdp/bdpbase/ipc/n;
HSPLorg/android/spdy/SoInstallMgrSdk;->initSo(Ljava/lang/String;I)Z
HSPLorg/android/spdy/SpdyAgent;->InvlidCharJudge([B[B)V
Lanet/channel/e/a$b;
Lcom/bytedance/alliance/services/impl/c;
...
1
在CompileMethodQuick方法中可以看到针对不同的方法(jni方法、虚方法、构造函数等)有不同的处理方式,常规方法会通过ShouldCompileBasedOnProfile来判断某个method是否需要被编译。
具体判断条件如下:
PLcom/bytedance/apm/perf/b/f;->a(Lcom/bytedance/apm/perf/b/f;)Ljava/lang/String;
PLcom/bytedance/bdp/bdpbase/ipc/n$a;->a()Lcom/bytedance/bdp/bdpbase/ipc/n;
HSPLorg/android/spdy/SoInstallMgrSdk;->initSo(Ljava/lang/String;I)Z
HSPLorg/android/spdy/SpdyAgent;->InvlidCharJudge([B[B)V
Lanet/channel/e/a$b;
Lcom/bytedance/alliance/services/impl/c;
...
2
可以看到是依据profile_compilation_info_是否命中hotmethod来判断。我们把编译日志打开,可以看到具体哪些方法被编译,哪些方法被跳过,如下图所示,这与我们配置的profile是一致的。
机器码生成的实现在CodeGenerator类中,代码如下,具体细节将不再展开。
PLcom/bytedance/apm/perf/b/f;->a(Lcom/bytedance/apm/perf/b/f;)Ljava/lang/String;
PLcom/bytedance/bdp/bdpbase/ipc/n$a;->a()Lcom/bytedance/bdp/bdpbase/ipc/n;
HSPLorg/android/spdy/SoInstallMgrSdk;->initSo(Ljava/lang/String;I)Z
HSPLorg/android/spdy/SpdyAgent;->InvlidCharJudge([B[B)V
Lanet/channel/e/a$b;
Lcom/bytedance/alliance/services/impl/c;
...
3
另外,profile_compilation_info_也会影响机器码重排,我们知道系统在通过IO加载文件的时候,一般都是按页维度来加载的(一般等于4KB),热点代码重排在一起,可以减少IO读取的次数,从而提升性能。
odex文件的机器码布局部分由OatWriter
类实现,声明代码如下:
PLcom/bytedance/apm/perf/b/f;->a(Lcom/bytedance/apm/perf/b/f;)Ljava/lang/String;
PLcom/bytedance/bdp/bdpbase/ipc/n$a;->a()Lcom/bytedance/bdp/bdpbase/ipc/n;
HSPLorg/android/spdy/SoInstallMgrSdk;->initSo(Ljava/lang/String;I)Z
HSPLorg/android/spdy/SpdyAgent;->InvlidCharJudge([B[B)V
Lanet/channel/e/a$b;
Lcom/bytedance/alliance/services/impl/c;
...
4
从中可以看到profile_compilation_info_会被OatWriter
类用到,用于生成odex机器码的布局。
具体代码如下:
PLcom/bytedance/apm/perf/b/f;->a(Lcom/bytedance/apm/perf/b/f;)Ljava/lang/String;
PLcom/bytedance/bdp/bdpbase/ipc/n$a;->a()Lcom/bytedance/bdp/bdpbase/ipc/n;
HSPLorg/android/spdy/SoInstallMgrSdk;->initSo(Ljava/lang/String;I)Z
HSPLorg/android/spdy/SpdyAgent;->InvlidCharJudge([B[B)V
Lanet/channel/e/a$b;
Lcom/bytedance/alliance/services/impl/c;
...
5
在LayoutCodeMethodVisitor类中,根据profile_compilation_info_指定的热点方法的FLAG,判断是否打开hotness_bits标志位。热点方法会一起被重排在odex文件靠前的位置。
小结一下,在系统安装app阶段,会读取apk中baselineprofile文件,经过porfman根据当前系统版本做一定转换并序列化到本地的reference_profile路径下,再通过dexoat编译热点方法为本地机器码并通过代码重排提升性能。
厂商合作
Baseline Profile安装时优化需要Google Play支持,但国内手机由于没有Google Play,无法在安装期做实现优化效果。为此,我们协同抖音与小米、华为等主流厂商建立了合作,共同推进Baseline Profile安装时优化在国内环境的落地。具体的合作方式是:
我们通过编译期改造,提供带Baseline Profile的APK给到厂商验证联调。 厂商具体的优化策略会综合考量安装时长、dex2oat消耗资源情况而定,比如先用默认策略安装apk,再后台异步执行Baseline Profile编译。 最后通过Google提供的初步显示所用时间 (TTID) 来验证优化效果(TTID指标用于测量应用生成第一帧所用的时间,包括进程初始化、activity 创建以及显示第一帧。)
在与厂商联调的过程中,我们解决了各种问题,其中包括有一个资源压缩方式错误。具体错误信息如下:
PLcom/bytedance/apm/perf/b/f;->a(Lcom/bytedance/apm/perf/b/f;)Ljava/lang/String;
PLcom/bytedance/bdp/bdpbase/ipc/n$a;->a()Lcom/bytedance/bdp/bdpbase/ipc/n;
HSPLorg/android/spdy/SoInstallMgrSdk;->initSo(Ljava/lang/String;I)Z
HSPLorg/android/spdy/SpdyAgent;->InvlidCharJudge([B[B)V
Lanet/channel/e/a$b;
Lcom/bytedance/alliance/services/impl/c;
...
6
原来安卓系统要求apk内的baseline.prof二进制是不压缩格式的。我们可以用unzip -v来检验文件是否未被压缩,Defl标志表示压缩,Stored标志表示未压缩。
我们可以在打包流程中指定其为STORED格式,即不压缩。
PLcom/bytedance/apm/perf/b/f;->a(Lcom/bytedance/apm/perf/b/f;)Ljava/lang/String;
PLcom/bytedance/bdp/bdpbase/ipc/n$a;->a()Lcom/bytedance/bdp/bdpbase/ipc/n;
HSPLorg/android/spdy/SoInstallMgrSdk;->initSo(Ljava/lang/String;I)Z
HSPLorg/android/spdy/SpdyAgent;->InvlidCharJudge([B[B)V
Lanet/channel/e/a$b;
Lcom/bytedance/alliance/services/impl/c;
...
7
改完之后我们再检查一下文件是否被压缩。
baseline.prof二进制是不压缩对包体积影响比较小,因为这个文件大部分都是int类型的methodid。经测试,7万+热点方法文件,生成baseline.prof二进制文件62KB,压缩率只有0.1%;如果通过通配符配置,压缩率在5%左右。
一般应用商店下载安装包时在网络传输过程中做了(压缩)https://zh.wikipedia.org/wiki/HTTP%E5%8E%8B%E7%BC%A9处理,这种情况不压缩处理基本不影响包大小,同时不压缩处理也能避免解压缩带来的耗时。
优化效果
在自测中,我们可以通过下面的方式通过install-multiple
命令安装APK。
PLcom/bytedance/apm/perf/b/f;->a(Lcom/bytedance/apm/perf/b/f;)Ljava/lang/String;
PLcom/bytedance/bdp/bdpbase/ipc/n$a;->a()Lcom/bytedance/bdp/bdpbase/ipc/n;
HSPLorg/android/spdy/SoInstallMgrSdk;->initSo(Ljava/lang/String;I)Z
HSPLorg/android/spdy/SpdyAgent;->InvlidCharJudge([B[B)V
Lanet/channel/e/a$b;
Lcom/bytedance/alliance/services/impl/c;
...
8
在厂商测试中通过下面的命令测试冷启动耗时
PLcom/bytedance/apm/perf/b/f;->a(Lcom/bytedance/apm/perf/b/f;)Ljava/lang/String;
PLcom/bytedance/bdp/bdpbase/ipc/n$a;->a()Lcom/bytedance/bdp/bdpbase/ipc/n;
HSPLorg/android/spdy/SoInstallMgrSdk;->initSo(Ljava/lang/String;I)Z
HSPLorg/android/spdy/SpdyAgent;->InvlidCharJudge([B[B)V
Lanet/channel/e/a$b;
Lcom/bytedance/alliance/services/impl/c;
...
9
冷启动Activity耗时比较 | 未优化 | 已优化 | 优化率 |
---|---|---|---|
荣耀Android11 | 950ms | 884ms | 6.9% |
小米Android13 | 821ms | 720ms | 12.3% |
可以看到,在开启Baseline Profile优化之后,首装冷启动(TTID)耗时减少约10%左右,为新用户的启动速度体验带来了极大的提升。
参考文章
团队介绍
我们是字节跳动西瓜视频客户端团队,专注于西瓜视频 App 的开发和基础技术建设,在客户端架构、性能、稳定性、编译构建、研发工具等方向都有投入。如果你也想一起攻克技术难题,迎接更大的技术挑战,欢迎点击阅读原文,或者投递简历到[email protected]。
最 Nice 的工作氛围和成长机会,福利与机遇多多,在北上杭三地均有职位,欢迎加入西瓜视频客户端团队 !
点击「阅读原文」一键投递!
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...