文章目录

  • 一、Google减少应用包体积方案演进
  • 1.1 单APK时代
  • 1.2 Multiple APK
  • 1.3 Android App Bundle
  • 1.3.1 dynamic feature
  • 1.3.2 Split APKs(Android5.0)
  • 1.3.2.1 PackageInstaller
  • 1.3.3 Split APKs加载原理
  • 1.3.3.1 ClassLoader
  • 1.3.3.2 Resources
  • 1.3.3.3 不支持四大组件的新增
  • 1.3.3.4 多进程问题
  • 二、爱奇艺动态化框架Qigsaw
  • 2.1 背景知识
  • 2.2 比googlePlay更好的打包体验
  • 2.3 Qigsaw原理还是基于插件化
  • 2.3.1 代码加载使用打补丁
  • 2.3.2 资源加载基于打补丁的方式同时不会冲突
  • 资源id冲突问题
  • 反射Resource对象
  • 2.3.3 不支持四大组件的新增
  • 参考文献


一、Google减少应用包体积方案演进

Android动态化方案,在国内已蓬勃发展数年之久,其核心目的是减少应用包体积,提升应用安装率。Google在减少应用包体积上的探索也从未停息,下面我们一起来看看Google在这方面的努力。

1.1 单APK时代

回首Android第一个10年,其应用发布方式如下。

android interface 动态 android 动态化方案_Android


从应用开发到上传应用商店,最后再到用户下载环节,参与产物都是APK

android interface 动态 android 动态化方案_加载_02

  • 应用将包含所有CPU架构so文件、所有屏幕分辨率资源文件以及所有语言资源文件,那么存在如下两个问题。
  • APK文件过大导致用户下载时长增加。
  • 大量不会被使用的代码和资源侵占用户磁盘空间。

在国内,开发者一般都只会放一种CPU架构的so文件和一种屏幕分辨率资源文件,以此来减少包体积,但这种方式一定程度上会影响用户体验。

1.2 Multiple APK

Google意识到包体积问题的严峻性,于Android 5.0推出Multiple APK,旨在减少安装包体积

Multiple APK是Google Play提供一个功能,它允许您的应用针对不同的设备配置发布不同的APK。通过一张图来了解下其工作流程。

android interface 动态 android 动态化方案_加载_03


图中左边手机是nexus 5,右边手机是nexus 6p,它们的CPU架构、屏幕分辨率均不同,因此Google Play会根据当前设备配置下载对应APK。

Google提供打包配置选项,让开发者根据不同设备配置生成不同APK文件。

android {
  ...
  splits {

    // Configures multiple APKs based on screen density.

  density {
     ...
     // Specifies a list of screen densities Gradle should not create multiple APKs for.
      exclude "ldpi", "xxhdpi", "xxxhdpi"
    }
    // Configures multiple APKs based on ABI.

    abi {
      ...
      // Specifies a list of ABIs that Gradle should create APKs for.
      include "x86", “x86_64"

      // Specifies that we do not want to also generate a universal APK that includes all ABIs.
      universalApk false
    }
  }
}

通过density和abi两个配置维度即可生成一系列APKs。

android interface 动态 android 动态化方案_Android_04


上图中生成的产物,通过文件名我们可以很清楚知道该APK作用于何种配置的设备。

Android设备的多样性,导致Multiple APK并未朝着Google期待的方向发展。因为您有可能为每个版本构建数百个APKs,大大降低迭代效率。国外开发者对此也并不感冒,这也成为Google的一块心病。

1.3 Android App Bundle

Android App Bundle是一种全新的应用上传格式(.aab),它包含所有编译代码和资源。当您上传aab文件至Google Play后,Google Play将aab文件拆分成一系列APKs并签名。

android interface 动态 android 动态化方案_App_05

1.3.1 dynamic feature

此外,您也可以在应用项目中添加dynamic feature模块,这些模块并不需要在应用首次安装时一起被下载安装。您可以通过使用Play Core Libray在应用运行过程中动态安装dynamic feature。dynamic feature类似国内插件化提供的能力,但dynamic feature功能更强大。

通过下图,可以看到dynamic feature可以基于设备配置选取对应的Configuration Split APKs,如此可以进一步减小dynamic feature安装包体积。

android interface 动态 android 动态化方案_加载_06

1.3.2 Split APKs(Android5.0)

Android App Bundle之所以能够支持应用运行期间安装dynamic feature,得益于Android 5.0推出的Split APKs功能。

