基于 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 插件四大组件启动分析session

  • Qigsaw 插件资源加载详解app

  • 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 工做的基础。为后续文章讲解打下坚实基础。

相关文章
相关标签/搜索