基於 Android App Bundle 動態化框架 Qigsaw 源碼分析系列(一)

Qigsaw 是愛奇藝提供的一套基於 Android App Bundle 的動態化方案,無需谷歌 Play Service 便可在國內體驗 Android App Bundle開發工具。它支持動態下發插件 APK,讓應用可以在不從新安裝的狀況下實現動態安裝插件。java

路過請留下您的star,謝謝!android

目前 Qigsaw 在愛奇藝 App 已上線快滿一年,在這一年期間愛奇藝 App 總共上線 8 個插件,包括百度小程序框架、愛奇藝小遊戲框架、泡泡、彈幕等。集團其餘業務線共有四款獨立 App 成功上線 Qigsaw,包括愛奇藝極速版。git

從 Qigsaw 開源至今收穫了很多朋友的確定,爲了更好回饋你們,我將開闢 Qigsaw 源碼分析系列文章,讓你們進一步瞭解 Qigsaw。github

本系列將以已下章節分別介紹。shell

  • Android App Bundles 簡介小程序

    • Android App Bundles 初窺門徑
    • Android App Bundles 加載原理
  • Qigsaw 打包插件流程分析安全

  • Qigsaw 插件代碼加載詳解bash

  • Qigsaw 插件四大組件啓動分析markdown

  • Qigsaw 插件資源加載詳解session

  • Qigsaw 插件熱更新介紹

  • Qigsaw 兼容性問題概述

目前國內應用開發者比較少接觸 Android App Bundles 開發,所以在介紹 Qigsaw 以前,咱們首先來了解下 Android App Bundles 相關知識。

Android App Bundles 初窺門徑

Android App Bundles 是 Google 於 2018 年推出的全新應用分發方式,它核心目的是協助應用減小安裝體積。依據最新 Android Dev Summit 2019 相關介紹,目前全球已有超過 25 萬應用採用 App Bundles 方式分發應用而且提高了 25% 安裝率。所以,國內一些專一海外市場的公司能夠嘗試 App Bundles。

出於某些安全因素考慮,大部分廠商還處於觀望狀態,畢竟使用 App Bundles 方案,您須要將 App 簽名上傳至 Play Console。

Android App Bundles 核心功能之一是動態分發即 dynamic features,本文將重點介紹 dynamic features 工做原理。首先請請前往 DynamicFeatures 示例地址並下載體驗。

在 DynamicFeatures 項目目錄的 features 文件夾下有五個 dynamic features 模塊。

apply plugin: 'com.android.dynamic-feature'

