往期文章:

《00. 文章合集目录》

《09. 图解协程原理》

《10. 揭秘 Compose 原理》

你好,我是朱涛。这是「沉思录」的第三篇文章。

今天我们来扒一下 Baseline Profiles 的底层原理。

Android 强推的 Baseline Profiles 国内能用吗?我找 Google 工程师求证了!

正文

今年 Google I/O 大会上,Android 官方强推了一把 Baseline Profile,不仅在 Android、Jetpack 的主题携程电话演讲里有提到了它,就连 Jetpack Compose、Android Studio 相关的主题里也有它的身影。

Android 强推的 Baseline Profiles 国内能用吗?我找 Google 工程师求证了!

第一眼,我就被它给惊艳到了!动辄 30%、40% 的启动优化成绩,还是一个虚拟机的危害通用的解决方案,真的很牛逼了!而且 App 越复杂,提升明显!说实线程池话,刚开始我甚至有点不太相信。

国内能用吗?

在官方介绍 Baselin线程撕裂者e Profandroidstudio怎么设置中文ile 的时候,放了一张这样的图,貌似 Google Play Service 在中androidstudio线性布局间扮演着重要的角色。

Android 强推的 Baseline Profiles 国内能用吗?我找 Google 工程师求证了!

Google Play??我心里顿时就凉了半截。完了线程数是什么!这么牛逼的东西,国内不能用线程数是什么吗? 吓得我赶紧找来了文档,仔细看了一遍 Baseline Profile 的用法虚拟机安装教程以及原理,这才放线程数越多越好吗下心来:

国内能用 BAndroid+Studioaseline Profiles,只是 Cloud Profiles 不可用而已

为了保险起见,我也在 Twitter 上找了 Google 工程师,对方也证实了我的想法。

Android 强推的 Baseline Profiles 国内能用吗?我找 Google 工程师求证了!

那就没毛病了!学起来!

底层携程电话原理

其实吧,Baseline Profile 并不是一个新的东西。而且它也不是一个 Jetpack Library,它只是存在于 Android 系统的一个文件。

这里,我们要从 Android 系统的发展说起。

  • 对于 Android 5.0、6.0 系统来说,我们的代码会在安装期间进行全量 AOT 编译。虽然 AOT 的性能更高,但它会带来额外的问题:应用安装时间大大增加、磁盘占用更加大。
  • 对于 Android 7.0+ 系统来说,Android 支持 JIT、AOT 并存的混合编译模式。在这些高版本的系统当中,ART 虚拟机会在运行时统计到应用的热点代码,存放在/data/m携程旅行app官方下载isc/profiles/cur/0/包名/primary.手机性能优化prof这个路径下。ART 虚拟机虚拟机会针对这些热点代码进行 AOT 编译,这种方式要比全量 AOT 编译灵活很多。

看到这里,你是不是已经猜到了 Baseline Profile 的底层原理了呢?

Android 强推的 Baseline Profiles 国内能用吗?我找 Google 工程师求证了!

不难发现,对吧?由于 ART 虚拟机需要执行一段时间线程撕裂者以后,才能统计出热点代码,而且由于每个用户的使用场景、时长不一样,最终统计出来的热点代码也不一定是最优的。

Google 的思路其实也很简单:让开虚拟机下载发者把热点代码提前统计好,然后跟着代码一起打到 APK 当中,然后将对应的规则存到/data/misc/profiles/cur/0/androidstudio怎么设置中文个目录下即可。总的来说,就是分成两步:1. 统计热点代码的规则;2. 将规则存到特定目录下。

统计热点代码

Baseline Profile 其实就是一个文件,它里面会记录我们应用的热点代码,最终被放在 APK 的 assets/dexopt/baseline.pandroidstudio怎么设置中文rof 目录下。有了它,ART 虚拟机就可以进行相应的androidstudio新建项目 AOT 编译了。

Android 强推的 Baseline Profiles 国内能用吗?我找 Google 工程师求证了!

虽然,我们也可以往 Baseline Profile 当中手动添加对应的方法,但 Google 更加推荐我们使用 Jetpack 当中的 Macrobenchmark。它是 AndroiAndroid+Studiod 里的一个性能优化库,借助这个库,我们可以:生成Baseline Profile文件

@ExperimentalBaselineProfilesApi
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
    @get:Rule val baselineProfileRule = BaselineProfileRule()
    @Test
    fun startup() =
        baselineProfileRule.collectBaselineProfile(packageName = "com.example.app") {
            pressHome()
            // This block defines the app's critical user journey. Here we are interested in
            // optimizing for app startup. But you can also navigate and scroll
            // through your most important UI.
            startActivityAndWait()
        }
}

唯一需要注意的,就是我们需要在 root 过后的 AOSP 9.0+ 的系统上才能采集到热点代码的信息。最终,Macrobenchmark 会把统计到的热点代码信息放到文件里。

/storage/emulated/0/Android/media/package.name.SampleStartupBenchmark_startup-baseline-prof.txt

