天天看点

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

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

今天我们来扒一下 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 越复杂,提升明显!说实话,刚开始我甚至有点不太相信。

国内能用吗?

在官方介绍 Baseline Profile 的时候,放了一张这样的图,貌似 Google Play Service 在中间扮演着重要的角色。

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

Google Play??我心里顿时就凉了半截。完了!这么牛逼的东西,国内不能用吗? 吓得我赶紧找来了文档,仔细看了一遍 Baseline Profile 的用法以及原理,这才放下心来:

国内能用 Baseline 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/misc/profiles/cur/0/包名/primary.prof​

    ​这个路径下。ART 虚拟机会针对这些热点代码进行 AOT 编译,这种方式要比全量 AOT 编译灵活很多。

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

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

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

Google 的思路其实也很简单:让开发者把热点代码提前统计好,然后跟着代码一起打到 APK 当中,然后将对应的规则存到​

​/data/misc/profiles/cur/0/​

​这个目录下即可。总的来说,就是分成两步:1. 统计热点代码的规则;2. 将规则存到特定目录下。

统计热点代码

Baseline Profile 其实就是一个文件,它里面会记录我们应用的热点代码,最终被放在 APK 的 ​

​assets/dexopt/baseline.prof​

​ 目录下。有了它,ART 虚拟机就可以进行相应的 AOT 编译了。

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

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

​生成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.      

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

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

我们拿到这个统计的文件,将其重命名为​

​baseline-prof.txt​

​,放到工程里去即可。

写入 baseline.prof

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

这时候,我们需要用到另一个 Jetpack Library:​​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 的思路,也是常规操作了。我们来分析一下源代码吧!

总的来说,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 {
        if (Build.VERSION.SDK_INT < ProfileVersion.MIN_SUPPORTED_SDK) {
            // 小于 7.0 的系统没必要执行
            return new Result();
        }
        // 延迟 5 秒,写入 profile 文件
        delayAfterFirstFrame(context.getApplicationContext());
            return new Result();
        }
    }
}      

接着,我们来看看 Delay 是如何实现的:

@RequiresApi(16)
void delayAfterFirstFrame(@NonNull {
    // 从第一帧开始算,延迟 5 秒
    Choreographer16Impl.postFrameCallback(() -> installAfterDelay(appContext));
}

void installAfterDelay(@NonNull {
    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      

可以看到,为了避免 Profile 的写入影响到 App 的正常执行,这里延迟了 5 秒左右。最终,会执行​

​writeInBackground()​

​,进行真正的写入操作。

private static void writeInBackground(@NonNull {
    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 {
    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      

​writeProfile()​

​​的主要逻辑就是判断当前是否要强制写入 Profile 文件(正常情况是不强制的),以及之前是否已经写入过了。之后,程序会执行​

​transcodeAndWrite()​

​​方法,也就是​

​转码并写入​

​。

终于到关键逻辑了!我们来看看它的逻辑。

private static void transcodeAndWrite(
        @NonNull AssetManager assets,
        @NonNull String packageName,
        @NonNull PackageInfo packageInfo,
        @NonNull File filesDir,
        @NonNull String apkName,
        @NonNull Executor executor,
        @NonNull {
    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 文件,如果支持才会继续。

至此,我们整个 Baseline Profile 的技术方案就分析完了!

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

注意事项

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

  • 第一,由于 Android 手机有许多的厂商,每个厂商会对系统进行一些定制化,也许某些厂商会封死 Profile 文件的写入权限。即使这个方案无需 Google Play,但国内支持写入 Profile 的手机具体占多大的比例,我目前还没有数据,欢迎大家在使用了 Baseline Profile 以后来向我反馈。
  • 第二,如何衡量 Baseline Profile 带来的性能提升?这一点, Macrobenchmark 也提供了相关的能力,具体可以看这个官方文档的​​链接​​。
  • 第三,Debug 编译的 App,是不会进行 AOT 编译的,因此它的性能会比 release 低不少。
  • 第四,​

    ​baseline-prof.txt​

    ​​放的位置很关键,它必须跟​

    ​AndroidManifest.xml​

    ​是同级目录下。
  • 第五,Baseline Profile 必须使用​

    ​AGP 7.1.0-alpha05​

    ​​ 及以上的版本,​

    ​7.3.0-beta01​

    ​及以上对 App Bundle、多 Dex 应用的支持会更好。
  • 第六,​

    ​baseline-prof.txt​

    ​ 文件大小不得超过 1.5M,且,其中定义的规则不能太宽泛,否则可能反而降低性能。

一个有趣的故事

这个故事具体的来源是谁,我忘了,反正是某个 Google 工程师说的。关于,Baseline Profile 是如何诞生的。

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

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

这是为什么呢?