摘要: Android App Bundles 在今年的Google I/O大会上,Google向 Android 引入了新 App 动态化框架(即Android App Bundle,缩写为AAB),与Instant App不同,AAB是借助Split Apk完成动态加载,使用AAB动态下发方式,可以大幅度减少应用体积。

## Android App Bundles
在今年的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

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](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](http://androidxref.com/5.0.0_r2/xref/frameworks/base/core/java/android/content/pm/ApplicationInfo.java)类中,可以看到如下信息:
`/**
   * Full paths to zero <span class="hljs-keyword">or</span> more <span class="hljs-keyword">split</span> APKs that, <span class="hljs-keyword">when</span> combined with the base
   * APK <span class="hljs-keyword">defined</span> in {@link <span class="hljs-comment">#sourceDir}, form a complete application.</span>
   *<span class="hljs-regexp">/
  public String[] splitSourceDirs;

  /</span>**
   * Full path to the publicly available parts of {@link <span class="hljs-comment">#splitSourceDirs},</span>
   * including resources <span class="hljs-keyword">and</span> manifest. This may be different from
   * {@link <span class="hljs-comment">#splitSourceDirs} if an application is forward locked.</span>
   *<span class="hljs-regexp">/
  public String[] splitPublicSourceDirs;</span>`
[LoadeApk](http://androidxref.com/5.0.0_r2/xref/frameworks/base/core/java/android/app/LoadedApk.java)中有PathClassLoader和Resources创建过程。LoadedApk#mClassLoader是PathClassLoader实例引用,接着看PathClassLoader的创建过程。
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; } }" data-snippet-id="ext.b0abcb7003a3d76aeb1ae260fba2afe7" data-snippet-saved="false" data-codota-status="done">`&lt;span class="hljs-function">&lt;span class="hljs-keyword">public&lt;/span> ClassLoader &lt;span class="hljs-title">getClassLoader&lt;/span>&lt;span class="hljs-params">()&lt;/span> &lt;/span>{
      &lt;span class="hljs-keyword">synchronized&lt;/span> (&lt;span class="hljs-keyword">this&lt;/span>) {
          &lt;span class="hljs-keyword">if&lt;/span> (mClassLoader != &lt;span class="hljs-keyword">null&lt;/span>) {
              &lt;span class="hljs-keyword">return&lt;/span> mClassLoader;
          }

          &lt;span class="hljs-keyword">if&lt;/span> (mIncludeCode && !mPackageName.equals(&lt;span class="hljs-string">"android"&lt;/span>)) {

              ......

              &lt;span class="hljs-keyword">final&lt;/span> ArrayList&lt;String&gt; zipPaths = &lt;span class="hljs-keyword">new&lt;/span> ArrayList&lt;&gt;();
              &lt;span class="hljs-keyword">final&lt;/span> ArrayList&lt;String&gt; libPaths = &lt;span class="hljs-keyword">new&lt;/span> ArrayList&lt;&gt;();

              .......

              zipPaths.add(mAppDir);
              &lt;span class="hljs-comment">//将split apk路径追加到zipPaths中&lt;/span>
              &lt;span class="hljs-keyword">if&lt;/span> (mSplitAppDirs != &lt;span class="hljs-keyword">null&lt;/span>) {
                  Collections.addAll(zipPaths, mSplitAppDirs);
              }

              libPaths.add(mLibDir);

              ......

              &lt;span class="hljs-keyword">final&lt;/span> String zip = TextUtils.join(File.pathSeparator, zipPaths);
              &lt;span class="hljs-keyword">final&lt;/span> String lib = TextUtils.join(File.pathSeparator, libPaths);

              ......
              &lt;span class="hljs-comment">//如果mSplitAppDirs不为空,则zip将包含split apps所有路径。&lt;/span>
              mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib,
                      mBaseClassLoader);

              StrictMode.setThreadPolicy(oldPolicy);
          } &lt;span class="hljs-keyword">else&lt;/span> {
              &lt;span class="hljs-keyword">if&lt;/span> (mBaseClassLoader == &lt;span class="hljs-keyword">null&lt;/span>) {
                  mClassLoader = ClassLoader.getSystemClassLoader();
              } &lt;span class="hljs-keyword">else&lt;/span> {
                  mClassLoader = mBaseClassLoader;
              }
          }
          &lt;span class="hljs-keyword">return&lt;/span> mClassLoader;
      }
  }`
在创建PathClassLoader时,dex文件路径包含base app和split apps路径,LoadedApk#mResources是Resources实例引用,Resources的源码如下:
`&lt;span class="hljs-function">&lt;span class="hljs-keyword">public&lt;/span> Resources &lt;span class="hljs-title">getResources&lt;/span>(&lt;span class="hljs-params">ActivityThread mainThread&lt;/span>) &lt;/span>{
      &lt;span class="hljs-keyword">if&lt;/span> (mResources == &lt;span class="hljs-literal">null&lt;/span>) {
          mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
                  mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, &lt;span class="hljs-literal">null&lt;/span>, &lt;span class="hljs-keyword">this&lt;/span>);
      }
      &lt;span class="hljs-keyword">return&lt;/span> mResources;
  }`
可以发现:split apks资源路径(LoadedApk#mSplitResDirs)也会被增加至Resources中。

Android App Bundles

下面再来看Android App Bundles,Android App Bundle 支持模块化,通过Dynamic Delivery with split APKs,将一个apk拆分成多个apk,按需加载(包括加载C/C++ libraries),这样开发者可以随时按需交付功能,而不是仅限在安装过程中。





Android App Bundle 通常会包括以下几个文件:




- Base Apk:首次安装的apk,公共代码和资源,所以其他的模块都基于Base Apk;

- Configuration APKs:native libraries 和适配当前手机屏幕分辨率的资源;

- Dynamic feature APKs:不需要在首次安装就加载的模块。





![这里写图片描述](https://img-blog.csdn.net/20180516221843565)





AAB并不是一个插件化框架,它利用的是Android Framework提供的split apks技术来完成的,而所有安装split apk工作均是通过IPC交由google play完成。

具体使用时,在Android Studio新增一项module——Dynamic Feature Module。 这里写图片描述 在创建dynamic_feature时,有两个选项是默认勾选的,当然我们也可以更改其状态。 这里写图片描述

- Enable on-demand: 是否支持按需下载模式。如果不支持,那么该feature则在安装app时被安装。

- Fusing: 如果app运行在Android 5.0(不包括5.0)以下,勾选Fusing则表示该feature会被一起打包至完整apk中。





下面看一个简单的实例程序。

这里写图片描述 在示例中,有四个feature,通过module名很清楚这些feature是举例介绍如何访问代码、资源、so等。 dynamic feature module编译所使用的插件com.android.dynamic-feature,那么该插件有何独特之处,通过编译产物分析,运行示例后,发现在所有dynamic feature模块build目录下均会生成apk文件。

接着反编译主apk(com.android.application插件生成产物),会发现两个有趣的现象:




- 所有dynamic feature module的代码、资源、so并未打包至主apk中。

- 主apk manifest信息包括所有dynamic feature module的manifest,即feature manifest会被合并至主apk manifest中。

Build Bundle(s)

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

Play Core Library是AAB提供的核心库,用于下载、安装dynamic feature模块。另外,我们也可以用这些API下载on-demand模块用于instant app。关于Play Core Library具体如何使用,大家可以查看相关文档。

兼容性问题处理

6.0以下版本

当app运行设备版本不高于6.0时,需要使用SplitCompat库才能立即访问下载模块代码和资源。AAB提供SplitCompatApplication类用于开启SplitCompat。
`&lt;span class="hljs-keyword">public&lt;/span> &lt;span class="hljs-class">&lt;span class="hljs-keyword">class&lt;/span> &lt;span class="hljs-title">SplitCompatApplication&lt;/span> &lt;span class="hljs-keyword">extends&lt;/span> &lt;span class="hljs-title">Application&lt;/span> &lt;/span>{
  &lt;span class="hljs-function">&lt;span class="hljs-keyword">public&lt;/span> &lt;span class="hljs-title">SplitCompatApplication&lt;/span>&lt;span class="hljs-params">()&lt;/span> &lt;/span>{
  }

  &lt;span class="hljs-function">&lt;span class="hljs-keyword">protected&lt;/span> &lt;span class="hljs-keyword">void&lt;/span> &lt;span class="hljs-title">attachBaseContext&lt;/span>&lt;span class="hljs-params">(Context var1)&lt;/span> &lt;/span>{
      &lt;span class="hljs-keyword">super&lt;/span>.attachBaseContext(var1);
      SplitCompat.install(&lt;span class="hljs-keyword">this&lt;/span>);
  }
}`
在Application#attachBaseContext(Context)中调用SplitCompat.install(Context)。在该方法中主要完成split apks代码(dex和so)和资源的安装。下面是一些兼容的条件分支语句:
`&lt;span class="hljs-function">&lt;span class="hljs-keyword">public&lt;/span> &lt;span class="hljs-keyword">static&lt;/span> a &lt;span class="hljs-title">a&lt;/span>&lt;span class="hljs-params">()&lt;/span> &lt;/span>{
      &lt;span class="hljs-keyword">if&lt;/span> (VERSION.SDK_INT == &lt;span class="hljs-number">21&lt;/span>) {
      &lt;span class="hljs-comment">//com.google.android.play.core.splitcompat.b.c&lt;/span>
          &lt;span class="hljs-keyword">return&lt;/span> &lt;span class="hljs-keyword">new&lt;/span> c();
      } &lt;span class="hljs-keyword">else&lt;/span> &lt;span class="hljs-keyword">if&lt;/span> (VERSION.SDK_INT == &lt;span class="hljs-number">22&lt;/span>) {
      &lt;span class="hljs-comment">//com.google.android.play.core.splitcompat.b.f&lt;/span>
          &lt;span class="hljs-keyword">return&lt;/span> &lt;span class="hljs-keyword">new&lt;/span> f();
      } &lt;span class="hljs-keyword">else&lt;/span> &lt;span class="hljs-keyword">if&lt;/span> (VERSION.SDK_INT == &lt;span class="hljs-number">23&lt;/span>) {
      &lt;span class="hljs-comment">//com.google.android.play.core.splitcompat.b.g&lt;/span>
          &lt;span class="hljs-keyword">return&lt;/span> &lt;span class="hljs-keyword">new&lt;/span> g();
      } &lt;span class="hljs-keyword">else&lt;/span> {
          &lt;span class="hljs-keyword">throw&lt;/span> &lt;span class="hljs-keyword">new&lt;/span> AssertionError();
      }
  }`

高于8.0版本

在Android 8.0中,Instant Apps相关代码嵌入至Framework。因此如果on-demand模块用于Instant Apps中,需要在on-demand下载成功中,调用SplitInstallHelper.updateAppInfo(Context)。
25) { a.a(&quot;Calling dispatchPackageBroadcast!&quot;, new Object[0]); try { Class var1; Method var2; (var2 = (var1 = Class.forName(&quot;android.app.ActivityThread&quot;)).getMethod(&quot;currentActivityThread&quot;)).setAccessible(true); Object var3 = var2.invoke((Object)null); Field var4; (var4 = var1.getDeclaredField(&quot;mAppThread&quot;)).setAccessible(true); Object var5; (var5 = var4.get(var3)).getClass().getMethod(&quot;dispatchPackageBroadcast&quot;, Integer.TYPE, String[].class).invoke(var5, 3, new String[]{var0.getPackageName()}); a.a(&quot;Calling dispatchPackageBroadcast&quot;, new Object[0]); } catch (Exception var6) { a.a(var6, &quot;Update app info with dispatchPackageBroadcast failed!&quot;, new Object[0]); } } }" data-snippet-id="ext.fc596701974d7885a5867ac0edb0ea3a" data-snippet-saved="false" data-codota-status="done">`public &lt;span class="hljs-keyword">static&lt;/span> &lt;span class="hljs-keyword">void&lt;/span> updateAppInfo(Context var0) {
      &lt;span class="hljs-keyword">if&lt;/span> (VERSION.SDK_INT &gt; &lt;span class="hljs-number">25&lt;/span>) {
          a.a(&lt;span class="hljs-string">"Calling dispatchPackageBroadcast!"&lt;/span>, &lt;span class="hljs-keyword">new&lt;/span> &lt;span class="hljs-built_in">Object&lt;/span>[&lt;span class="hljs-number">0&lt;/span>]);

          &lt;span class="hljs-keyword">try&lt;/span> {
              Class var1;
              Method var2;
              (var2 = (var1 = Class.forName(&lt;span class="hljs-string">"android.app.ActivityThread"&lt;/span>)).getMethod(&lt;span class="hljs-string">"currentActivityThread"&lt;/span>)).setAccessible(&lt;span class="hljs-literal">true&lt;/span>);
              &lt;span class="hljs-built_in">Object&lt;/span> var3 = var2.invoke((&lt;span class="hljs-built_in">Object&lt;/span>)&lt;span class="hljs-literal">null&lt;/span>);
              Field var4;
              (var4 = var1.getDeclaredField(&lt;span class="hljs-string">"mAppThread"&lt;/span>)).setAccessible(&lt;span class="hljs-literal">true&lt;/span>);
              &lt;span class="hljs-built_in">Object&lt;/span> var5;
              (var5 = var4.get(var3)).getClass().getMethod(&lt;span class="hljs-string">"dispatchPackageBroadcast"&lt;/span>, Integer.TYPE, &lt;span class="hljs-built_in">String&lt;/span>[].class).invoke(var5, &lt;span class="hljs-number">3&lt;/span>, &lt;span class="hljs-keyword">new&lt;/span> &lt;span class="hljs-built_in">String&lt;/span>[]{var0.getPackageName()});
              a.a(&lt;span class="hljs-string">"Calling dispatchPackageBroadcast"&lt;/span>, &lt;span class="hljs-keyword">new&lt;/span> &lt;span class="hljs-built_in">Object&lt;/span>[&lt;span class="hljs-number">0&lt;/span>]);
          } &lt;span class="hljs-keyword">catch&lt;/span> (Exception var6) {
              a.a(var6, &lt;span class="hljs-string">"Update app info with dispatchPackageBroadcast failed!"&lt;/span>, &lt;span class="hljs-keyword">new&lt;/span> &lt;span class="hljs-built_in">Object&lt;/span>[&lt;span class="hljs-number">0&lt;/span>]);
          }
      }
  }`
从上述代码得知其反射调用ActivityThread#dispatchPackageBroadcast方法。最终是调用至LoadedApk#updateApplicationInfo。该方法做了如下事情




- 重新创建mClassLoader

- 重新创建mResources

- 更新applicationInfo(调用LoadedApk#setApplicationInfo完成)。





官方bundletool https://developer.android.com/studio/command-line/bundletool





转自:https://yq.aliyun.com/articles/593597