使用播放功能交付

什么是功能交付?

谷歌的开发者文档详细介绍了这一功能,它主要是允许开发者以 Google Play 商店可以将应用内容拆分成多个可下载包的方式来组织项目。它还能让开发者控制向用户交付内容的方式。这些拆分的软件和内容包将通过 Android 应用程序包 (AAB) 发送到 Google Play 商店。

示例项目:FDMapLoader

此应用程序使用播放功能交付,在用户请求时向其提供图片。可以轻松修改该应用程序,创建一个超过 200MB 的应用程序,以测试应用程序大小限制,并使用大型功能交付模块进行下载。

构建设置

  • fdwintermapmodule:的功能模块项目Qt Creator
  • fdmaploader:的主应用程序项目Qt Creator
  • fdmaploader-android-project:用于编译 Android 应用程序包的 Android 项目。

编译 FDWintermapModule - 功能模块

加载fdwintermapmodule项目到Qt Creator ,为目标平台进行配置,然后构建。

编译 FDMapLoader - 主应用程序

fdmaploader载入Qt Creator ,并根据目标平台进行配置,然后构建。

修改 Android 项目

  • 将功能模块和主应用程序构建目录中的已构建二进制文件(.so)复制到 Android 项目中相应的库目录。(app/src/main/jniLibs/[target ABI]/fdwintermapmodule/src/main/jniLibs/[target ABI]/ )
  • 复制其他已更改的文件,如资源文件和 Java 类。(.../res/....../src/main/java/... 文件夹)
  • 使用 Android Studio 或通过命令行使用 Gradle 封装程序(./gradlew bundle)构建 Android 项目
  • 要进行测试,可以使用bundletool 从 bundle 创建 APK 并将本地测试版本安装到设备上。Bundletool 是 Google 提供的一个工具,我们将在本文档的稍后部分介绍它的使用。为了测试和发布,您可以将捆绑包上传到 Google Play 控制台。

添加自己的内容

本示例旨在让开发者可以轻松添加自己的内容来测试功能交付。为超出 Play Store 最大软件包大小,可在 FDMapLoader 和 FDWintermapModule 的图像文件夹中添加地图图像(不一定是地图图像,但要符合本示例的主题),图像名称也必须添加到images.qrc 文件中。

创建您自己的

以下各节将介绍如何创建自己的项目来使用播放功能交付。建议以示例为基础。在该项目中,使用的是 Qt 6.7.2。

功能模块

功能交付会像处理普通共享库一样处理 C++ 库,这些库在运行时可能可用,也可能不可用。在调用库之前,必须检查库的可用性。

  • 使用Qt Creator 创建 C++ 共享库。
  • 实现功能并添加资源。
  • 构建以创建 .so 二进制文件。

主应用程序(Qt)

  • 使用Qt Creator 创建应用程序(此处使用的是Qt Quick 项目模板)。
  • 实现对功能交付库的访问。Google Play Feature Delivery Java 库的中心类是SplitInstallManager
  • Android 模板文件可通过 QtCreatorProjects -> Build&Run -> [target ABI] -> Build Steps -> Build Android APK 上的 "创建模板 "按钮创建
  • 模板在项目中的 "android "文件夹中创建。
  • 将 Java 文件添加到.../android/src/java/[package...] 文件夹,并将文件路径添加到CMakeLists.txt
    qt_add_executable...
    ...[path]/[java-filename.java]
    ...
  • 在该示例中,创建了一个 Java 类来处理调用和回调。然后将使用 JNI 从 Qt 访问 Java 类。Android 文档对如何请求模块有一个简单的描述。
  • 在项目的 android 文件夹下添加 Java 文件时,必须将QT_ANDROID_PACKAGE_SOURCE_DIR属性添加到CMakeLists.txt
    ...
    set_property(TARGET appFDMainApp APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR
                 ${CMAKE_CURRENT_SOURCE_DIR}/android)
    ...
  • 此外,主应用程序build.gradle 必须有功能应用程序接口的依赖项:在依赖项块中,替换为
    implementation 'androidx.core:core:1.13.1'

    替换为

    implementation("com.google.android.play:feature-delivery:2.1.0")
  • 实现对功能模块提供的库的访问。由于功能模块可能对主应用程序可用,也可能不可用,因此模块在构建时不会链接,必须在运行时解决对模块的调用。示例
    typedef void* (*LoadModuleRulesFunc)();
    LoadModuleRulesFunc loadModuleRules =
        (LoadModuleRulesFunc) mFDModuleLibrary.resolve("loadModuleRules");
    if (loadModuleRules) {
        void* result = loadModuleRules();
        QScopedPointer<QString> resultStr{static_cast<QString*>(result)};
    }
  • 实现用户界面和主应用程序的其他必要部分。

