1. 程式人生 > >Gradle實現多渠道打包(不同資原始檔打不同的包)

Gradle實現多渠道打包(不同資原始檔打不同的包)

在日常開發中,我們或多或少都會碰到多渠道打包的一些問題,有些是同一個版本要上傳到不同的平臺,有些是要提供給不同的代理商,中間可能需要改動裡面的圖片或其他的一些資原始檔,對於版本比較少的我們可以簡單的替換,但是遇到很多版本時,相信再通過手動替換會令人吐血~
廢話不說,先來看下效果圖,下面是根據不同平臺需要打包出來的兩個apk
這裡寫圖片描述
apk介面展示效果如下(為了方便展示我在productFlavors裡用了不同的applicationId “包名”,防止安裝apk時被替換)
介面展示

下面先來介紹一下如何根據不同版本的需求存放不同的資原始檔

資原始檔的適配

假如有這麼一個需求:現在需要打包兩個版本,小米和華為,其頁面僅有一張圖片,但要求兩個版本的圖片不一樣,並且app的名稱也不同,簡單的做下適配
1、首先看下頁面佈局

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.liuw.gradle.MainActivity">

    <ImageView
        android:layout_width
="match_parent" android:layout_height="match_parent" android:src="@mipmap/userhead"/>
</LinearLayout>

正如需求所說,僅有一張圖片,預設我們的res資原始檔夾下肯定會有一張預設的userhead圖片,但是要做到不同版本展示不同的圖片,是不是很自然的會想到根據不同版本建立不同的資原始檔目錄
建立資原始檔目錄
2、這裡我建立了兩個資原始檔,res-huawei和res-xiaomi,展開看一下里面的內容
這裡寫圖片描述
這裡我直接將華為和小米平臺所需的圖片和文字用了相同的名稱添加了進去,這樣專案在打包時就會直接覆蓋res檔案下對應的資源
3、在gradle裡配置不同平臺採用不同的資原始檔

//以下程式碼放在android{}內
//配置資原始檔路徑,可動態指定不同版本資原始檔
    sourceSets {
        main {
            manifest.srcFile 'src/main/AndroidManifest.xml'
            java.srcDirs = ['src/main/java']
            resources.srcDirs = ['src/main/resources']
            aidl.srcDirs = ['src/main/aidl']
            renderscript.srcDirs = ['src/maom']
            res.srcDirs = ['src/main/res']
            assets.srcDirs = ['src/main/assets']
            jniLibs.srcDir 'src/main/jniLibs'
        }

    //用各自對應的資原始檔路徑
        xiaomi.res.srcDirs = ['src/main/res-xiaomi']
        huawei.res.srcDirs = ['src/main/res-huawei']

        // Move the tests to tests/java, tests/res, etc...
        instrumentTest.setRoot('tests')
        // Move the build types to build-types/<type>
        // For instance, build-types/debug/java, build-types/debug/AndroidManifest.xml, ...
        // This moves them out of them default location under src/<type>/... which would
        // conflict with src/ being used by the main source set.
        // Adding new build types or product flavors should be accompanied
        // by a similar customization.
        debug.setRoot('build-types/debug')
        release.setRoot('build-types/release')
    }

//渠道Flavors,配置不同風格的app
productFlavors {
        xiaomi{}
        huawei{}
 }

//批量配置
    productFlavors.all {
        flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
    }

配置完gradle裡,別忘了在AndroidManifest裡配置渠道標籤

    <meta-data
            android:name="UMENG_CHANNEL"
            android:value="${UMENG_CHANNEL_VALUE}">
        </meta-data>

到這裡資原始檔的基本配置就算結束了

配置多渠道打包

內容比較多,程式碼裡註釋也比較詳細,這裡就直接上程式碼了

apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "26.0.0"
    defaultConfig {
        applicationId "com.liuw.gradle"
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    //配置資原始檔路徑,可動態指定不同版本資原始檔
    sourceSets {

        main {
            manifest.srcFile 'src/main/AndroidManifest.xml'
            java.srcDirs = ['src/main/java']
            resources.srcDirs = ['src/main/resources']
            aidl.srcDirs = ['src/main/aidl']
            renderscript.srcDirs = ['src/maom']
            res.srcDirs = ['src/main/res']
            assets.srcDirs = ['src/main/assets']
            jniLibs.srcDir 'src/main/jniLibs'
        }
        //用各自對應的資原始檔路徑
        xiaomi.res.srcDirs = ['src/main/res-xiaomi']
        huawei.res.srcDirs = ['src/main/res-huawei']

        // Move the tests to tests/java, tests/res, etc...
        instrumentTest.setRoot('tests')
        // Move the build types to build-types/<type>
        // For instance, build-types/debug/java, build-types/debug/AndroidManifest.xml, ...
        // This moves them out of them default location under src/<type>/... which would
        // conflict with src/ being used by the main source set.
        // Adding new build types or product flavors should be accompanied
        // by a similar customization.
        debug.setRoot('build-types/debug')
        release.setRoot('build-types/release')
    }

    // rename the apk with the version name,版本比較多時,自定義匯出的APK名稱,不同的渠道編出的APK的檔名應該是不一樣的
    applicationVariants.all { variant ->
        variant.outputs.each { output ->
            def outputFile = output.outputFile
            Map<String, String> map = System.getenv();
            String userName = map.get("USER");
            def des = '/Users/' + userName + '/Desktop/release/'
            //根據apk型別打包(release和debug兩個版本,一般供單個應用打包使用)
//            if (variant.buildType.name.equals('release')) {
//                def releaseApkName = 'gradle_v' + getVersionName(project) + '.apk'
//                output.outputFile = new File(des + releaseApkName)
//            }
//            if (variant.buildType.name.equals('debug')) {
//            }

            //根據apk名稱打包(這裡就用到了同個apk,為不同平臺打包,也可以打debug版本)
            if (outputFile != null) {
            //不知道名稱的可以提前跑一遍,看下這裡打印出來的日誌
                println "======outputFile.name======" + outputFile.name;
                def fileName;
                if (outputFile.name.endsWith('app-xiaomi-release.apk')) {
                //打出包的包名,版本號為當前gradle裡的versionName
                   //fileName="xiaomi-gradle_v-${defaultConfig.versionName}.apk"
                   //打出包的包名,版本號為AndroidManifest裡獲取的versionName 
                    fileName="xiaomi-gradle_v-${getVersionName(project)}.apk" 
                }else if(outputFile.name.endsWith('app-huawei-release.apk')){
//                    fileName="huawei-gradle_v-${defaultConfig.versionName}.apk"
                    fileName="huawei-gradle_v-${getVersionName(project)}.apk"
                } else {
                    fileName='liuw-release.apk';//or debug版本
                }
                //預設outputs/apk目錄下
                //output.outputFile = new File(outputFile.parent, fileName)
                //指定目錄,根據自己需要指定
                output.outputFile = new File(des + fileName)
            }
        }
    }

   //debug和release版本的簽名配置
    signingConfigs {
        debug {
            //這樣寫就得把keystore.jks檔案放在專案目錄,絕對路徑
            storeFile file("keystore.jks")
            keyAlias = 'keystore'
            keyPassword = '123456'
            storePassword = '123456'
        }

        release {
        //這樣keystore.jks檔案可以放在桌面任何地方,配置一下路徑即可
            storeFile file(getKeyStorePath())
            keyAlias = 'keystore'
            keyPassword = '123456'
            storePassword = '123456'
        }

      //有不同簽名時,可定義多個,在productFlavors中呼叫
//        release1 {
//            storeFile file(getKeyStorePath())
//            keyAlias = 'keystore'
//            keyPassword = '123456'
//            storePassword = '123456'
//        }
    }

    //構建型別,通常有release和debug兩種
    buildTypes {
        debug {
            // 顯示Log
            buildConfigField "boolean", "LOG_DEBUG", "true"
            versionNameSuffix "-debug"
            minifyEnabled false
            zipAlignEnabled false
            shrinkResources false
            signingConfig signingConfigs.debug
            manifestPlaceholders=[jpush_appkey:"0f56521436362122"]
        }

        release {
            // 不顯示Log
            buildConfigField "boolean", "LOG_DEBUG", "false"
            //混淆
            minifyEnabled true
            //Zipalign優化
            zipAlignEnabled true
            // 移除無用的resource檔案
            shrinkResources true
            //前一部分代表系統預設的android程式的混淆檔案,該檔案已經包含了基本的混淆宣告
            //如果需要自定義混淆可把後面引數替換成混淆檔案
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            //指定打包時的簽名,如果有不同的簽名可放在productFlavors不同專案下執行,不過一般都會相同
            signingConfig signingConfigs.release
            //manifestPlaceholders 可以替換androidmanifest檔案中的標籤的value值,可作為快速渠道打包替換渠道名的一種方式
            //也可以自定義標籤用來替換需要的文字,多作為不同環境不同key的修改(如debug、release模式)
            manifestPlaceholders=[jpush_appkey:"0d14522663255632"]
        }
    }

    /*
    * 渠道Flavors,配置不同風格的app
    * 資原始檔不能用test欄位命令(這裡我嘗試過會執行錯誤,如res-test)
    * debug模式下新增的資原始檔預設只有一個,可以根據res圖示看出來,執行時預設執行上次的檔案,不過對打包無影響
    * */
    productFlavors {
        xiaomi{}
        huawei{}
//        baidu{
//            applicationId "com.liuw.badu"//可為不同版本動態指定包名
//            manifestPlaceholders=[UMENG_CHANNEL_VALUE:"xiaomi"] //可以替換AndroidManifest中標籤的value值,此處也可用作多渠道打包標識,不過一般採用下面的批量配置
//            signingConfig signingConfigs.release1//此處可單獨指定簽名,前提是在signingConfigs裡定義過了
//        }
//注意一點,這裡的flavor名如果是數字開頭,必須用引號引起來,如"360"{}
    }

    //批量配置
    productFlavors.all {
        flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
    }

    dexOptions {
        //最大堆記憶體
        javaMaxHeapSize '2048m'
        //預編譯
        preDexLibraries = true
        //執行緒數
        threadCount = 16
        dexInProcess = true
    }
}

