Android 多渠道打包多模块项目

387 阅读3分钟

多渠道背景

多渠道打包是指在同一份代码基础上,利用不同的渠道信息(如渠道名称、图标、应用名称、环境配置等)生成多个 APK 包。常见的用途包括:

  • 根据不同应用商店(比如华为、小米、应用宝、百度助手等)显示不同的渠道标识,以便统计用户安装来源;
  • 针对不同环境(开发、内测、线上)配置不同的服务器地址或特殊功能;
  • 根据渠道需求对依赖包(如第三方 SDK)进行有选择性的引入,确保不同版本的 APK 符合市场审核或功能需求

基本原理与配置

多渠道打包通常利用 Gradle 的 productFlavors 机制实现。每个 flavor 可以分别设置:

  • applicationIdSuffix:用于区分包名,比如 app.main.debug 与 app.main.release 可以共用同一份代码但在包名后缀不同;
  • manifestPlaceholders:可将渠道名称等变量注入 AndroidManifest.xml(例如用${UMENG_CHANNEL}来替换对应的渠道标识);
  • 资源文件覆盖:在项目中建立与渠道同名的源目录(如 src/baidusrc/xiaomi 等),以便重写主模块中的资源或代码。

配置多渠道打包

以Kotlin DSL(build.gradle.kts)为例

androidJunkCode {
    variantConfig {
        // prodRelease
        create("prodRelease") {
            packageBase = "包名"
            packageCount = 35
            activityCountPerPackage = 4
            excludeActivityJavaFile = false
            otherCountPerPackage = 55
            methodCountPerClass = 25
            resPrefix = "ai_"
            drawableCount = 350
            stringCount = 350
        }
    }
}
android {
    compileSdk = 33
    buildToolsVersion = "33.0.0"
    defaultConfig {
        applicationId = "com.example.multichannel"
        minSdk = 21
        targetSdk = 33
        versionCode = 1
        versionName = "1.0"
        // 确保所有 flavor 属于同一维度
        flavorDimensions += "channel"
    }

    // 配置构建类型
    buildTypes {
        getByName("release") {
            isMinifyEnabled = false
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
        getByName("debug") {
            // debug 不做混淆
            isMinifyEnabled = false
             proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }

    // 定义各渠道,每个渠道可以单独配置参数或资源
    productFlavors {
        create("prod") {
            dimension = "channel"
            // 渠道信息通过 manifestPlaceholders 传递
            manifestPlaceholders["UMENG_CHANNEL_VALUE"] = "prod"
            // 可以定制包名后缀、版本名后缀等
            applicationIdSuffix = ".prod"
            versionNameSuffix = "-prod"
            versionCode = android.defaultConfig.versionCode
            versionName = android.defaultConfig.versionName
            archivesName.value("name-v${versionName}-${versionCode}")

        }
        create("tst") {
            dimension = "channel"
            manifestPlaceholders["UMENG_CHANNEL_VALUE"] = "tst"
            applicationIdSuffix = ".tst"
            versionNameSuffix = "-tst"
            versionCode = android.defaultConfig.versionCode
            versionName = android.defaultConfig.versionName
            archivesName.value("name-v${versionName}-${versionCode}")
        }
        create("googlePlay") {
            dimension = "channel"
            manifestPlaceholders["UMENG_CHANNEL_VALUE"] = "googlePlay"
            applicationIdSuffix = ".googlePlay"
            versionNameSuffix = "-googlePlay"
            versionCode = android.defaultConfig.versionCode
            versionName = android.defaultConfig.versionName
            archivesName.value("name-v${versionName}-${versionCode}")
        }
    }

signingConfigs {
    create("release") {
        keyAlias = "密钥名"
        keyPassword = "密钥密码"
        storeFile = file("./AppKey.keystore")
        storePassword = "密钥密码"
    }

    getByName("debug") {
       keyAlias = "密钥名"
        keyPassword = "密钥密码"
        storeFile = file("./AppKey.keystore")
        storePassword = "密钥密码"
    }
}

// 统一对所有 productFlavors 设置占位符(若存在大量渠道,可使用遍历方式)
// productFlavors.all { flavor ->
//     flavor.manifestPlaceholders["UMENG_CHANNEL_VALUE"] = flavor.name
// }

// 定制输出的 APK 文件名称,例如 app-<渠道>-<buildType>-v<versionName>.apk
//    applicationVariants.all {
//        outputs.all {
//            val outputFile = outputFile
//            if (outputFile != null && outputFile.name.endsWith(".apk")) {
//                // 构造新的文件名,如:multichannel-xiaomi-release-v1.0.apk
//                val newName = "${defaultConfig.applicationId}-${flavorName}-${buildType.name}-v${defaultConfig.versionName}.apk"
//                outputFileName = newName
//            }
//        }
//    }
    
//开启了 ViewBinding 特性,可以通过生成的绑定类来直接操作视图,减少 findViewById 的使用
buildFeatures {
     viewBinding = true
      }
}


异常:

Unresolved reference: ads

问题:

image.png

多模块项目根据不同渠道加载模块

场景:同一个项目需要同时上架海外和国内,但是海外上架不允许引入国内的广告资源,但是国内的包又需要使用广告。这个时候就可以将广告变成单独的模块,然后通过判断该不该加载广告资源模块。下面以广告模块(ads)在googlePlay渠道包上为例

  1. 创建一个模块New->Module->Android Library

image.png

  1. 在一个公共模块中创建一个公共的广告接口,被其他模块实现

image.png

  1. 创建一个广告ads模块,里面包含广告方法的实现和资源的引入,实现公共接口AdsInterface

image.png

  1. 在ads同一层下创建一个空实现的广告模块emptyAd,里面实现和ads模块完全一样的包名和类名

image.png

  1. 在项目的gradle.properties里面创建一个INTERNATIONAL_VERSION,用来判断加载的是哪个模块,名字任意

image.png

  1. 创建一个公共模块ChannelBase,实现一个工厂方法,获取对应广告的初始值。这个方法最关键的是ads和emptyAd模块使用了同样的模块包名才可以不管你导入的是哪个模块都可以调用该模块下的方法

image.png

6.在公共模块ChannelBase的build.gradle.kts里面调用gradle.properties的值进行判断引入哪个模块资源

val international: Boolean by project.extra {
    (findProperty("INTERNATIONAL_VERSION") as? String)?.toBoolean() ?: false
}
plugins {
   
}
android {
}

dependencies {

   //这个是广告接口AdsInterface所在的类,必须引入才能在工厂类AdManagerFactory里面调用
    implementation(project(":core:tool-utils"))

   //根据gradle.properties中定义的值进行引入对应的模块资源
    if (international) {
        implementation(project(":core:emptyAd"))
    } else {
        implementation(project(":core:ads"))
    }
}


  1. 其他模块可通过工厂类直接调用广告,这样不管打的包里面有没有实现对应的广告方法都不会导致引用处报错
AdManagerFactory.csj().initADS()
AdManagerFactory.csj().loadSplashAd()