【Android开发】App Bundle技术之动态功能模块

前言

自 2021 年 8 月起,Google Play 将开始要求新应用使用 Android App Bundle 进行发布。该格式将取代 APK 作为标准发布格式。虽然这个政策目前还无法影响到国内应用,但是作为Android开发者,对于新的动态还是要有一定的认识。

Android App Bundle是基于google play的分发技术,可以有效的减少apk的大小。

Android App Bundle可以自动将不同的分辨率、语言、平台(CPU)的资源分别打包,这样当安装时只根据当前设备安装对应的资源,这样大大的节省了内存空间。

除了这些资源,Android App Bundle还支持动态功能模块(Dynamic Feature Module),这样如果用户不需要这部分功能,则不安装这个模块,这样就更加高度优化了下载和安装。

本文并不是介绍Android App Bundle,因为它不是新的事物,相关的文章也有很多了,本文重点介绍的是Android App Bundle中“动态功能模块”的实际应用。

创建模块

下面就来看看如何创建动态功能模块:

  1. 从菜单栏中依次选择 File > New > New Module。这一步与创建普通模块一样。

  2. 在 Create New Module 对话框中,选择 Dynamic Feature Module,然后点击 Next。

image.png

  1. 在 Configure your new module 部分中,完成以下操作:
  • 从下拉菜单中选择应用项目的 Base application module。(一般就是main主模块)

  • 指定 Module name。IDE 会使用此名称在 Gradle 设置文件中将该模块标识为 Gradle 子项目。当您构建 app bundle 时,Gradle 会使用子项目名称的最后一个元素在功能模块的清单中注入 属性。

  • 指定该模块的 package name。默认情况下,Android Studio 会建议一个软件包名称,该名称由基本模块的根目录软件包名称和您在上一步中指定的模块名称组合而成。

  • 选择您希望该模块支持的 Minimum API level。此值应与基本模块的值一致。

  • 点击 Next。

  1. 在 Module Download Options 部分中,完成以下操作:
  1. 指定最多包含 50 个字符的 Module title。(这个会展示给用户)

  2. 在 Install-time inclusion 下的下拉菜单中,选择分发时机,有下面三种:

  • Include module at install-time 安装时分发(与普通模块类似)

  • Do not include module at install-time 按需分发(安装时不分发,后面单独说)

  • Only include module at app install for devices with specified features 按条件分发

    按条件分发还需要配置条件,如下图

image.png

点击“+ device-feature”可以添加一个条件(可以添加多个条件)。在 每个device-feature 旁边,从下拉菜单中选择以下选项之一,然后为其指定值:

  • Name:用于指定设备为在安装应用时下载模块所需具备的硬件或软件功能。按条件分发所支持的功能与被 PackageManager 列为 FEATURE_* 常量的功能相同。如果您选择此选项,请先在下拉菜单旁边的字段中输入相应功能的常量值的任意部分(例如“bluetooth”),然后在所显示的建议中选择一个。
  • OpenGL ES Version:用于指定设备为在安装应用时下载模块所需的 OpenGL ES 版本。如果您选择此选项,请先在下拉菜单旁边的字段中输入相应版本(例如“0x00030001”),然后在所显示的建议中选择一个。
  1. 如果您希望此模块可提供给搭载 Android 4.4(API 级别 20)及更低版本的设备并包含在多 APK 中,请选中 Fusing 旁边的复选框。这意味着,您可以从不支持下载和安装拆分 APK 的设备上将其省略。

(这部分的配置最终会在模块下的Manifest中体现,下面会提到)

  1. 点击 Finish即可创建模块

配置模块

创建出的模块目录如下:

image.png

可以看到与正常模块一样,但是这里的Manifest内容有差异,如下:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:dist="http://schemas.android.com/apk/distribution"
    package="com.huichongzi.dynamicfeature">

    <dist:module
        dist:instant="false"
        dist:title="@string/title_dynamicfeature">
        <dist:delivery>
            <dist:on-demand />
        </dist:delivery>
        <dist:fusing dist:include="true" />
    </dist:module>
</manifest>

可以看到这里出现了一个dist标签,这个标签下就是动态功能模块的一些配置。比如dist:instant="false"就是不支持免安装(免安装后面会细说),而title就是上面4步中设置的module title。而