Split APKs是Android 5.0引入的一种全新应用安装机制,其目的是为解决APK体积日益增大问题。Split APK可以将一个完整庞大的APK按照CPU架构、屏幕密度等维度拆分成多个独立APKs。当应用APK下载更新时,依据当前设备配置选取对应配置APKs安装即可。

Android 5.0之前,一个APK代表一个应用。在Split APKs问世之后,一个应用可能对应多个APKs。所有Split APKs拥有相同包名和签名。

Android提供两种方式安装Split APKs。

  1. adb install-multiple [base-apk, split1-apk]
  2. PackageInstaller.

Android App Bundle为dynamic feature提供全新插件com.android.dynamic-feature,它的编译产物是.apk文件。当您的项目编译完成后,Android Studio通过命令adb install-multiple命令将base apk和split apks安装至您的手机。如果您的开发手机系统版本低于5.0,则会依据当前手机设备组装成一个完整apk文件安装至该手机。

android interface 动态 android 动态化方案_App_07

下面我们重点介绍第二种安装方式,Android 5.0提供PackageInstaller用于安装Base APK和Split APKs。

1.3.2.1 PackageInstaller

当第三方应用通过PackageInstaller在应用运行期安装Split APKs时,系统会启动安装器界面供用户选择是否安装此次更新。

android interface 动态 android 动态化方案_App_08

在用户选择安装后,应用将会被系统“杀死”。当应用再次启动之后,Split APKs就会生效。

在我们实际测试过程中,某些国产手机对PackageInstaller有改动,导致无法正常安装Split APKs。

系统应用可以静默安装Split APKs,且当Split APKs安装完成后,可以决定是否“杀死“应用进程。

public static class SessionParams implements Parcelable {

    ...

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

SessionParams是PackageInstaller内部类,setDontKillApp可决定当APK安装完成后是否杀死应用进程。setDontKillApp属于系统Api,因此第三方应用无法调用。

1.3.3 Split APKs加载原理

android interface 动态 android 动态化方案_App_09

通过Android 9.0 LoadedAPK源码片段,我们一起了解下Split APKs加载过程。

是的,系统级的代码本身就支持了Split Apk的加载

1.3.3.1 ClassLoader

通过createOrUpdateClassLoaderLocked方法名,可以知道该方法是用于创建和更新ClassLoader

android interface 动态 android 动态化方案_加载_10

该方法有两个核心步骤。

  1. 如果mClassLoader为空,则创建PathClassLoader实例。
  2. 如果addedPaths不为空,则更新PathClassLoader实例

该方法指明,应用进程是可以动态加载Split APKs代码

1.3.3.2 Resources

通过getResources方法代码片段,可知Split APKs的资源路径作为mResources创建参数

android interface 动态 android 动态化方案_App_11

1.3.3.3 不支持四大组件的新增

Android App Bundle在Manifest文件合并过程中,会将split APKs manifest文件内容合并至base APK中。因此,所有split APKs四大组件信息都是已经声明在base APK中。

Android App Bundle这种处理方式不支持Manifest更新,例如新增四大组件

1.3.3.4 多进程问题

Android App Bundle所支持的功能特性有部分局限性

  • 多进程问题

android interface 动态 android 动态化方案_加载_12


依据Qigsaw安装、加载split APKs原则,当游戏APK安装完成后,就会在主进程完成加载。在游戏APK中有两个Activity,他们所处进程不同。当启动GameActivity01时,页面正常启动。但当启动GameActivity02,您的App会出现崩溃。原因是GameActivity02运行在:game进程,游戏APK仅在主进程加载,并未在:game进程加载,因此系统会抛出ClassNotFoundException异常。

为解决这类问题,Qigsaw提供了如下解决方案。

  1. 在进程启动之初即Applicatin#attachBaseContext调用时,加载所有已安装splits。
  • 第一种方案解决的场景是:game进程首次启动,即启动GameActivity02之前:game进程从未启动过。
  1. Hook PathClassLoader。
  • 第二种方案解决的场景是:game进程已经启动并正在运行

Hook PathClassLoader具体做了如下事情。

