文章目录
- [Android 多渠道打包总结](#Android 多渠道打包总结)
Android 多渠道打包总结
概述
多渠道打包是指为同一个 Android 应用生成多个不同渠道的安装包,每个渠道包具有唯一的标识,用于区分不同的发布渠道(如应用商店、推广渠道等)。
定义多渠道
在 app/build.gralde 中配置:
groovy
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace 'com.test.myapplication'
compileSdk 35
defaultConfig {
applicationId "com.test.myapplication"
minSdk 27
targetSdk 35
versionCode 1
versionName "1.0"
}
// 签名配置
signingConfigs {
release {
storeFile file("./myapp.jks")
storePassword "123456"
keyAlias "app"
keyPassword "123456"
}
debug {
storeFile file("./myapp.jks")
storePassword "123456"
keyAlias "app"
keyPassword "123456"
}
}
// 构建类型
buildTypes {
release {
signingConfig signingConfigs.debug
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-debug"
signingConfig signingConfigs.debug
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
// 定义2个维度
flavorDimensions "channel", "seo"
productFlavors {
// 应用市场渠道
huawei {
dimension "channel"
}
xiaomi {
dimension "channel"
}
// 推广渠道
wechat {
dimension "seo"
}
douyin {
dimension "seo"
}
}
// 配置APK命名规则
applicationVariants.all { variant ->
variant.outputs.each { output ->
// 获取构建类型
def buildType = variant.buildType.name
// 获取渠道名称
def flavorName = variant.flavorName
// 获取版本信息
def versionName = defaultConfig.versionName
def versionCode = defaultConfig.versionCode
// 获取当前日期
def nowDate = new Date().format("yyyyMMddHHmm")
// 创建文件名
def fileName = "app_${flavorName}_${buildType}_v${versionName}_${versionCode}_${nowDate}.apk"
// 兼容写法
if (output.properties.containsKey('outputFileName')) {
output.setProperty('outputFileName', fileName)
} else {
output.outputFileName = fileName
}
}
}
}
dependencies {
implementation libs.androidx.core.ktx
implementation libs.androidx.appcompat
implementation libs.material
implementation libs.androidx.activity
implementation libs.androidx.constraintlayout
testImplementation libs.junit
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.espresso.core
}
配置说明:
- namespace:命名空间
- signingConfigs:配置签名
- buildTypes:配置构建类型
- flavorDimensions:配置渠道维度
- productFlavors:配置渠道
- applicationVariants:配置apk命名规则
- compileOptions:配置Java编译项
- kotlinOptions:配置Kotlin编译项
buildConfigField
buildConfigField 是 Gradle 提供的一种机制,用于在构建过程中生成 BuildConfig.java 文件中的静态常量字段。这些字段可以直接在代码中访问。
语法
buildConfigField "类型", "字段名", "值"
- 类型:支持 Java 基本数据类型和 String 类型:boolean、int、long、float、double、short、byte、char、String、String[]
- 字段名:生成静态常量字段名,使用驼峰命名法
- 值:字段的具体值
groovy
buildConfigField "int", "COUNT", "3"
buildConfigField "boolean", "MODE", "true"
buildConfigField "long", "time", "3000L"
buildConfigField "String", "URL", "www.baidu.com"
buildConfigField "String[]", "LANGUAGES", '{"中文","英文","日语"}'
优先级规则
buildTypes > productFlavors > defaultConfig
开启
在较新的 Android Gradle 插件版本(尤其是与 Gradle 8 配合使用时), BuildConfig 生成功能默认被禁用 。这是 Android Gradle 插件为了优化构建性能,避免生成不必要的文件而引入的变更。
groovy
buildFeatures {
buildConfig true
}
用法
在defaultConfig中定义
适用于所有渠道:
groovy
defaultConfig {
buildConfigField "String", "USER_NAME", '"小明"'
buildConfigField "int", "USER_AGE", "18"
buildConfigField "boolean", "USER_SEX", "true"
buildConfigField "String[]", "USER_ADDRESS", '{"北京市","朝阳区"}'
}
在productFlavors中定义
适用于特定渠道:
groovy
productFlavors {
huawei {
dimension "channel"
buildConfigField "String", "USER_NAME", '"我是华为"'
buildConfigField "int", "USER_AGE", "28"
buildConfigField "boolean", "USER_SEX", "true"
buildConfigField "String[]", "USER_ADDRESS", '{"广东省","深圳市"}'
}
xiaomi {
dimension "channel"
buildConfigField "String", "USER_NAME", '"我是小米"'
buildConfigField "int", "USER_AGE", "38"
buildConfigField "boolean", "USER_SEX", "false"
buildConfigField "String[]", "USER_ADDRESS", '{"湖北省","武汉市"}'
}
}
在buildTypes中定义
适用于指定构建类型:
groovy
buildTypes {
release {
buildConfigField "String", "USER_NAME", '"我是release"'
}
debug {
buildConfigField "String", "USER_NAME", '"我是debug"'
}
}
编译后生成BuildConfig.java
java
/**
* Automatically generated file. DO NOT MODIFY
*/
package com.test.myapplication;
public final class BuildConfig {
public static final boolean DEBUG = Boolean.parseBoolean("true");
public static final String APPLICATION_ID = "com.test.myapplication.debug";
public static final String BUILD_TYPE = "debug";
public static final String FLAVOR = "huaweiDouyin";
public static final String FLAVOR_channel = "huawei";
public static final String FLAVOR_seo = "douyin";
public static final int VERSION_CODE = 1;
public static final String VERSION_NAME = "1.0-debug";
// Field from product flavor: huawei
public static final String[] USER_ADDRESS = {"广东省","深圳市"};
// Field from product flavor: huawei
public static final int USER_AGE = 28;
// Field from build type: debug
public static final String USER_NAME = "我是debug";
// Field from product flavor: huawei
public static final boolean USER_SEX = true;
}
在代码中使用
kotlin
val userName = BuildConfig.USER_NAME
val userAge = BuildConfig.USER_AGE
val userSex = BuildConfig.USER_SEX
val userAddress = BuildConfig.USER_ADDRESS
Log.e("TAG", "userName: $userName")
Log.e("TAG", "userAge: $userAge")
Log.e("TAG", "userSex: $userSex")
for (address in userAddress) {
Log.e("TAG", "address: $address")
}
resValue
resValue 是 Gradle 提供的一种机制,用于在构建过程中生成字符串资源。这些资源会被添加到应用的 strings.xml 文件中,可以在布局文件和代码中直接使用,支持国际化。
语法
resValue "类型", "资源名", "值"
-
类型:支持 string、integer、bool、color、dimen
-
资源名:生成资源的名称
-
值:资源的具体值
resValue "string", "name", "小明"
resValue "integer", "age", "18"
resValue "bool", "sex", "true"
resValue "color", "primary_color", "#ff0000"
resValue "dimen", "height", "48dp"
用法
在defaultConfig中定义
groovy
defaultConfig {
resValue "string", "name", "小明"
resValue "integer", "age", "18"
resValue "bool", "sex", "true"
resValue "color", "primary_color", "#ff0000"
resValue "dimen", "height", "48dp"
}
在productFlavors中定义
groovy
productFlavors {
huawei {
dimension "channel"
resValue "string", "name", "小华"
resValue "integer", "age", "28"
resValue "bool", "sex", "false"
resValue "color", "primary_color", "#00ff00"
resValue "dimen", "height", "58dp"
}
xiaomi {
dimension "channel"
resValue "string", "name", "小米"
resValue "integer", "age", "38"
resValue "bool", "sex", "true"
resValue "color", "primary_color", "#0000ff"
resValue "dimen", "height", "68dp"
}
}
在buildType中定义
groovy
buildTypes {
release {
resValue "string", "name", "release"
resValue "integer", "age", "48"
resValue "bool", "sex", "true"
resValue "color", "primary_color", "#ff00ff"
resValue "dimen", "height", "78dp"
}
debug {
resValue "string", "name", "debug"
resValue "integer", "age", "58"
resValue "bool", "sex", "true"
resValue "color", "primary_color", "#ff00ff"
resValue "dimen", "height", "88dp"
}
}
在代码中使用
kotlin
val name = resources.getString(R.string.name)
val age = resources.getInteger(R.integer.age)
val sex = resources.getBoolean(R.bool.sex)
val primaryColor = resources.getColor(R.color.primary_color, theme)
val height = resources.getDimension(R.dimen.height)
在布局中使用
xml
<TextView
android:layout_width="wrap_content"
android:layout_height="@dimen/height"
android:text="@string/name"
android:textColor="@color/primary_color" />
manifestPlaceholders
manifestPlaceholders 是 Gradle 提供的一种机制,用于在构建过程中替换 AndroidManifest.xml 文件中的占位符。它允许开发者在 build.gradle 中定义键值对,在 AndroidManifest.xml 中使用 ${} 语法使用这些值。
语法
groovy
manifestPlaceholders = [
键名1: "值1",
键名2: "值2",
...
]
- 键名:在 AndroidManifest.xml 中占位符的名称
- 值:占位符的具体值
用法
在defaultConfig中定义
groovy
defaultConfig {
manifestPlaceholders = [
APP_NAME : "我的APP",
APP_VERSION: "1.1.1",
APP_URL : "http://www.baidu.com"
]
}
在productFlavors中定义
groovy
productFlavors {
// 应用市场渠道
huawei {
dimension "channel"
manifestPlaceholders = [
APP_NAME : "我的华为",
APP_VERSION: "2.2.2",
APP_URL : "http://www.huawei.com"
]
}
xiaomi {
dimension "channel"
manifestPlaceholders = [
APP_NAME : "我的小米",
APP_VERSION: "3.3.3",
APP_URL : "http://www.xiaomi.com"
]
}
}
在buildTypes中定义
groovy
buildTypes {
release {
manifestPlaceholders = [
APP_NAME : "我的release",
APP_VERSION: "4.4.4",
APP_URL : "http://www.release.com"
]
}
debug {
manifestPlaceholders = [
APP_NAME : "我的debug",
APP_VERSION: "5.5.5",
APP_URL : "http://www.debug.com"
]
}
}
在AndroidManifest.xml中使用
xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:label="${APP_NAME}">
<meta-data
android:name="APP_VERSION"
android:value="${APP_VERSION}" />
<meta-data
android:name="APP_URL"
android:value="${APP_URL}" />
</application>
</manifest>
在代码中使用
kotlin
fun getMetaDataValue(key: String): String? {
return try {
val appInfo = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
appInfo.metaData.getString(key)
} catch (e: Exception) {
Log.e("TAG", "获取 meta-data 失败")
null
}
}
val appVersion = getMetaDataValue("APP_VERSION")
val appUrl = getMetaDataValue("APP_URL")
Log.e("TAG", "appVersion: $appVersion")
Log.e("TAG", "appUrl: $appUrl")
定制资源
优先级规则
- 资源文件 :构建类型资源(release、debug) > 渠道资源(a渠道、b渠道) > 主包资源(main目录) > 依赖库资源
- 代码文件 :构建系统会将所有源集(main、渠道包)的代码合并到同一个编译环境中,当存在 包名+类名完全相同 的类时,编译器会报"重复声明"(Redeclaration)错误,而不是覆盖。
渠道包目录结构
project/
├── app/
│ ├── build.gradle # 应用级构建配置
│ ├── src/
│ │ ├── main/ # 主包(默认资源)
│ │ │ ├── java/com/example/multichanneldemo/ # 默认 Java/Kotlin 代码
│ │ │ │ ├── MainActivity.kt # 主活动
│ │ │ │ ├── AppConfig.kt # 应用配置
│ │ │ │ └── MetaDataUtil.kt # 元数据工具类
│ │ │ ├── res/ # 默认资源文件
│ │ │ │ ├── drawable/ # 默认图标
│ │ │ │ ├── layout/ # 默认布局
│ │ │ │ │ └── activity_main.xml # 主布局
│ │ │ │ └── values/ # 默认字符串
│ │ │ │ ├── strings.xml # 字符串资源
│ │ │ │ └── styles.xml # 样式资源
│ │ │ ├── assets/ # 默认资产文件
│ │ │ │ └── channels.txt # 渠道列表文件
│ │ │ └── AndroidManifest.xml # 默认清单文件
│ │ ├── huawei/ # 华为渠道资源
│ │ │ ├── java/com/example/multichanneldemo/ # 华为特定代码
│ │ │ │ └── HuaweiChannelUtil.kt # 华为渠道工具类
│ │ │ ├── res/ # 华为特定资源
│ │ │ │ ├── drawable/ # 华为特定图标
│ │ │ │ └── values/ # 华为特定字符串
│ │ │ │ └── strings.xml # 华为特定字符串
│ │ │ └── AndroidManifest.xml # 华为特定清单文件
│ └── build/ # 构建输出目录
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar # Gradle 包装器
│ └── gradle-wrapper.properties # Gradle 包装器配置
├── build.gradle # 项目级构建配置
├── settings.gradle # 项目设置
├── gradle.properties # Gradle 属性配置
└── local.properties # 本地配置(不提交到版本控制)
- 每个渠道都可以有自己的资源目录,结构与 main 目录相同。
- 渠道特定代码会与 main 目录中的代码合并,会覆盖 main 目录中的同名资源。
定制字符串资源
小米特定渠道:
在 app/src/xiaomi/res/values/string.xml:
xml
<resources>
<string name="app_name">小米APP</string>
<string name="hello">hello小米</string>
</resources>
定制图片资源
小米特定渠道:
在 app/src/xiaomi/res/drawable 添加同名的图片。
定制代码
buildConfigField+条件判断
配置 build.gradle:
groovy
productFlavors {
huawei {
dimension "channel"
buildConfigField "String", "CHANNEL_ID", '"huawei"'
}
xiaomi {
dimension "channel"
buildConfigField "String", "CHANNEL_ID", '"xiaomi"'
}
}
主包中定义工具类:
kotlin
object Utils {
fun sayHello() {
Log.e("TAG", "hello APP")
when (BuildConfig.CHANNEL_ID) {
"xiaomi" -> Log.e("TAG", "hello 小米")
"huawei" -> Log.e("TAG", "hello 华为")
}
}
}
接口+渠道特定代码
在主包中定义接口:
kotlin
interface BaseUtils {
fun sayHello()
}
在渠道包中实现特定代码:
kotlin
object ImplUtils : BaseUtils {
override fun sayHello() {
Log.e("TAG", "hello 小米")
}
}