android {

    compileSdkVersion versions.compileSdk

    defaultConfig {
        minSdkVersion versions.minSdk
        targetSdkVersion versions.targetSdk
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
}

dependencies {

    implementation project(':app')

    androidTestImplementation "androidx.test.espresso:espresso-contrib:${versions.espresso}"
    androidTestImplementation "androidx.test:rules:${versions.testRules}"
    androidTestImplementation "androidx.test.ext:junit:${versions.extJunit}"

    // When using API in base, some dependencies might have to be re-added for test implementation.
    androidTestImplementation "androidx.appcompat:appcompat:${versions.appcompat}"


}
複製代碼

上述代碼出自 java 模塊 build.gradle 文件,java 模塊使用的 gradle plugin 是 com.android.dynamic-feature,該插件對應 Android Gradle Plugin 源碼類是 DynamicFeaturePlugin。

上圖是 Android 打包插件關係類圖,

  1. LibraryPlugin 對應 com.android.library,其編譯產物格式爲aar。
  2. InstantAppPlugin 對應 com.android.instantapp,其編譯產物格式爲zip。
  3. FeaturePlugin 對應 com.android.feature,其編譯產物格式爲apk。
  4. AppPlugin 對應 com.android.application,其編譯產物格式爲apk。
  5. DynamicFeaturePlugin 對應 com.android.dynamic-feature,其編譯產物格式爲apk。

InstantAppPlugin 和 FeaturePlugin 兩個插件爲 Instant Apps 開發使用。

DynamicFeaturePlugin 與 AppPlugin 均繼承自 AbstractAppPlugin,所以它們之間有不少共性之處。

爲了更清晰理解 dynamic feature 工做原理,咱們點擊 Run 'app' 按鈕運行 sample 。點擊 Android Studio 右下角 Event Log 按鈕查看執行的任務。

Executing tasks: [
:features:java:assembleDebug, 
:features:initialInstall:assembleDebug, 
:instant:url:assembleDebug, 
:instant:split:assembleDebug, 
:features:kotlin:assembleDebug,
:app:assembleDebug, 
:features:assets:assembleDebug, 
:features:native:assembleDebug
] in project ......
複製代碼

從該任務中能夠看出除了app模塊 assemble 任務被執行外,全部 dynamic feature 模塊 assemble 任務也被執行。

另外須要注意,在點擊 Run 'app' 後並非總會執行上述任務。

點擊上圖 Edit Configurations 按鈕,進入配置頁面。

Deploy 方式選擇 APK from app bundle,接着再次 Run 'app',Event Log 顯示的執行任務以下。

Deploy 默認是 Default APK 方式。

Executing tasks: [:app:extractApksForDebug] in project .....
複製代碼

依據 Deploy 方式不一樣,Run 'app' 後執行的任務不一樣。

那麼 Default APK 和 APK from app bundle 兩種 Deploy 方式有何不一樣呢?

使用 Default APK 方式啓動 sample app,點擊 START KOTLIN FEATURE 按鈕,正常啓動 dynamic-feature kotlin 的KotlinSampleActivity。

使用 APK from app bundle 方式啓動 sample app,點擊 START KOTLIN FEATURE 按鈕,停留在 kotlin 正在啓動畫面,KotlinSampleActivity 未正常啓動。

Android Studio 3.4+ 開始,應用安裝命令再也不輸出。爲弄明白二者區別,需下載 Android Studio 3.2+ 或 3.3+ 版本,本文以 3.3.2 爲例。官方 sample 工程若是直接修改 Android Gradle Plugin 版本號至 3.3.2,會編譯異常。所以,我本地寫了一個 demo,只包含一個 dynamic feature 模塊 java。

首先查看 Default APK 方式安裝日誌,點擊Android Studio 左下底角 Run 按鈕。

10/30 15:11:54: Launching app
$ adb install-multiple -r -t 
/Users/kissonchen/Dev/AABSample/app/build/outputs/apk/debug/app-debug.apk 
/Users/kissonchen/Dev/AABSample/features/java/build/outputs/apk/debug/java-debug.apk 
Split APKs installed in 2 s 495 ms
......
複製代碼

經過日誌,清晰看到該方式採用 adb install-multiple 命令安裝了四個 apk,除了應用自身的 app-debug.apk,還有三個 dynamic-feature 生成的三個 apk。

測試中發現 vivo 和 oppo 手機不支持多 apk 安裝。

接着,再觀察 APK from app bundle 方式安裝日誌。

10/30 15:24:14: Launching app
$ adb install-multiple -r -t 
/Users/kissonchen/Dev/AABSample/app/build/intermediates/extracted_apks/debug/extractApksForDebug/out/base-armeabi_v7a_2.apk 
/Users/kissonchen/Dev/AABSample/app/build/intermediates/extracted_apks/debug/extractApksForDebug/out/base-master_2.apk 
/Users/kissonchen/Dev/AABSample/app/build/intermediates/extracted_apks/debug/extractApksForDebug/out/base-zh.apk 
/Users/kissonchen/Dev/AABSample/app/build/intermediates/extracted_apks/debug/extractApksForDebug/out/base-xxhdpi.apk 
Split APKs installed in 1 s 912 ms
......
複製代碼

經過日誌,能夠看到該方式採起用 adb install-multiple 命令安裝了四個 apk,這四個 apk 能夠理解爲是 app 模塊編譯產物 app-debug.apk 基於當前設備配置來拆分的。好比 base-armeabi_v7a_2.apk 只包含適配當前設備的 Library 文件,base-zh.apk 至包含當前設備語言環境的資源文件等。 須要注意,該種方式 java 模塊相關 apk 並未安裝,這又是爲何呢?

經過 APK from app bundle 方式安裝應用,Android Studio 會依據 dynamic-feature 模塊的 AndroidManifest.xml 中 "onDemand" 配置來決定是否安裝其編譯生成的 apk。這就是前文提到官方 sample 中,kotlin 模塊的 KotlinSampleActivity 沒法啓動的緣由,你能夠嘗試修改 kotlin 模塊 "onDemand" 值爲 false,再次觀察結果。

從 Android 5.0 開始,Android 支持一個應用拆分紅多個APK進行安裝,包括 base apk 和 split apks。使用 adb install-multiple 或 PackageInstaller 能夠完成多 APK 安裝。

split apks 不能單獨安裝。base apk 安裝成功後,split apks 才能安裝。

爲了更好理解 split apks,咱們能夠查看下split apks AndroidManifest.xml文件。

DefaultAPK 安裝方式,查看官方 sample 中 java 模塊編譯產物 java-debug.apk 中 AndroidManifest.xml 文件。

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:dist="http://schemas.android.com/apk/distribution"
    android:versionCode="1"
    android:versionName="1.0"
    android:isFeatureSplit="true"
    android:compileSdkVersion="28"
    android:compileSdkVersionCodename="9"
    package="com.google.android.samples.dynamicfeatures.ondemand"
    platformBuildVersionCode="28"
    platformBuildVersionName="9"
    split="java">
    
......
    
</manifest>
    
複製代碼

上述內容有兩個屬性記錄該 split 的名字和類型。

android:isFeatureSplit 說明該 apk 是 dynamic-feature 的產物。 split="java" 指明該split 名稱爲 java。 package="com.google.android.samples.dynamicfeatures.ondemand" 內容與 app-debug 記錄的包名一致。

APK form app bundle 安裝方式,查看官方 sample 中 app/build/intermediates/extracted_apks/debug/extractApksForDebug/out 路徑下 split apks,選擇 initialInstall-xxhdpi.apk 查看其 AndroidManifest.xml 內容。

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="1"
    configForSplit="initialInstall"
    package="com.google.android.samples.dynamicfeatures.ondemand"
    split="initialInstall.config.xxhdpi">

    <application
        android:hasCode="false" />
</manifest>

複製代碼

configForSplit="initialInstall" 指明該 split 是 initialInstall 的配置 apk。 若是 split 是配置 apk,那麼其名稱會包含 config 關鍵字,同時其 android:hasCode 屬性一直爲false。

Split APKs 安裝

前文提到 Run 'app',兩種不一樣 Deploy 方式均採起 adb install-multiple 命令安裝 base apk 和 split apks。 那麼咱們能夠嘗試手動調用 adb install-multiple 命令來安裝應用。

咱們直接使用 APK form app bundle 這種 Deploy 方式的編譯產物來實踐,切至 app/build/intermediates/extracted_apks/debug/extractApksForDebug/out 目錄下。

選取 base-master.apkbase-xxhdpibase-zh.apk 三個 apk 來安裝。base-master.apk 是 base apk,base-xxhdpibase-zh.apk 是 split apks。

安裝命令以下。

adb install-multiple 
/Users/kissonchen/GitHub/app-bundle-samples/DynamicFeatures/app/build/intermediates/extracted_apks/debug/extractApksForDebug/out/base-master.apk
/Users/kissonchen/GitHub/app-bundle-samples/DynamicFeatures/app/build/intermediates/extracted_apks/debug/extractApksForDebug/out/base-xxhdpi.apk 
/Users/kissonchen/GitHub/app-bundle-samples/DynamicFeatures/app/build/intermediates/extracted_apks/debug/extractApksForDebug/out/base-zh.apk
複製代碼

若是僅安裝 base-xxhdpibase-zh.apk 兩個 apk,能成功安裝嗎?

執行以下命令。

adb install-multiple 
/Users/kissonchen/GitHub/app-bundle-samples/DynamicFeatures/app/build/intermediates/extracted_apks/debug/extractApksForDebug/out/base-xxhdpi.apk 
/Users/kissonchen/GitHub/app-bundle-samples/DynamicFeatures/app/build/intermediates/extracted_apks/debug/extractApksForDebug/out/base-zh.apk
複製代碼

運行結果以下。

adb: failed to finalize session
Failure [INSTALL_FAILED_INVALID_APK: Full install must include a base package]
複製代碼

經過錯誤日誌可知,多 APK 的安裝必需要保證 base apk 存在。 若是當前設備已經安裝官方 sample base apk。可否經過 adb install-multiple 繼續安裝 split apks呢。

base*.apk 安裝至設備後,繼續安裝 java-master.apkjava-xxhdpi.apk 兩個 split apks。

保持 sample app 前臺運行,執行以下命令。

adb install-multiple -p com.google.android.samples.dynamicfeatures.ondemand  
/Users/kissonchen/GitHub/app-bundle-samples/DynamicFeatures/app/build/intermediates/extracted_apks/debug/extractApksForDebug/out/java-master.apk 
/Users/kissonchen/GitHub/app-bundle-samples/DynamicFeatures/app/build/intermediates/extracted_apks/debug/extractApksForDebug/out/java-xxhdpi.apk

複製代碼

在上述命令執行成功後,你會發現 sample app 被系統「殺死」,重啓 sample 並點擊 START JAVA FEATURE 按鈕,java 模塊的頁面被正常啓動,說明 java split 被成功安裝。

-p 參數表示 partial application install,意思是部分安裝。後面的參數值 com.google.android.samples.dynamicfeatures.ondemand 表示 sample app 包名。

若是不指定包名參數值,會有什麼現象呢?

執行如下命令。

adb install-multiple -p  
/Users/kissonchen/GitHub/app-bundle-samples/DynamicFeatures/app/build/intermediates/extracted_apks/debug/extractApksForDebug/out/java-master.apk 
/Users/kissonchen/GitHub/app-bundle-samples/DynamicFeatures/app/build/intermediates/extracted_apks/debug/extractApksForDebug/out/java-xxhdpi.apk

複製代碼

提示錯誤以下。

java.lang.IllegalArgumentException: Missing inherit package name
        at com.android.server.pm.PackageManagerShellCommand.makeInstallParams(PackageManagerShellCommand.java:2212)
        at com.android.server.pm.PackageManagerShellCommand.runInstallCreate(PackageManagerShellCommand.java:977)
        at com.android.server.pm.PackageManagerShellCommand.onCommand(PackageManagerShellCommand.java:173)
        at android.os.ShellCommand.exec(ShellCommand.java:103)
        at com.android.server.pm.PackageManagerService.onShellCommand(PackageManagerService.java:23384)
        at android.os.Binder.shellCommand(Binder.java:642)
        at android.os.Binder.onTransact(Binder.java:540)
        at android.content.pm.IPackageManager$Stub.onTransact(IPackageManager.java:2804)
        at com.android.server.pm.PackageManagerService.onTransact(PackageManagerService.java:4435)
        at com.android.server.pm.HwPackageManagerService.onTransact(HwPackageManagerService.java:994)
        at android.os.Binder.execTransact(Binder.java:739)

複製代碼

該異常說明未指定需繼承應用的包名,即已經安裝至設備 base apk 的包名。

經過堆棧信息可知,adb install-multiple 命令的實如今 PackageManagerShellCommand 類中。

private InstallParams makeInstallParams() {
2134        final SessionParams sessionParams = new SessionParams(SessionParams.MODE_FULL_INSTALL);
2135        final InstallParams params = new InstallParams();
2136        params.sessionParams = sessionParams;
2137        String opt;
2138        boolean replaceExisting = true;
2139        while ((opt = getNextOption()) != null) {
2140            switch (opt) {
2141                case "-l":
2142                    sessionParams.installFlags |= PackageManager.INSTALL_FORWARD_LOCK;
2143                    break;
2144                case "-r": // ignore
2145                    break;
2146                case "-R":
2147                    replaceExisting = false;
2148                    break;
2149                case "-i":
2150                    params.installerPackageName = getNextArg();
2151                    if (params.installerPackageName == null) {
2152                        throw new IllegalArgumentException("Missing installer package");
2153                    }
2154                    break;
2155                case "-t":
2156                    sessionParams.installFlags |= PackageManager.INSTALL_ALLOW_TEST;
2157                    break;
2158                case "-s":
2159                    sessionParams.installFlags |= PackageManager.INSTALL_EXTERNAL;
2160                    break;
2161                case "-f":
2162                    sessionParams.installFlags |= PackageManager.INSTALL_INTERNAL;
2163                    break;
2164                case "-d":
2165                    sessionParams.installFlags |= PackageManager.INSTALL_ALLOW_DOWNGRADE;
2166                    break;
2167                case "-g":
2168                    sessionParams.installFlags |= PackageManager.INSTALL_GRANT_RUNTIME_PERMISSIONS;
2169                    break;
2170                case "--dont-kill":
2171                    sessionParams.installFlags |= PackageManager.INSTALL_DONT_KILL_APP;
2172                    break;
2173                case "--originating-uri":
2174                    sessionParams.originatingUri = Uri.parse(getNextArg());
2175                    break;
2176                case "--referrer":
2177                    sessionParams.referrerUri = Uri.parse(getNextArg());
2178                    break;
2179                case "-p":
2180                    sessionParams.mode = SessionParams.MODE_INHERIT_EXISTING;
2181                    sessionParams.appPackageName = getNextArg();
2182                    if (sessionParams.appPackageName == null) {
2183                        throw new IllegalArgumentException("Missing inherit package name");
2184                    }
2185                    break;
2186                case "--pkg":
2187                    sessionParams.appPackageName = getNextArg();
2188                    if (sessionParams.appPackageName == null) {
2189                        throw new IllegalArgumentException("Missing package name");
2190                    }
2191                    break;
2192                case "-S":
2193                    final long sizeBytes = Long.parseLong(getNextArg());
2194                    if (sizeBytes <= 0) {
2195                        throw new IllegalArgumentException("Size must be positive");
2196                    }
2197                    sessionParams.setSize(sizeBytes);
2198                    break;
2199                case "--abi":
2200                    sessionParams.abiOverride = checkAbiArgument(getNextArg());
2201                    break;
2202                case "--ephemeral":
2203                case "--instant":
2204                case "--instantapp":
2205                    sessionParams.setInstallAsInstantApp(true /*isInstantApp*/);
2206                    break;
2207                case "--full":
2208                    sessionParams.setInstallAsInstantApp(false /*isInstantApp*/);
2209                    break;
2210                case "--preload":
2211                    sessionParams.setInstallAsVirtualPreload();
2212                    break;
2213                case "--user":
2214                    params.userId = UserHandle.parseUserArg(getNextArgRequired());
2215                    break;
2216                case "--install-location":
2217                    sessionParams.installLocation = Integer.parseInt(getNextArg());
2218                    break;
2219                case "--force-uuid":
2220                    sessionParams.installFlags |= PackageManager.INSTALL_FORCE_VOLUME_UUID;
2221                    sessionParams.volumeUuid = getNextArg();
2222                    if ("internal".equals(sessionParams.volumeUuid)) {
2223                        sessionParams.volumeUuid = null;
2224                    }
2225                    break;
2226                case "--force-sdk":
2227                    sessionParams.installFlags |= PackageManager.INSTALL_FORCE_SDK;
2228                    break;
2229                default:
2230                    throw new IllegalArgumentException("Unknown option " + opt);
2231            }
2232            if (replaceExisting) {
2233                sessionParams.installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
2234            }
2235        }
2236        return params;
2237    }
複製代碼

上述代碼片斷是 makeInstallParams 方法,做用是構造 apk 安裝參數。代碼段中 case "-p" 邏輯中拋出的異常就是致使前文安裝異常的緣由。

代碼片斷截取自 PackageManagerShellCommand

PackageManagerShellCommand 中安裝 apk 邏輯最終也是經過 PackageInstaller 實現。

使用 PackageInstaller 提供的相關接口,便可經過代碼實現 base apk 和 split apks 的安裝。

import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInstaller;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;

public class SplitAPKInstaller {

    private Context appContext;

    private PackageInstaller mPackageInstaller;

    public SplitAPKInstaller(Context context) {
        this.appContext = context;
        this.mPackageInstaller = context.getPackageManager().getPackageInstaller();
    }

    /**
     * 完整安裝模式,必須包含 base apk。
     * 更多詳情參考 {@link android.content.pm.PackageInstaller.SessionParams#MODE_FULL_INSTALL}
     *
     * @param apkPaths apk 路徑列表
     */
    public void fullInstallApks(String[] apkPaths) throws IOException {
        installApk(apkPaths, null);
    }

    /**
     * 繼承安裝模式,用於安裝 split apk,確保 base apk 已經安裝至設備中。
     * 更多詳情參考 {@link android.content.pm.PackageInstaller.SessionParams#MODE_INHERIT_EXISTING}
     *
     * @param apkPaths          apk 路徑列表
     * @param targetPackageName 已安裝 base apk 的包名
     */
    public void inheritInstallApks(String[] apkPaths, String targetPackageName) throws IOException {
        installApk(apkPaths, targetPackageName);
    }

    private void installApk(String[] apkPaths, String targetPackageName) throws IOException {

        Map<String, String> fileNameToPathMap = new HashMap<>();

        long apkTotalSize = 0;

        for (String apkPath : apkPaths) {
            File apkFile = new File(apkPath);
            if (apkFile.isFile()) {
                fileNameToPathMap.put(apkFile.getName(), apkPath);
                apkTotalSize += apkFile.length();
            }
        }

        final PackageInstaller.SessionParams sessionParams = makeInstallParams(targetPackageName, apkTotalSize);

        int sessionId = runInstallCreate(sessionParams);

        for (Map.Entry<String, String> entry : fileNameToPathMap.entrySet()) {
            runInstallWrite(sessionId, entry.getKey(), entry.getValue());
        }
        doCommitSession(sessionId);
    }

    private int runInstallCreate(PackageInstaller.SessionParams sessionParams) throws IOException {
        return mPackageInstaller.createSession(sessionParams);
    }

    private void runInstallWrite(int sessionId, String splitName, String apkPath) throws IOException {
        final File file = new File(apkPath);
        long sizeBytes = file.length();
        PackageInstaller.Session session = mPackageInstaller.openSession(sessionId);
        InputStream in = new FileInputStream(apkPath);
        OutputStream out = session.openWrite(splitName, 0, sizeBytes);
        byte[] buffer = new byte[65536];
        int c;
        while ((c = in.read(buffer)) != -1) {
            out.write(buffer, 0, c);
        }
        session.fsync(out);
        try {
            out.close();
            in.close();
            session.close();
        } catch (IOException ignored) {

        }
    }

    private void doCommitSession(int sessionId) throws IOException {
        PackageInstaller.Session session = mPackageInstaller.openSession(sessionId);
        //SplitApkInstallerService 用於接收安裝結果
        Intent callbackIntent = new Intent(appContext, SplitApkInstallerService.class);
        PendingIntent pendingIntent = PendingIntent.getService(appContext, 0, callbackIntent, 0);
        session.commit(pendingIntent.getIntentSender());
        session.close();
    }

    private static PackageInstaller.SessionParams makeInstallParams(String targetPackageName, long totalSize) {
        final PackageInstaller.SessionParams sessionParams;
        if (TextUtils.isEmpty(targetPackageName)) {
            sessionParams = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
        } else {
            sessionParams = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_INHERIT_EXISTING);
            sessionParams.setAppPackageName(targetPackageName);
        }
        sessionParams.setSize(totalSize);
        return sessionParams;
    }

    public class SplitApkInstallerService extends Service {

        private static final String TAG = "SplitApkInstallerService";

        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999);
            switch (status) {
                case PackageInstaller.STATUS_PENDING_USER_ACTION:
                    break;
                case PackageInstaller.STATUS_SUCCESS:
                    Log.d(TAG, "Installation succeed");
                    break;
                default:
                    Log.d(TAG, "Installation failed");
                    break;
            }
            stopSelf();
            return START_NOT_STICKY;
        }

        @Nullable
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
    }

}

