Android 多渠道打包实操(更改包名、图标、主题资源 、替换常量、第三方SDK Appkey配置)

时间:2024-05-19 11:20:22

最近在做一个通用版的会员系统,给予不同的公司使用,先前是通过切换版本分支来管理的,后面发现实在是繁琐和痛苦管理,仅仅是需要更改不同的常量、主题资源、包名、图标等等,主体代码逻辑功能基本不变。

先前了解过多渠道包的使用,其实这里完全可以通过 Gradle 的多渠道打包来这个痛点,期间也踩了坑,在这里做个记录

目录

一、通过 productFlavors 配置不同的渠道/环境

二、manifestPlaceholders 占位符使用

三、了解 ApplicationId 与 PackageName的区别

四、替换资源文件

五、打包和调试编译安装不同版本的渠道

以下为完整的实际项目配置这有两个渠道等同于给两家不同的公司会员 app 使用的配置



apply plugin: 'com.android.application'
def releaseTime() {
    return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
}
//加载本地文件
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.ablegenius.member"
        minSdkVersion 15
        targetSdkVersion 28
        versionCode 101
        versionName "1.0.101"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

        multiDexEnabled true

        ndk {
            //选择要添加的对应cpu类型的.so库。
            abiFilters "armeabi", "armeabi-v7a", "arm64-v8a", "x86", "arm64-v8a", "x86_64"
            //, 'mips', 'mips64'
        }

        // 渠道配置 gradle 3.0.0 以上需要有这个
        flavorDimensions "app"
    }

    signingConfigs {

        AblegeniusMemberConfig {
            //第一种:使用gradle直接签名打包
            /*  keyAlias 'dongwang'
            keyPassword '123123'
            storeFile file('src/main/WineverzhudiStoreFile.jks')
            storePassword '123123'*/

            //第二种:为了保护签名文件,把它放在local.properties中并在版本库中排除
            // ,不把这些信息写入到版本库中(注意,此种方式签名文件中不能有中文)

            storeFile file(properties.getProperty("keystroe_storeFile"))
            storePassword properties.getProperty("keystroe_storePassword")
            keyAlias properties.getProperty("keystroe_keyAlias")
            keyPassword properties.getProperty("keystroe_keyPassword")

            v2SigningEnabled false
        }

    }

    // 多渠道/多环境 的不同配置
    productFlavors {


        SatayKing {
            //此处的常量都会通过Gradle 在 BuildConfig.java 文件中生成 , 你可以直接在Class中使用 BuildConfig.XXXX 进行使用
            // 每个环境包名不同
            applicationId "com.ablegenius.member.satayking"
            // 动态添加 string.xml 字段;
            // 注意,如果在这添加,在 string.xml 不能有这个字段,会重名!!!这里使用资源文件覆盖的方式来处理应用名称
//            resValue "string", "app_name", "沙嗲王會員x"
            resValue "bool", "auto_updates", 'false'
            // 动态修改 常量 字段
            buildConfigField "String", "MAIN_H5_URL", '"https://xxxxxxx22/index.html"'
            //服務器請求地址
            buildConfigField "String", "SERVER_URL", '"https://cloudxxxx22/a"'
           //一些常量
            buildConfigField "String", "company", '"SatayKing"'
            buildConfigField "String", "serial", '"xxxxx"'
            buildConfigField "int", "ENVIRONMENTInt", '2'
            // 修改 AndroidManifest.xml 里渠道变量
            manifestPlaceholders = [CHANNEL_VALUE: "SatayKing"
                                    , app_icon   : "@mipmap/ic_launcher_shadiewang",
                                    //此方式可直接在 manifest 中通过 ${icon} 进行占位引用; 或者在main同级中创建不同渠道后创建 res 资源文件
                                    icon         : "@mipmap/ic_launcher_shadiewang",
                                    //极光相关
                                    JPUSH_PKGNAME: applicationId,
                                    JPUSH_APPKEY : "xxxxxxx", //JPush上注册的包名对应的appkey.
                                    JPUSH_CHANNEL: "developer-default", //暂时填写默认值即可.
                                    //Google Map 相关
                                    GoogleMapKey : "AIzaSyCLJ9Gng-xxxxx",

            ]

        }

        WineverHK {

            dimension "app"

            applicationId "com.ablegenius.member.wineverzhudi"

//            resValue "string", "app_name", "築地日本料理"
            resValue "bool", "auto_updates", 'true'
            resValue "drawable", "isrRank", 'true'
         
            buildConfigField "String", "MAIN_H5_URL", '"http://xxxxindex.html"'
            buildConfigField "String", "SERVER_URL", '"http://cloud.xxxx/a"'

            buildConfigField "String", "company", '"WineverHK"'
            buildConfigField "String", "serial", '"xxxx"'


            manifestPlaceholders = [CHANNEL_VALUE: "WineverHK"
                                    , app_icon   : "@mipmap/ic_launcher_zhudi",
                                    icon         : "@mipmap/ic_launcher_zhudi",

                                    JPUSH_PKGNAME: applicationId,
                                    JPUSH_APPKEY : "247aef555a20e8836d1ac361", //JPush上注册的包名对应的appkey.
                                    JPUSH_CHANNEL: "developer-default", //暂时填写默认值即可.

                                    GoogleMapKey : "AIzaSyCtAVjIVmGdnP44W2Nk8DjCT_OJISYUVxA",
            ]
        }


    }

    buildTypes {
        release {
            // release模式下,不显示log
            buildConfigField("boolean", "LOG_DEBUG", "false")
            // 为版本名添加后缀
//            versionNameSuffix "-relase"
            // 不开启混淆
            minifyEnabled false
            // 移除无用的resource文件
            shrinkResources false
            // 开启ZipAlign优化
            zipAlignEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

            signingConfig signingConfigs.AblegeniusMemberConfig


        }

        debug {

            // debug模式下,显示log
            buildConfigField("boolean", "LOG_DEBUG", "true")

            //为已经存在的applicationId添加后缀
//            applicationIdSuffix ".debug"
            // 为版本名添加后缀
            versionNameSuffix "-debug"
            // 不开启混淆
            minifyEnabled false
            // 不开启ZipAlign优化
            zipAlignEnabled false
            // 不移除无用的resource文件
            shrinkResources false

            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.AblegeniusMemberConfig
        }
    }

    // 3.0 gradle 批量打包
    android.applicationVariants.all { variant ->
        variant.outputs.all {
            //输出apk名称为:渠道名_版本名_时间.apk
            outputFileName = "${variant.productFlavors[0].name}Member_v${defaultConfig.versionName}_${releaseTime()}.apk"
        }
    }

    sourceSets {
        SatayKing { res.srcDirs = ['src/SatayKing/res', 'src/SatayKing/res/'] }
        WineverHK { res.srcDirs = ['src/WineverHK/res', 'src/WineverHK/res/'] }
        main { res.srcDirs = ['src/main/res', 'src/main/res/'] }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation xxxx
}

