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 简介小程序
Qigsaw 打包插件流程分析安全
Qigsaw 插件代码加载详解bash
Qigsaw 插件四大组件启动分析session
Qigsaw 插件资源加载详解app
Qigsaw 插件热更新介绍
Qigsaw 兼容性问题概述
目前国内应用开发者比较少接触 Android App Bundles 开发,所以在介绍 Qigsaw 以前,咱们首先来了解下 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 打包插件关系类图,
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。
前文提到 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.apk、base-xxhdpi、base-zh.apk 三个 apk 来安装。base-master.apk 是 base apk,base-xxhdpi 和 base-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-xxhdpi、base-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.apk 和 java-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 工做的基础。为后续文章讲解打下坚实基础。