功能模块(Qt)

  • 使用Qt Creator 创建应用程序(使用了 Qt XML C++ 库项目模板)。
  • 实现模块提供的功能。

安卓项目(安卓)

创建用于构建 Android 应用程序捆绑包以交付功能的项目主要基于 Android 文档:

手动或使用 Android Studio 创建一个 Android 项目(使用 "无活动 "模板)。修改后的项目包含一个顶级项目和两个子项目appfeature-module 。Android Studio 模板创建了app 子项目,feature-module 可使用File -> New -> New Module 模板添加。

模板项目需要进行几处修改:

  • 在主层build.gradle 添加功能交付插件:
    plugins {
        id 'com.android.application' version '8.5.2' apply false
        id 'com.android.dynamic-feature' version '8.5.2' apply false
        id 'com.android.library' version '8.5.2' apply false
    }
  • settings.gradle 中添加功能模块,必要时更改rootProject.name
    ...
    rootProject.name = "name-of-the-root-project"
    include(:app)
    include(:name-of-the-feature-module)

应用程序 - 子项目

  • Android 项目需要主应用程序项目中的 Qt 二进制文件:
    • 复制 Qt 构建中的本地库:[build directory]/android-build/libs/[target ABI]app/src/main/jniLibs/[target ABI]
    • 复制[build directory]/android-build/libs/ 中的 jars 到app/libs/
  • 从 Qt XML 构建中,res 文件夹、AndroidManifest.xmllocal.properties 中的内容也会被复制到 Android 项目中的相应位置。
  • app/src/main/res/values 文件夹中添加文件feature_names.xml ,其中包含功能模块的字符串:
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <string name="feature_module_name">name-of-the-feature-module-here</string>
    </resources>
  • 将文件keep.xml 添加到app/src/main/res/raw 文件夹,其中包含:
    <?xml version="1.0" encoding="utf-8"?>
    <resources xmlns:tools="http://schemas.android.com/tools"
        tools:keep="@string/feature_module_winter_map"
        tools:discard="" />

修改应用程序子项目的构建文件

复制到 Android 项目的构建文件需要进行一些修改。

app - 子项目

build.gradle
  • 移除buildScriptrepositories 区块。
  • 主应用程序build.gradle 中的 Android 区块需要进行一些修改:
    • defaultConfig
    • packagingOptions
    • dynamicFeatures
    • sourceSets
    • aaptOptions
    • dependencies
android {
...
  defaultConfig {
  ...
    applicationId "your-project-name-here"
  ...
  }
  packagingOptions.jniLibs.useLegacyPackaging true

  dynamicFeatures = [":your-dynamic-feature-name-here"]

  sourceSets {
    main {
      manifest.srcFile 'src/main/AndroidManifest.xml'
      java.srcDirs = [qtAndroidDir + '/src', 'src', 'java']
      aidl.srcDirs = [qtAndroidDir + '/src', 'src', 'aidl']
      res.srcDirs = [qtAndroidDir + '/res', 'res']
      resources.srcDirs = ['resources']
      renderscript.srcDirs = ['src']
      assets.srcDirs = ['assets']
      jniLibs.srcDirs = ['src/main/jniLibs/']
    }
  }

  // Do not compress Qt binary resources file
  aaptOptions {
    noCompress 'rcc'
  }
...
}

dependencies {
...
  implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
  implementation 'com.google.android.play:feature-delivery:2.1.0'
  implementation libs.material
...
}

同时在 android 块中添加签名配置:

android {
...
  signingConfigs {
    release {
      store

      \code
android {
...
  signingConfigs {
    release {
      storeFile file("/absolute/path/to/the/keystore.jks")
      storePassword "myStorePassword"
      keyAlias "myKeyAlias"
      keyPassword "myKeyPassword"
    }
  }
  buildTypes {
    release {
      signingConfig signingConfigs.release
      ...
    }
  }
...
}
gradle.properties

Qt XML 已在gradle.properties 中添加了项目变量。如有需要,请更改androidPackageName 的值。

AndroidManifest.xml
  • 移除package
    ...
    <manifest
    ...
      android:package... <--remove
    ...
    >
    ...
  • 如有需要,可更改labelandroid.app.lib_name
    ...
    <application ...
      android:label=" ...
      <activity ... >
        <meta-data android:name="android.app.lib_name" android:value=" ...
        />
    ...

功能模块 - 子项目

应用程序和功能模块是作为顶级 Android 项目的子项目创建的。文件夹和文件结构与应用程序子项目类似。

  • 从 Qt 构建的功能模块二进制文件被复制到[name-of-feature-module]/src/main/jniLibs/
  • 与主应用程序中一样,src/main/res/ 文件夹应包含xmlvalues 文件夹,分别包含qtprovider_paths.xmllibs.xml 。这两个文件都可以从应用程序项目中复制。
  • 如果src/main/res/ 文件夹包含 drawable 或 mipmap 文件夹,而功能不需要它们,则可以将其删除。
  • 在功能模块中,src/main/res/values 不应包含app_name 字段。在不需要 strings.xml 作其他用途的简单项目中,可将其删除。
  • libs.xml 只包含功能模块的名称:
    ...
        <array name="load_local_libs">
            <item>name-of-the-feature-module-here</item>
        </array>
    
        <string name="static_init_classes"></string>
        <string name="use_local_qt_libs">0</string>
        <string name="bundle_local_qt_libs">0</string>
    ...
  • AndroidManifest.xml 被添加到 目录中:src/main/
    <?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">
    
        <dist:module
            dist:instant="false"
            dist:title="@string/feature_module_title_string">
            <dist:delivery>
                <dist:on-demand />
            </dist:delivery>
            <dist:fusing dist:include="false" />
        </dist:module>
        <!-- This feature module does contain code. -->
        <application android:hasCode="true"/>
    </manifest>
  • 功能模块build.gradle 与应用程序项目中的功能模块非常相似,只是做了一些改动。下面是示例项目中的一个例子:
    plugins {
        id 'com.android.dynamic-feature'
    }
    
    dependencies {
        implementation project(':app')
        implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
        implementation 'com.google.android.play:feature-delivery:2.1.0'
    }
    
    android {
    
        namespace = androidPackageName
        compileSdk = androidCompileSdkVersion
        ndkVersion androidNdkVersion
    
        // Extract native libraries from the APK
        packagingOptions.jniLibs.useLegacyPackaging true
    
        defaultConfig {
            resConfig "en"
            minSdkVersion qtMinSdkVersion
            targetSdkVersion qtTargetSdkVersion
        }
    
        sourceSets {
            main {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                resources.srcDirs = ['resources']
                renderscript.srcDirs = ['src']
                assets.srcDirs = ['assets']
                jniLibs.srcDirs = ['src/main/jniLibs/']
           }
        }
    
        tasks.withType(JavaCompile) {
            options.incremental = true
        }
    
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
    
        lintOptions {
            abortOnError false
        }
    
        // Do not compress Qt binary resources file
        aaptOptions {
            noCompress 'rcc'
        }
    }
  • gradle.properties 文件可以从应用程序子项目中复制过来,将 改为功能模块包。androidPackageName

构建和部署

可使用 gradle 封装器在命令行中构建 AAB 捆绑程序:./gradlew bundle 生成的 AAB 将放在build/outputs/bundle/release (或debug )文件夹中。然后可将 AAB 复制到 Google Play Store 并发布测试。也可以通过bundletool--local-testing 参数在本地进行测试。Bundletool 文档

使用的 Bundletool 命令

  • 从捆绑包生成 APK:s:
    bundletool build-apks --bundle=/path/to/bundle.aab --output=/path/to/apk/package.apks --local-testing
  • 将应用程序安装到设备上:
    bundletool install-apks --apks=/path/to/apk/package.apks

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.