android {//lint檢查
    lintOptions {
        //所有正式版構建執行規則生成崩潰的lint檢查,如果有崩潰問題將停止構建
        checkReleaseBuilds false
        // Or, if you prefer, you can continue to check for errors in release builds,
        // but continue the build even when errors are found:
        //錯誤發生後停止gradle構建
        abortOnError false
    }
}

//以下的兩個def方法可以放在總工程路徑下的build.gradle裡使用ext置成全域性供多個Module呼叫(通過rootProject.ext.xxx呼叫獲取)
//獲取桌面上的keystore路徑
def getKeyStorePath() {
    Map<String, String> map = System.getenv();
    String userName = map.get("USER");
    def des = '/Users/' + userName + '/' + 'keystore.jks'
    return des
}

//獲取AndroidManifest中的versionName
def getVersionName(Project project) {
    def xmlFile = project.file("src/main/AndroidManifest.xml")
    def rootManifest = new XmlSlurper().parse(xmlFile)
    def versionName = rootManifest['@android:versionName']
    return versionName
}

//依賴的jar包
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.3.1'
    testCompile 'junit:junit:4.12'
}

如果想要看渠道命名是否成功,可以在主頁面裡呼叫以下程式碼

String channel;
        try {
            ApplicationInfo appInfo = getPackageManager()
                    .getApplicationInfo(getPackageName(),
                            PackageManager.GET_META_DATA);

            channel = appInfo.metaData.getString("UMENG_CHANNEL");

            Log.i("TAG","UMENG_CHANNEL_VALUE=" + channel);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();

        }

簽名檔案如果有不會的可以參考下生成簽名,我這裡為了方便測試,在桌面上和專案根目錄各放了一份,MAC和windows路徑可能不一樣,如果出現錯誤,可能要自己修改一下。
Ok,到這裡基本就算結束了,我們可以執行一下./gradlew assembleRelease,等著打包完成吧。
assemble 這個命令,會結合 Build Type 建立自己的task,如:

./gradlew assembleDebug
./gradlew assembleRelease

除此之外 assemble 還能和 Product Flavor 結合建立新的任務,其實 assemble 是和 Build Variants 一起結合使用的,而 Build Variants = Build Type + Product Flavor , 舉個例子大家就明白了:

如果我們想打包xiaomi渠道的release版本,執行如下命令就好了:

./gradlew assembleXiaomiRelease

如果我們只打xiaomi渠道版本,則:

./gradlew assembleXiaomi

此命令會生成xiaomi渠道的Release和Debug版本
同理我想打全部Release版本:
./gradlew assembleRelease
這條命令會把Product Flavor下的所有渠道的Release版本都打出來。

當我們productFlavors有多個版本,並且只想打包其中一個版本時,除了以上這種執行單個命令外,還可以在gradle裡動態配置
1、先在根目錄的gradle裡新增一個全域性引數

ext {
    LAUNCHER_FLAVOR = 'xiaomi'
}

2、再去Module的gradle裡新增variantFilter進行過濾

android {
//用於指定版本,只打包指定版本
    variantFilter { variant ->
        def flavor = variant.flavors*.name

        println "======variant.flavors======" + flavor;

        String name = 'defaultProject'
        if (project.hasProperty('LAUNCHER_FLAVOR')) {
            name = LAUNCHER_FLAVOR
        }

        if (!flavor.contains(name)) {
            setIgnore(true)
        } else if (flavor.contains('xiaomi')) {
            dependencies {
                xiaomiCompile project(':依賴的檔名')  // 動態新增依賴,括號內格式可能要注意下
            }
        }
    }
 }

這樣打出來的包就只有xiaomi版本的apk。

完整專案已上傳至CSDN資源,因為寫在一起可能不便理解,可下載下來跑一遍對比看下效果可能會更容易理解些~
Gradle功能還是比較強大的,這裡只是簡單介紹,如有不妥之處,歡迎指正更改~
還看到兩篇不錯的文章,說的比較詳細,也一起分享出來供大家參考Android打包的那些事Gradle配置最佳實踐