在今年的Google I/O大会上,Google向 Android 引入了新 App 动态化框架(即Android App Bundle,缩写为AAB),与Instant App不同,AAB是借助Split Apk完成动态加载,使用AAB动态下发方式,可以大幅度减少应用体积。现在只须在 Android Studio 中构建一个应用束 (app bundle),就可以将应用所需的全部内容 (适用于所有设备) 都涵盖在内:所有语言、所有设备屏幕大小、所有硬件架构。
下面是Dynamic Delivery示意效果图:
不过要想体验Dynamic Delivery,需要先下载 Android Studio 3.2
学习Android App Bundles可以将它和Split Apks来对比学习。
split apks是Android 5.0开始提供多apk构建机制,借助split apks可以将一个apk基于ABI和屏幕密度两个维度拆分城多个apk,这样可以有效减少apk体积。当用户下载应用程序安装包时,只会包含对应平台的so和资源。因为需要google play支持,所以国内就没戏了。针对不同cpu架构问题,国内应用开发商大部分都会将so文件只放在armabi目录下,如此做虽然可以有效减少包体积,但可能带来性能问题。split apks详细的内容可以访问下面的链接:https://link.zhihu.com/?target=https%3A//developer.android.com/studio/build/configure-apk-splits%3Fauthuser%3D2
Split Apks的运作原理有点类似于Android的组件化,安装应用程序时,首先安装base apk,然后安装split apks。为了说明splite apks运作原理,来看一下Android 5.0关于splite apks的源码。
打开ApplicationInfo类中,可以看到如下信息:
/**
* Full paths to zero or more split APKs that, when combined with the base
* APK defined in {@link #sourceDir}, form a complete application.
*/
public String[] splitSourceDirs;
/**
* Full path to the publicly available parts of {@link #splitSourceDirs},
* including resources and manifest. This may be different from
* {@link #splitSourceDirs} if an application is forward locked.
*/
public String[] splitPublicSourceDirs;
LoadeApk中有PathClassLoader和Resources创建过程。LoadedApk#mClassLoader是PathClassLoader实例引用,接着看PathClassLoader的创建过程。
public ClassLoader getClassLoader() {
synchronized (this) {
if (mClassLoader != null) {
return mClassLoader;
}
if (mIncludeCode && !mPackageName.equals("android")) {
......
final ArrayList<String> zipPaths = new ArrayList<>();
final ArrayList<String> libPaths = new ArrayList<>();
.......
zipPaths.add(mAppDir);
//将split apk路径追加到zipPaths中
if (mSplitAppDirs != null) {
Collections.addAll(zipPaths, mSplitAppDirs);
}
libPaths.add(mLibDir);
......
final String zip = TextUtils.join(File.pathSeparator, zipPaths);
final String lib = TextUtils.join(File.pathSeparator, libPaths);
......
//如果mSplitAppDirs不为空,则zip将包含split apps所有路径。
mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib,
mBaseClassLoader);
StrictMode.setThreadPolicy(oldPolicy);
} else {
if (mBaseClassLoader == null) {
mClassLoader = ClassLoader.getSystemClassLoader();
} else {
mClassLoader = mBaseClassLoader;
}
}
return mClassLoader;
}
}
在创建PathClassLoader时,dex文件路径包含base app和split apps路径,LoadedApk#mResources是Resources实例引用,Resources的源码如下:
public Resources getResources(ActivityThread mainThread) {
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this);
}
return mResources;
}
可以发现:split apks资源路径(LoadedApk#mSplitResDirs)也会被增加至Resources中。
下面再来看Android App Bundles,Android App Bundle 支持模块化,通过Dynamic Delivery with split APKs,将一个apk拆分成多个apk,按需加载(包括加载C/C++ libraries),这样开发者可以随时按需交付功能,而不是仅限在安装过程中。
Android App Bundle 通常会包括以下几个文件:
AAB并不是一个插件化框架,它利用的是Android Framework提供的split apks技术来完成的,而所有安装split apk工作均是通过IPC交由google play完成。 具体使用时,在Android Studio新增一项module——Dynamic Feature Module。
在创建dynamic_feature时,有两个选项是默认勾选的,当然我们也可以更改其状态。
下面看一个简单的实例程序。
在示例中,有四个feature,通过module名很清楚这些feature是举例介绍如何访问代码、资源、so等。 dynamic feature module编译所使用的插件com.android.dynamic-feature,那么该插件有何独特之处,通过编译产物分析,运行示例后,发现在所有dynamic feature模块build目录下均会生成apk文件。
接着反编译主apk(com.android.application插件生成产物),会发现两个有趣的现象:
Android App Bundle提供一种全新编译产物格式文件aab,使用Android Studio提供的App Bundle即可。
如上图,当选择Build Bundle(s)时,在主工程build目录下回生成bundle.aab文件,该文件是压缩格式文件,解压该aab文件内容如下。
从aab文件内容,可知其包含base和feature的代码、资源、so等,同时还有BundleConfig.pb这一配置文件,该配置文件是google play用于拆分apk。如果我们需要在google play上支持动态发布,只需要上传aab文件即可,后续工作交给google play完成。
Play Core Library是AAB提供的核心库,用于下载、安装dynamic feature模块。另外,我们也可以用这些API下载on-demand模块用于instant app。关于Play Core Library具体如何使用,大家可以查看相关文档。
当app运行设备版本不高于6.0时,需要使用SplitCompat库才能立即访问下载模块代码和资源。AAB提供SplitCompatApplication类用于开启SplitCompat。
public class SplitCompatApplication extends Application {
public SplitCompatApplication() {
}
protected void attachBaseContext(Context var1) {
super.attachBaseContext(var1);
SplitCompat.install(this);
}
}
在Application#attachBaseContext(Context)中调用SplitCompat.install(Context)。在该方法中主要完成split apks代码(dex和so)和资源的安装。下面是一些兼容的条件分支语句:
public static a a() {
if (VERSION.SDK_INT == 21) {
//com.google.android.play.core.splitcompat.b.c
return new c();
} else if (VERSION.SDK_INT == 22) {
//com.google.android.play.core.splitcompat.b.f
return new f();
} else if (VERSION.SDK_INT == 23) {
//com.google.android.play.core.splitcompat.b.g
return new g();
} else {
throw new AssertionError();
}
}
在Android 8.0中,Instant Apps相关代码嵌入至Framework。因此如果on-demand模块用于Instant Apps中,需要在on-demand下载成功中,调用SplitInstallHelper.updateAppInfo(Context)。
public static void updateAppInfo(Context var0) {
if (VERSION.SDK_INT > 25) {
a.a("Calling dispatchPackageBroadcast!", new Object[0]);
try {
Class var1;
Method var2;
(var2 = (var1 = Class.forName("android.app.ActivityThread")).getMethod("currentActivityThread")).setAccessible(true);
Object var3 = var2.invoke((Object)null);
Field var4;
(var4 = var1.getDeclaredField("mAppThread")).setAccessible(true);
Object var5;
(var5 = var4.get(var3)).getClass().getMethod("dispatchPackageBroadcast", Integer.TYPE, String[].class).invoke(var5, 3, new String[]{var0.getPackageName()});
a.a("Calling dispatchPackageBroadcast", new Object[0]);
} catch (Exception var6) {
a.a(var6, "Update app info with dispatchPackageBroadcast failed!", new Object[0]);
}
}
}
从上述代码得知其反射调用ActivityThread#dispatchPackageBroadcast方法。最终是调用至LoadedApk#updateApplicationInfo。该方法做了如下事情