天天看點

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 應用的體驗才會慢慢好起來。

這是為什麼呢?