  • 当出现ClassNotFoundException时,判断该类是否为splits四大组件。
  • 当异常类为splits四大组件时,加载所有已安装未加载split APKs。
  • 如加载完所有已安装未加载split APKs后依然出现ClassNotFoundException异常,则返回空四大组件类,防止进程崩溃。

二、爱奇艺动态化框架Qigsaw

2.1 背景知识

在2018年上半年,我们就进行动态组件化方案的调研。起初方案是基于Instant App方案实现,当整体功能基本实现后,Google于2018年Google IO大会上推出Android App Bundle。在调研Android App Bundle之后,我们发现Android App Bundle完全符合最初的需求。

依据我们最初设计初衷和Android App Bundle特点,总结出Qigsaw应满足以下核心特点。

  • 利用Android App Bundle开发套件,体验原生极速开发体验。
  • 少量私有Api访问,保证框架稳定性。
  • 如果您的应用有出海需求,可无缝切换至Android App Bundle方案。

关于私有Api访问应该是大家比较关心的,最近一段时间某大厂开源了号称零反射插件化框架,但是通过阅读其源码,我们发现它还是做了PathClassLoader的parent ClassLoader反射替换。

另外它也调用了Resources构造方法创建Resources实例,虽然这样做并没有任何私有Api访问,但是通过查看Resources构造方法源码,我们可知该方法属于过时方法,且注释写明第三方应用不应该创建Resources实例。

所以插件化框架不应该仅仅以是否零反射为目标,我们应该从开发流程及产品形态选取合适方案,助力开发效率。

2.2 比googlePlay更好的打包体验

在发布阶段,Qigsaw提供打包插件让开发者享受一条龙服务,开发者不必关心dynamic feature的上传分发。

Qigsaw打包插件支持内置dynamic feature,所有内置dynamic feature都会被拷贝至base apk的assets目录。对于非内置dynamic feature,Qigsaw打包插件会将其上传至CDN服务器,解决业务方后顾之忧。

android interface 动态 android 动态化方案_Android_13

2.3 Qigsaw原理还是基于插件化

Qigsaw借助Android App Bundle开发套件完成dynamic feature的打包,大大降低Qigsaw开发维护成本。因此Qigsaw关心的重点落在 如果安装加载dynamic feature生成apk上

第三方应用利用PackageInstaller安装split APKs体验极其不友好,且某些国产手机对split APKs功能支持不完善,所以我们最终还是按照一般插件化方式安装加载split APKs。

android interface 动态 android 动态化方案_加载_14


依据上图,如果需要动态加载split APKs,需要解决代码、资源以及四大组件的加载

2.3.1 代码加载使用打补丁

针对splits代码加载,Qigsaw采用单类加载器方式,即base APK和split APKs采用同一ClassLoader加载。

android interface 动态 android 动态化方案_App_15


在DexPathList中,为每个split创建对应的Element和NativeLibraryElement实例即可。关于单类加载器更多细节,本文不再赘述,相关原理已非常成熟。

2.3.2 资源加载基于打补丁的方式同时不会冲突

android interface 动态 android 动态化方案_App_16

Splits资源加载相较于代码加载会复杂,因为不同系统版本或不同手机厂商都会存在一些兼容性问题。

资源id冲突问题
  • Android Gradle Plugin在资源打包时,会对res目录下资源文件分配一个唯一Id。
  • Id前两位PP为Package Id,代表应用类型。是系统应用、第三方应用、Instant App或Dynamic Feature等。
  • Id中间两位TT为Type,代表资源类型。是drawable、layout或string等。
  • Id后四位EE为Entry,代表该资源顺序。

所有第三方应用base APK资源Package Id均为7F,Android App Bundle对splits资源打包时会基于7F依次递减分配Package Id。因此,即使我们将split APKs资源添加到当前应用Resources实例中,也不会出现资源冲突问题,splits访问base资源也更加方便<.font>

Instant Apps资源打包是基于7F依次递增。

反射Resource对象

android interface 动态 android 动态化方案_加载_17


Qigsaw提供loadResources方法加载split APKs资源。为避免开发者写大量模板代码,Qigsaw打包插件采用字节码操作方式自动写入该方法。

2.3.3 不支持四大组件的新增

Android App Bundle在Manifest文件合并过程中,会将split APKs manifest文件内容合并至base APK中。因此,所有split APKs四大组件信息都是已经声明在base APK中。

Android App Bundle这种处理方式不支持Manifest更新,例如新增四大组件,所以Qigsaw也不支持新增四大组件。在正常开发迭代过程中,动态新增splits四大组件需求极少,所以Qigsaw与Android App Bundle特性保持一致。