<dist:delivery>
    <dist:on-demand />
</dist:delivery>

就是分发时机,on-demand就是Do not include module at install-time即按需分发(后面单独说),如果改成install-time就是Include module at install-time即安装时分发,按条件分发则不太一样,如下:

 <dist:delivery>
      <dist:install-time>
          <dist:conditions>
              <dist:device-feature dist:name="android.hardware.camera.ar"/>
              ...
          </dist:conditions>
      </dist:install-time>
  </dist:delivery>

模块创建后可以在这里添加新的分发条件。

这里也可以根据国家/地区指定条件

<dist:conditions>
   <!-- Set to "true" to specify countries to exclude from downloading
   this module at app install-time. By default, modules are available
   for download to all user countries. -->
  <dist:user-countries dist:exclude="true">
    <!-- Specifies the two-letter  CLDR country code for regions that should
    not download the module at app install-time. -->
    <dist:country dist:code="CN"/>
    <dist:country dist:code="HK"/>
  </dist:user-countries>
</dist:conditions>

根据 API 级别指定条件

<dist:conditions>
    <!-- Specifies the minimum API level that the device must satisfy
         in order to download your module at app install-time. The API level you
         specify must be greater or equal to the module's own minSdkVersion. -->
   <dist:min-sdk dist:value="21"/>
    <!-- Specifies the maximum API level that the device cannot exceed
         in order to download your module at app install-time. The API level you
         specify must be less than or equal to the module's own maxSdkVersion. -->
   <dist:max-sdk dist:value="24"/>
</dist:conditions>

关联主模块

当 Android Studio 创建功能模块时,它会自动在基本模块的 build.gradle 文件添加 android.dynamicFeatures 属性,以使该功能模块对基本模块可见,如下所示:

// In the base module’s build.gradle file.
android {
    defaultConfig {
        ...
    }
    ...
    // Specifies feature modules that have a dependency on
    // this base module.
    dynamicFeatures = [":dynamic_feature", ":dynamic_feature2"]
}

此外,Android Studio 还会将基本模块添加为功能模块的依赖项,如下所示:

// In the feature module’s build.gradle file:
...
dependencies {
    ...
    // Declares a dependency on the base module, ':app'.
    implementation project(':app')
}

部署应用

在开发支持功能模块的应用时,您可以像往常一样,从菜单栏中依次选择 Run > Run(或点击工具栏中的 Run 图标 ),将该应用部署到连接的设备。

如果您的应用项目包含一个或多个功能模块,您可以通过修改现有的运行/调试配置以选择需要在部署应用时包含的功能,具体操作步骤如下:

  • 从菜单栏中依次选择 Run > Edit Configurations。

  • 在 Run/Debug Configurations 对话框的左侧面板中,选择所需的 Android App 配置。

  • 在 General 标签页中的 Dynamic features to deploy 下,选中需要在部署应用时包含的每个功能模块旁边的复选框。

  • 点击 OK。

image.png

默认情况下,Android Studio 不会使用 app bundle 部署您的应用,而是由 IDE 构建针对部署速度(而非 APK 大小)进行了优化的 APK,并将其安装到设备中。如需将 Android Studio 配置为通过 app bundle 构建和部署 APK 以及免安装体验,请修改运行/调试配置。

按需分发

上面我们提到了三种分法模式:安装时分发,按需分发和按条件分发。

安装时分发比较好理解,即在google play下载安装应用的时候就会下载该模块;按条件分发也比较好理解,下载安装应用时,如果当前设备满足条件才会下载该模块;只有按需分发,并不是很容易理解。

按需分发与国内很多应用的插件化类似,就是在功能被使用的时候才会去下载那部分代码和资源。官方描述是这样的:

如果在使用购物平台应用的用户中,只有 20% 的人发布待售商品,有一个不错的策略可以减少大多数用户的初始下载大小,那就是将拍照、输入商品描述及上架商品的功能配置为按需下载。也就是说,您可以为应用的销售功能配置功能模块,使该功能仅在用户希望在购物平台上销售商品时才下载。此外,如果用户在一段时间后不再出售商品,应用可以通过请求卸载该功能减小其安装大小。