一、通过 productFlavors 配置不同的渠道/环境

productFlavors {
    SatayKing {
        applicationId "com.ablegenius.member.satayking"
    }

    WineverHK {
        applicationId "com.ablegenius.member.wineverzhudi"
    }
}

 

这里注意,在 defaultConfig 中,大家应该都是写了个默认的 applicationId 的。
经测试,productFlavors 设置的不同环境包名会覆盖 defaultConfig 里面的设置,
所以我们可以推测,它执行的顺序应该是先执行默认的,然后在执行分渠道的,如果冲突,会覆盖处理,这也很符合逻辑。

二、manifestPlaceholders 占位符使用

项目中使用到了极光、GoogleMap 等第三方SDK的配置,大家都知道极光推送需要根据不同的包名 JPush上注册的包名对应的appkey 的才能进行推送,如何去修改呢?

使用 manifestPlaceholders 来 定义 【GoogleMapKey 】常量,
在 AndroidManifest.xml 中 使用 “${GoogleMapKey}” 来占位,

<application
android:icon="${icon}"
      android:label="${app_name}"
      xxxxx>
      <!--渠道配置-->
      <meta-data
          android:name="CHANNEL"
          android:value="${CHANNEL_VALUE}" />


      <!-- Google Map Key -->   
      <meta-data
          android:name="com.google.android.geo.API_KEY"
          android:value="${GoogleMapKey}" /> 

<!--  极光推送-->
    
      <!-- User defined. 用户自定义的广播接收器-->
      <receiver
          android:name="com.ablegenius.member.receiver.JpushReceiver"
          android:enabled="true">
          <!--android:process=":remote"广播运行在远端单独进程中 ,调试断点无法执行需要关闭 或者 debug时候选择 remote ! -->
          <intent-filter>
            
            xxxxx
              <!--推送包名必须一致使用Gradle中的常量才是最终的 -->
              <category android:name="${applicationId}" />
          </intent-filter>
      </receiver>

</application>

此处的app名称和图标都可以使用占位符的方式进行引用,

Tpis:如果是这种方式修改应用名称,注意应用名称定义在外层,通过 resValue 定义的常量String 需要 先使用 单引号 里面再是字符串,’“应用名称”’
            resValue "string", "app_name", "築地日本料理"

三、了解 ApplicationId 与 PackageName的区别

调试和打包出来的名称会以Gradle 中的 applicationId 为最终包名,在 Manifest中的并不是最终的会被修改,地图在做key验证的时候填写的包名应该是ApplicationId ,而不是packageName

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.ablegenius.member">     

四、替换资源文件

每个应用资源布局 主题样式,启动页图标、应用名称可能 不一样,这时怎么做呢? Google 做法:

在 main 的同级目录下创建以渠道名命名的文件夹,然后创建资源文件(路径要与 main 中的一致),然后打包的时候 gradle 就会自己替换或者合并资源。 替换图片和合并颜色的原理也相似。必须名称统一使用!

在对应的渠道文件夹中创建res 文件, 注意渠道文件夹 目录为main 同级中, 创建 res为 : src/渠道名称/res

Android 多渠道打包实操(更改包名、图标、主题资源 、替换常量、第三方SDK Appkey配置)

五、打包和调试编译安装不同版本的渠道

选取不同的渠道,Gradle 会自动编译指定渠道,然后再运行项目即可

Android 多渠道打包实操(更改包名、图标、主题资源 、替换常量、第三方SDK Appkey配置)

多渠道打包后很多渠道时 需要默认 安装哪个渠道, 需要 在Build 中做切换

也可以通过命令打包: ./gradlew assembleRelease

最后如果你有涉及到第三方的Appkey之类的一定要检查好这块,以及配置的SHA1值等

参考:

Gradle多渠道打包(动态设定App名称,应用图标,替换常量,更改包名,变更渠道)

Gradle 实现 Android 多渠道定制化打包

ApplicationId 与 PackageName的区别