複製代碼

代碼參考 splitapkinstall 並作適當整合。

上述代碼完成多 APK 安裝功能。調用 inheritInstallApks 方法便可爲已經至設備的 base apk 繼續安裝 split apks。須要注意,第三方應用沒法靜默安裝 split apks,系統會彈出安裝器供用戶選擇。

上圖是華爲手機彈出的安裝器界面。

此外,當 split apks 安裝完成後,若是 base app 處於運行狀態,那麼其會被系統「殺死」。若是不但願 base app 在 split apks 安裝成功後被但願殺死,能夠經過android.content.pm.PackageInstaller.SessionParams 類的 setDontKillApp 方法來設置。不過該方法屬於系統 API,第三方應用沒法使用。

/** {@hide} */
@SystemApi
public void setDontKillApp(boolean dontKillApp) {
    if (dontKillApp) {
        installFlags |= PackageManager.INSTALL_DONT_KILL_APP;
    } else {
        installFlags &= ~PackageManager.INSTALL_DONT_KILL_APP;
    }
}
複製代碼

總結

本文主要圍繞 Android App Bundle 開發、安裝等知識點來讓你們對其有初步認識。因 Google Play Service 在國內不可用,因此國內不少 Android 開發者對 Android 多 APK 安裝機制並不瞭解。在介紹 Qigsaw 以前撰寫此文的目的也是可以讓你們瞭解 Qigsaw 工做的基礎。爲後續文章講解打下堅實基礎。

相關文章
相關標籤/搜索