但是通过上面创建部署动态模块的流程,我们发现没有关于按需下载的这部分逻辑。这是因为如果要使用动态模块,还需要配合google的play core library才可以:

implementation “com.google.android.play:core:${versions.playcore}”

通过这个library中的SplitInstallManager这个类来实现判断模块是否安装,如果没有安装则下载安装等逻辑。

这部分内容很多,受本文篇幅所限就不展开了,官方有一篇很详细的教程有兴趣的同学可以自行阅读(需要梯子)。以后有时间我会单独出一篇中文教程,请大家留意我的公众号更新。

免安装分发

还有一种动态功能模块,即免安装分发。Google Play 免安装体验让用户无需在设备上安装 APK 即可与您的应用互动。用户可以通过 Google Play 商店中的“立即体验”按钮或您创建的网址体验您的应用。这种内容分发形式可让您更轻松地提高用户与应用的互动度。

仅当应用的基本模块也支持免安装体验时,您才能使功能支持免安装体验。这是因为,如果用户想要体验您的应用中某个支持免安装体验的功能模块,他们的设备还必须下载应用的基本模块以获取通用代码和资源。请注意,为了支持 Google Play 免安装体验,基本模块和功能的下载必须满足几个条件:

最大大小:支持免安装体验的基本模块和支持免安装体验的功能模块的总大小不得超过 10 MB。如需了解详情,请阅读通过缩减应用或游戏大小支持免安装体验。

后台活动:支持免安装体验的模块无法使用后台服务。此外,此类模块在后台运行时无法发送通知。

如果您使用 Android Studio 3.5 或更高版本创建支持免安装体验的功能模块(如本部分所述),IDE 会在每个模块的清单中添加以下内容,从而自动使基本模块和功能模块支持免安装体验:

<manifest xmlns:dist="http://schemas.android.com/apk/distribution"
    ... >
    <dist:module dist:instant="true" />
    ...

此外,在下载并安装您的应用时,系统会随该应用的基本 APK 自动下载并安装支持免安装体验的功能模块。因此,IDE 还会在支持免安装体验的功能模块中添加以下内容。

    <dist:module ...>
        <dist:delivery>
            <dist:install-time />
        </dist:delivery>
    </dist:module>

此行为意味着,当您设置 dist:instant=“true” 时,不能同时包含<dist:on-demand />。不过,您可以根据需要使用 Play Core 库在免安装体验中请求支持免安装体验的模块。

创建免安装模块

与创建动态模块过程一样,只不过有几处不同:

  1. 创建选择module类型的时候,选Instant Dynamic Feature Module

  2. 没有选择分发时机的选项了,所以第四步Module Download Options没有了,剩余的选项合并到第三步Configure your new module中了,所以Configure your new module设置完就finish了。

部署应用

部署与其他动态功能模块一样。

总结

Android App Bundle可以有效的减少安装包体积,而如果利用好动态功能模块(Dynamic Feature Module)更是可以极大的压缩安装包大小,这样可以大幅减少用户下载安装的等待时间,用户可以更快的体验到应用,提高用户的体验。但是动态功能模块(尤其按需分发)一定要设计好,否则用户在使用过程中还要频繁的下载模块也会降低用户体验。


当然说到打包一定离不开签名,详细可以看博客《细说Android apk四代签名:APK v1、APK v2、APK v3、APK v4》

