服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

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

日期: 来源:玉刚说收集编辑:朱涛

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

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

1正文

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

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

2国内能用吗?

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

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

国内能用 Baseline Profiles,只是 Cloud Profiles 不可用而已

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

那就没毛病了!学起来!

3底层原理

其实吧,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 的底层原理了呢?

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

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

统计热点代码

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

虽然,我们也可以往 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.
            startActivityAndWait()
        }
}

唯一需要注意的,就是我们需要在 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 的代码结构很简单:

通过前面 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();
        }
    }
}

接着,我们来看看 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()方法,也就是转码并写入

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

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 文件,如果支持才会继续。

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

4注意事项

在研究 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,且,其中定义的规则不能太宽泛,否则可能反而降低性能。

5一个有趣的故事

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

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

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

这是为什么呢?

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

怎么办呢?当然是 Baseline Profile 啦!其实,这套方案,早在 2021 年就被率先引入 Jetpack Compose 当中。今年 2022 年的 Google I/O 大会上,才被官方拿出来大力推广。

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

6结束语

对了,最近有读者私信跟我说:经常收不到公众号的更新推送。我研究了一下,像我这种一个月不更新一次的「辣鸡」号,权重是很低的,貌似要“设为星标”才行。

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

相关阅读

  • flutter:一个bug的源码分析

  • 由一个bug引发的flutter的widget跟element关系的源码分析bug现象在页面本来有照片数据的(第一张照片数据),点击加号唤起系统拍照功能后,再返回页面A,原来的照片数据丢失了(部分And
  • Android 技术面试如何做好准备?

  • 这是 JsonChao 的第 210 期分享超友们,早上好,中秋节快乐~今天的干货来点轻松一点的,这次的分享是《技术面试如何做好准备?》,主要分为三个部分:第一部分:面试前。第二部分:面试中。
  • 芳芳频道|李艳芳三套卷复盘(数三①)

  • 往期直达数一复盘(已完结)数二复盘(已完结)李艳芳三套卷划重点数三第一套总体作为数三的第一套,自我感觉还是比较恰当的,整体计算量不大,概率题都很常规,较好得分,高数题和线代题不乏
  • 我会这样日常用ChatGPT练英语

  • 前两天写了篇《用ChatGPT学英语指南》,今天再做两点补充,希望我的学习思路对你也有启发。1.向 AI 提问社群里有位老师朋友提了个问题:第 48 题可以选 C 吗?我的结论是选 played
  • 新文速递 - 9.22/9.29

  • 纳米粒子修饰的微型机器人用于治疗急性细菌性肺炎的体内抗生素递送Biohybrid microrobots consisting of nanoparticle-modified microalgae are constructed for active dr
  • 新文速递:11.28-12.2

  • 基于二甲基铵阳离子添加剂的中间相工程用于稳定钙钛矿太阳能电池The stability of halide perovskite solar cells, determined by film morphology, is paramount to their

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • flutter:一个bug的源码分析

  • 由一个bug引发的flutter的widget跟element关系的源码分析bug现象在页面本来有照片数据的(第一张照片数据),点击加号唤起系统拍照功能后,再返回页面A,原来的照片数据丢失了(部分And
  • 美团招Android技术专家

  • 大家好,美团招人了,各种级别,欢迎大家投递简历。下面的条件不需要全部满足,能满足若干个就行,因为全部满足的人灰常少!美团App-Android开发专家岗位基本需求1. 扎实的移动端研发能
  • Android 技术面试如何做好准备?

  • 这是 JsonChao 的第 210 期分享超友们,早上好,中秋节快乐~今天的干货来点轻松一点的,这次的分享是《技术面试如何做好准备?》,主要分为三个部分:第一部分:面试前。第二部分:面试中。
  • 漫画:国内都有哪些程序员大牛?

  • 第一位,求伯君求伯君,是一位生长在浙江绍兴的小哥哥。在上世纪80年代末,求伯君毅然加入了当时还名不见经传的香港金山公司,从事一款办公软件的开发。一年后,这款办公软件正式问世