我们拿到这个统计的文件,将其重命名为ba线程池面试题seline-prof.txt,放到工程里去即可。

写入 baseline.prof

经过前面的分析,我们知道,baseline虚拟机手机版.prof 需要写入到系统特定的目录下,才能够引导 AOT 编译。这一点又是如何做到的呢?

这时候,我们需要用到另一个 Jetpack Li虚拟机下载brary:ProfileInstaller。从它的名字,我们就能看出,它的功能就是:将 APK 当中的 baseline.prof 写入到系统目录下

它的用法也很简单线程池

implementation "androidx.profileinstaller:profileinstaller:1.2.0-beta02"

引入携程网站官网依赖,这没什么好说的线程数越多越好吗,常规操作。然后就是初始化设置。


<provider
   android:name="androidx.startup.InitializationProvider"
   android:authorities="${applicationId}.androidx-startup"
   android:exported="false"
   tools:node="merge">
   <meta-data android:name="androidx.profileinstaller.ProfileInstallerInitializer"
             tools:node="remove" />
</provider>

可以看到,它是通过集成 androidx.startup 库,实现的初始虚拟机的危害化,用的是 Content Provider 的思路,也是常规操作了。我们来分析一下源代androidstudio模拟器运行不出来线程池面试题吧!

总的来说,ProfileInstaller 的代码结构很简单:

Android 强推的 Baseline Profiles 国内能用吗?我找 Google 工程师求证了!

通过前面 XML 的分析,我们知道,ProfileInstallerInitializer 肯定是功能的入口,我们来看它的逻辑。

public class ProfileInstallerInitializer
        implements Initializer<ProfileInstallerInitializer.Result> {
    private static final int DELAY_MS = 5_000;
    @NonNull
    @Override
    public Result create(@NonNull Context context) {
        if (Build.VERSION.SDK_INT < ProfileVersion.MIN_SUPPORTED_SDK) {
            // 小于 7.0 的系统没必要执行
            return new Result();
        }
        // 延迟 5 秒,写入 profile 文件
        delayAfterFirstFrame(context.getApplicationContext());
            return new Result();
        }
    }
}

接着,我们来看看androidstudio线性布局 Delay 是如何实现的:

@RequiresApi(16)
void delayAfterFirstFrame(@NonNull Context appContext) {
    // 从第一帧开始算,延迟 5 秒
    Choreographer16Impl.postFrameCallback(() -> installAfterDelay(appContext));
}
void installAfterDelay(@NonNull Context appContext) {
    Handler handler;
    if (Build.VERSION.SDK_INT >= 28) {
        handler = Handler28Impl.createAsync(Looper.getMainLooper());
    } else {
        handler = new Handler(Looper.getMainLooper());
    }
    Random random = new Random();
    int extra = random.nextInt(Math.max(DELAY_MS / 5, 1));
    // Handler 实现 delay
    handler.postDelayed(() -> writeInBackground(appContext), DELAY_MS + extra);
}

可以看到,为了避免 Profile 的写入影响到 App 的正常执行,这里延迟了携程网站官网 5 秒左右。最终,会执行writeInBackground(),进行真正的写入操作。

private static void writeInBackground(@NonNull Context context) {
    Executor executor = new ThreadPoolExecutor(
            /* corePoolSize = */0,
            /* maximumPoolSize = */1,
            /* keepAliveTime = */0,
            /* unit = */TimeUnit.MILLISECONDS,
            /* workQueue = */new LinkedBlockingQueue<>()
    );
    executor.execute(() -> ProfileInstaller.writeProfile(context));
}

这里,程序会创建一个线程数量为 1 的线程池,然后将执行流程交给 ProfileInstaller,进行 Profile 文件的写入。

static void writeProfile(
        @NonNull Context context,
        @NonNull Executor executor,
        @NonNull DiagnosticsCallback diagnostics,
        boolean forceWriteProfile
) {
    Context appContext = context.getApplicationContext();
    String packageName = appContext.getPackageName();
    ApplicationInfo appInfo = appContext.getApplicationInfo();
    AssetManager assetManager = appContext.getAssets();
    String apkName = new File(appInfo.sourceDir).getName();
    PackageManager packageManager = context.getPackageManager();
    PackageInfo packageInfo;
    try {
        packageInfo = packageManager.getPackageInfo(packageName, 0);
    } catch (PackageManager.NameNotFoundException e) {
        diagnostics.onResultReceived(RESULT_IO_EXCEPTION, e);
        return;
    }
    File filesDir = context.getFilesDir();
    // 判断是否要写入
    if (forceWriteProfile
            || !hasAlreadyWrittenProfileForThisInstall(packageInfo, filesDir, diagnostics)) {
        transcodeAndWrite(assetManager, packageName, packageInfo, filesDir, apkName, executor,
                diagnostics);
    }
}

writeProfile()的主要逻辑就是判断当前是否要强制写入 Profile 文件(正常情况是不强制的),以及之前是否已经写入过了。之后,程序会执行transcodeAndWrite()方法,也就是携程网上订票火车票码并写入