<think>我们正在讨论使用Android官方动态功能模块Dynamic Feature Module)来替代手动Dex加载的方案。动态功能模块允许按需下载和安装应用的特定功能,从而减小初始APK的大小。在动态功能模块中,代码和资源被打包在单独的模块中,主模块可以在需要时加载这些模块。 根据引用[1]和[3],我们知道动态加载技术允许主应用在运行时加载插件模块,而不需要静态包含。同时,引用[3]提供了一个典型的Gradle配置示例,我们可以在此基础上进行扩展。 使用动态功能模块,我们不需要手动处理Dex文件的加载(如使用DexClassLoader等),因为Play Core库提供了加载和安装模块的API。 以下是使用动态功能模块的步骤: 1. 在项目中创建动态功能模块。 2. 配置主模块和动态功能模块的依赖关系。 3. 使用Play Core库请求安装动态模块。 4. 在模块安装完成后,加载并使用其中的代码。 示例代码: 1. 首先,在项目的`build.gradle`文件中添加Google Maven仓库和Play Core库的依赖(如果尚未添加): ```groovy buildscript { repositories { google() // 其他仓库 } dependencies { classpath 'com.android.tools.build:gradle:7.0.0' // 或更高版本 } } ``` 2. 在app模块的`build.gradle`中,添加对Play Core库的依赖: ```groovy dependencies { implementation 'com.google.android.play:core:1.10.3' // 使用最新版本 // 其他依赖 } ``` 3. 创建动态功能模块: - 在Android Studio中,选择File > New > New Module,然后选择“Dynamic Feature Module”。 - 按照向导操作,注意选择的最小API级别需要与主模块一致或兼容。 4. 假设我们创建了一个名为`:dynamicfeature`的动态功能模块,其中包含一个名为`DynamicClass`的类,该类有一个方法`doSomething()`。 5. 在动态功能模块中,确保你有一个公开的类,例如: ```java package com.example.dynamicfeature; public class DynamicClass { public String doSomething() { return "Hello from Dynamic Feature!"; } } ``` 6. 在主模块中,我们需要请求安装动态功能模块。以下是一个示例Activity代码: ```java import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; import com.google.android.play.core.splitinstall.SplitInstallManager; import com.google.android.play.core.splitinstall.SplitInstallManagerFactory; import com.google.android.play.core.splitinstall.SplitInstallRequest; import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener; import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus; import java.lang.reflect.Method; public class MainActivity extends AppCompatActivity { private SplitInstallManager splitInstallManager; private SplitInstallStateUpdatedListener listener; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); splitInstallManager = SplitInstallManagerFactory.create(this); // 创建安装请求,指定模块名(在build.gradle中配置的dynamic feature模块的名字) SplitInstallRequest request = SplitInstallRequest.newBuilder() .addModule("dynamicfeature") // 模块名 .build(); // 注册监听器(可选,用于跟踪安装状态) listener = state -> { if (state.status() == SplitInstallSessionStatus.INSTALLED) { // 模块安装成功,可以加载模块中的类 loadAndUseDynamicModule(); } }; splitInstallManager.registerListener(listener); // 开始安装 splitInstallManager.startInstall(request) .addOnSuccessListener(sessionId -> { // 安装请求已开始 }) .addOnFailureListener(exception -> { // 处理错误 }); } private void loadAndUseDynamicModule() { // 使用反射加载动态模块中的类 try { // 注意:必须使用完整的类名 Class<?> dynamicClass = Class.forName("com.example.dynamicfeature.DynamicClass"); Object instance = dynamicClass.newInstance(); Method doSomethingMethod = dynamicClass.getMethod("doSomething"); String result = (String) doSomethingMethod.invoke(instance); // 使用result做点什么 } catch (Exception e) { e.printStackTrace(); } } @Override protected void onDestroy() { super.onDestroy(); splitInstallManager.unregisterListener(listener); } } ``` 注意:上述代码中使用了反射来加载类,因为动态功能模块在安装前,主模块无法直接引用其中的类。另一种方法是使用接口来解耦,即主模块中定义一个接口,动态功能模块中的类实现该接口。这样,主模块只需要通过反射创建实例,然后将其转换为接口即可。 7. 在动态功能模块的`build.gradle`中,确保已经正确配置: ```groovy apply plugin: 'com.android.dynamic-feature' android { compileSdk 33 defaultConfig { minSdk 21 targetSdk 33 } // 其他配置 } dependencies { implementation project(':app') // 主模块 } ``` 8. 还需要在主模块的`build.gradle`中添加动态功能模块的配置: ```groovy android { ... dynamicFeatures = [':dynamicfeature'] // 添加动态功能模块 } ``` 9. 另外,注意动态功能模块在打包时可以选择分发方式(按需安装、立即安装等),这里我们使用的是按需安装。 重要:使用动态功能模块需要发布到Google Play,并且设备上必须安装Play Store。在开发时,可以通过内部测试轨道进行测试。 以上代码示例展示了如何使用动态功能模块动态加载代码。与手动Dex加载相比,这种方式更加安全,且由Google官方支持,能够更好地处理模块的下载、安装和更新。 注意:由于动态功能模块在安装后会被合并到主应用中,因此可以通过类名直接加载,无需使用DexClassLoader。 参考文献: [^1]: 在插件化开发中,主应用可能需要加载并运行插件模块中的代码。动态加载技术提供了这样的可能性,允许主应用在运行时解析并加载插件模块,而不需要在主应用中静态地包含这些模块。 [^3]: 实战例子:下面是一个典型的app/build.gradle配置: android { compileSdk 33 defaultConfig { applicationId "com.example.myapp" minSdk 21 targetSdk 33 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } dependencies { implementation 'androidx.core:core:1.10.0' } §§相关问题§§ 1. 动态功能模块在安装后如何更新? 2. 动态功能模块支持哪些分发方式? 3. 如何处理动态功能模块安装失败的情况?</think>### 使用动态功能模块加载 DEX 的示例代码 在 Android 官方动态功能模块方案中,不再需要手动处理 `DexClassLoader`,而是通过 Play Core Library 实现模块化加载。以下是完整的实现流程: #### 1. 添加 Gradle 依赖 ```groovy // app/build.gradle android { dynamicFeatures = [':dynamic_feature'] // 声明动态模块 } dependencies { implementation 'com.google.android.play:core:1.10.3' // Play Core 库 } ``` #### 2. 创建动态功能模块Android Studio 中: 1. File > New > New Module 2. 选择 "Dynamic Feature Module" 3. 命名模块(如 `:dynamic_feature`) #### 3. 动态加载代码实现 ```java // 主模块中 public class ModuleLoader { private final SplitInstallManager installManager; public ModuleLoader(Context context) { installManager = SplitInstallManagerFactory.create(context); } public void loadDynamicModule(String moduleName) { // 检查模块是否已安装 if (installManager.getInstalledModules().contains(moduleName)) { initializeModule(moduleName); return; } // 请求模块安装 SplitInstallRequest request = SplitInstallRequest.newBuilder() .addModule(moduleName) .build(); installManager.startInstall(request) .addOnSuccessListener(sessionId -> Log.d("DynamicModule", "Installation started") ) .addOnFailureListener(e -> Log.e("DynamicModule", "Install failed: " + e.getMessage()) ); } private void initializeModule(String moduleName) { try { // 加载动态模块中的类 Class<?> dynamicClass = Class.forName( "com.example.dynamic_feature.DynamicImpl" ); // 调用模块功能 Method initMethod = dynamicClass.getMethod("initialize"); initMethod.invoke(null); } catch (Exception e) { Log.e("DynamicModule", "Initialization failed: " + e.getMessage()); } } } ``` #### 4. 动态模块中的实现 ```java // dynamic_feature 模块中 public class DynamicImpl { public static void initialize() { // 动态加载的代码逻辑 System.out.println("动态模块已成功加载!"); } } ``` #### 5. 配置清单文件 ```xml <!-- 主模块 AndroidManifest.xml --> <application> <meta-data android:name="com.google.android.play.billingclient.version" android:value="2.0.0" /> </application> <!-- 动态模块 AndroidManifest.xml --> <dist:module dist:instant="false" dist:title="@string/module_name"> <dist:delivery> <dist:on-demand /> <!-- 按需加载 --> </dist:delivery> </dist:module> ``` ### 关键优势 1. **自动依赖管理**:Google Play 处理模块依赖和更新[^1] 2. **安全隔离**:模块在独立 ClassLoader 中运行 3. **资源分离**:动态模块资源独立编译(R8 自动优化)[^2] 4. **体积优化**:支持配置 `enableSeparateBuildPerVariant = true` 减少分发体积[^3] ### 注意事项 1. 动态模块需通过 Google Play 分发 2. 最小 API 要求 21 (Android 5.0+) 3. 测试时需使用内部测试轨道或 `bundletool` 生成 APKS > 此方案替代了传统 `DexClassLoader` 方式,通过官方支持机制实现更安全的动态加载[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BennuCTech

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值