终于到关键逻辑了!我们来androidstudio模拟器运行不出来看看它的逻辑。

private static void transcodeAndWrite(
        @NonNull AssetManager assets,
        @NonNull String packageName,
        @NonNull PackageInfo packageInfo,
        @NonNull File filesDir,
        @NonNull String apkName,
        @NonNull Executor executor,
        @NonNull DiagnosticsCallback diagnostics
) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
        result(executor, diagnostics, ProfileInstaller.RESULT_UNSUPPORTED_ART_VERSION, null);
        return;
    }
    File curProfile = new File(new File(PROFILE_BASE_DIR, packageName), PROFILE_FILE);
    DeviceProfileWriter deviceProfileWriter = new DeviceProfileWriter(assets, executor,
            diagnostics, apkName, PROFILE_SOURCE_LOCATION, PROFILE_META_LOCATION, curProfile);
    // 是否具备写入权限
    if (!deviceProfileWriter.deviceAllowsProfileInstallerAotWrites()) {
        return; /* nothing else to do here */
    }
    boolean success = deviceProfileWriter.read()
            .transcodeIfNeeded()
            .write();
    if (success) {
        noteProfileWrittenFor(packageInfo, filesDir);
    }
}
public boolean deviceAllowsProfileInstallerAotWrites() {
    if (mDesiredVersion == null) {
        result(ProfileInstaller.RESULT_UNSUPPORTED_ART_VERSION, Build.VERSION.SDK_INT);
        return false;
    }
    if (!mCurProfile.canWrite()) {
        // 某些厂商可能不允许写入 Profile 文件
        result(ProfileInstaller.RESULT_NOT_WRITABLE, null);
        return false;
    }
    mDeviceSupportsAotProfile = true;
    return true;
}

从上面的注释,我们可以看到,transcodeAndWrite()主要还是在判断当前设备是否支持写入 Profile 文件,如果支持才会继续。

至此,我虚拟机的危害们整个 Basel性能优化的方法ine Profile 的技术方案就分js性能优化析完了!

Android 强推的 Baseline Profiles 国内能用吗?我找 Google 工程师求证了!

注意事项

在研究 Baseline Profiles 的过程中,我androidstudio打包apk也发现了一些小细节,可能需要大家额外留意。

  • 第一,由于 Android 手机有许多的厂商,每个厂商会对系统进行一些定制化,也许某些厂商会封死 Profile 文件的写入权限。即使这个方案无需 Google Play,但国内支持写入 Profile 的手机具体占多大的比例,我目前还没有数据,欢迎大家在使用了 Basel页面性能优化i性能优化ne Profile 以后来向我反馈。
  • 第二,如何衡量 Baseline Profile 带来的性能提升?这一点虚拟机安装教程win10, Macrobenchmar性能优化的方法k 也提供了相关的能力,具体可以看这个官方文档的链接。
  • 第三,Debug 编译的 App,是不会进行 AOT 编译的,因此它的性能会比手机性能优化 release 低不少。
  • 第四,baseline-prof.txt放的位置很关键,它必须跟AndroidManifest.xml是同级目录下。
  • 第五,Baseline Profile 必须使用 AGP 7.1.0-alpha05 及以上的版本,7.3.0-beta01及以携程旅行app官方下载上对 App Bundle、多 Dex 应用的支持会更好。
  • 第六,baseline-prof.txt 文件大小不得超过 1.5M,且,其中定义的规则不能太宽泛,否则可能反而降低性携程线程的几种状态

一个有趣的故事

这个故事具体的来Android+Studio源是谁,我忘了,反正是某个 Google 工程师说的。关于,Baseline Profandroidstudio怎么设置中文ile 是如何诞生的。

其实,它跟 Jetpack Compose 还有一些渊源。Compose 由于它的底层原理,它的核心代码是会频率调用的,因此对性能要求非常高。

在 Google 内部研发 Jetpack Compose 的过程中,他们发现:Compose 应用在初次安装、启动的阶段,会非常的卡!等到应用使用一段时间后,Compose 应用的体验才会慢慢好起来。

页面性能优化是为什么呢?

你肯定能猜到,对吧?没错!因为 ART 默认情况下,并没有把 Compose 的核心代码进行 AOT 编译,而是 JIT 执行。这就要命了,像 Compose 底层的 Snapshot 系统、Slot Table,都是协程热点代码,短时间内会被频繁调用,JIT 根本无法满足 Compose 的性能要求

怎么办呢?当然是 Baseline Profile 啦!其实,这套方案,早在 2021 年就被率先引入 Jetp虚拟机怎么使用ack Compose 当中。今年 2022 年的 Googl汽车性能优化e I/O 大会上,才被官方拿出来大力推广。

感谢 Android 团队,让我们开发者拥有了一个新的角度,来优化应用的性能

OK,感谢你的阅读,咱们下携程网上订票飞机周……额……我也不知道啥时候能写出下一篇,总之,下次再见!