第四章 創建構建 Variant

第四章 創建構建 Variant

當開發一個應用時,通常會有幾個不同的版本,不同版本的不同配置會讓項目變得很復雜。
Gradle 中,有一些便捷和可擴展的概念可以用來定位這些常見問題。(定位!這些概念是用來描述問題的)。每個由 Android Studio 創建的新項目都會生成 debugrelease 構建類型 buildtype。另外一個概念 product flavor,其讓管理多個應用或依賴版本成為可能。
buildtypeproduct flavor 結合起來一起使用,可以很容易地處理各種版本的配置問題。結合起來的產物就叫做 構建 variant

一、構建類型

GradleAndroid 插件中,構建類型通常被用來定義如何構建一個應用或依賴庫。可以在 buildtypes 代碼塊中定義構建類型。

    buildTypes {
        release {
            //禁用清除無用的資源
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

注: 默認情況下,每個模塊都有一個 debug 構建類型,其被設置為默認構建類型。debug 構建類型的 debuggable 屬性為 true,其他構建類型的 debuggable 屬性均為 false

1. 創建構建類型

新的構建類型,只需要在 buildTypes 代碼塊中新增一個對象即可:

        demo{
            // 給應用 id 添加個后綴
            applicationIdSuffix ".demo"
            // 給 版本名 添加個后綴
            versionNameSuffix "-demo"
            buildConfigField("String","API_URL","\"http://demo.xx.com/api\"")
        }

應用 id 不同,這意味著可以在同一個設備上同時安裝 demo版本release版本
版本名 不同,這在同一個設備上區分多個應用版本時非常重要。

在新建一個構建類型A時,還可以用另一個構建類型B的屬性來初始化構建類型A。(類似于繼承)

        demo{
            initWith(buildTypes.debug)
            // 給應用 id 添加個后綴
            applicationIdSuffix ".demo"
            // 給 版本名 添加個后綴
            versionNameSuffix "-demo"
            buildConfigField("String","API_URL","\"http://demo.xx.com/api\"")
            debuggable false
        }

initWith() 方法創建了一個新的構建類型,并且復制一個已經存在的構建類型的所有屬性到新的構建類型中(debug -> demo),我們在 demo 中定義了 debuggableAPI_URL 來覆蓋掉 debug 的屬性。

2. 源集

我們知道源集指的就是一組會被一起執行和編譯的源文件。
當創建一個新的構建類型時,Gradle 也會創建一個新的源集。

3. 依賴

每個構建類型都可以有自己的依賴。Gradle 自動為每個構建類型創建新的依賴配置。如果執行在 debug 構建中添加一個 logging 框架,可以使用 debugImplementation 關鍵字添加依賴。

二、product flavor

與被用來配置相同 APPlibrary 的不同構建類型相反,product flavor 被用來創建不同的版本。
如果需要一個全新的 APP ,獨立于已有的應用發布,那么就使用 product flavor。否則使用構建類型即可。

1. 創建 product flavor

創建 product flavor 和創建 構建類型 類似。你可以通過在 productFlavor 代碼塊中添加新的 product flavor 來創建:

android{
    productFlavors{
        red{
            applicationId 'com.zyf.gradlevariant.red'
            versionCode 3
            flavorDimensions("why")
        }
    
        blue{
            applicationId 'com.zyf.gradlevariant.blue'
            versionCode 4
            minSdkVersion 14
            flavorDimensions("why")
        }
    }
}

ProductFlavorDefaultConfig 均繼承 BaseFlavor

image

DefaultConfig 類只有一個構造方法,沒有對 BaseFlavor 做任何其他的擴展。

image

ProductFlavor 類對 BaseFlavor 做了一些擴展。

/**
 * 封裝此項目的所有  產品風格(product flavor)  的屬性
 *
 * <p>如果你想在同一個 設備(谷歌商店或存儲庫) 中同時安裝你項目的不同版本,那么就使用      
 * 產品風格(product flavor) 來表示。例如,你可以給你的應用配置 ‘demo’ 和 ‘full’ 
 * 這兩個共享公共源碼和資源的產品風格,可以為每種產品風格指定不同的功能、設備要求、資
 * 源、應用ID。所以,產品風格允許你通過簡單的輸出你項目的不同版本,僅僅改變他們之間不
 * 同的組件和設置。 
 *
 * <p>配置產品風格與配置 構建類型(build-type) 相似:把 productFlavors代碼塊 添
 * 加到模塊中的 build.gradle 文件中,然后配置你需要的內容,產品風格支持與 
 * defaultConfig 同樣的屬性,因為 defaultConfig 實際上也是一個 ProductFlavor 
 * 對象,defaultConfig 這個 ProductFlavor 被用來作為其他 ProductFlavor 
 * 的基礎配置。可以覆蓋 defaultConfig 所有默認的屬性,eg:applicationId 等
 * 
 * <p>當 Android 插件版本為 3.+ 時,每個 Flavor 都必須依附于一個 dimension
 *
 * <p>配置 product flavor 時,安卓插件會自動將 product flavor 與 buildtype 
 * 中的配置相關聯來創建 guild variant (構建 variant)
 * 如果插件創建了你不行要的 variant ,則可以使用 VariantFilter 進行過濾
 */
public class ProductFlavor extends BaseFlavor {

    @Inject
    public ProductFlavor(
            @NonNull String name,
            @NonNull Project project,
            @NonNull ObjectFactory objectFactory,
            @NonNull DeprecationReporter deprecationReporter,
            @NonNull Logger logger) {
        super(name, project, objectFactory, deprecationReporter, logger);
    }

    private ImmutableList<String> matchingFallbacks;

    public void setMatchingFallbacks(String... fallbacks) {
        this.matchingFallbacks = ImmutableList.copyOf(fallbacks);
    }

    public void setMatchingFallbacks(String fallback) {
        this.matchingFallbacks = ImmutableList.of(fallback);
    }

    public void setMatchingFallbacks(List<String> fallbacks) {
        this.matchingFallbacks = ImmutableList.copyOf(fallbacks);
    }

    /**
     * 當一個明確的版本不與本地的模塊依賴匹配時,插件應該嘗試使用基于 product 
     * flavor 的分類列表
     *
     * <p>安卓插件3.+ 會嘗試使用模塊中的依賴匹配你模塊中的每個 構建variant 。例如
     * 當你構建一個 freeDebug 版本的app時,插件會嘗試將其與本地依賴模塊的 
     * freeDebug 版本匹配。For example, when you build a "freeDebug" 
     * 
     * <p>然而,也可能會有這種情況,同樣的 dimension(范圍;維度) 既存在于app中,
     * 也存在于 app 的依賴庫中。你的 app 包含一個 依賴庫 不包含的 flavor。例如,
     * 假如你的 app 和 app 的依賴庫中 包含一個 “tier” 的 風格維度(flavor 
     * dimension). app 中的 "tier" 維度包含 "free" 和 "paid" (兩個 
     * flavor)。 但在同樣的 dimension 下其中的一個依賴項只包含 “demo” 和 
     * “paid” 這兩個 flavor(風格) 。當插件嘗試構建 “free” 版本時,插件并不知道
     * 要用哪個依賴版本。此時會提示錯誤信息:
     *
     * <pre>
     * Error:Failed to resolve: Could not resolve project :mylibrary.
     * Required by:
     *     project :app
     * </pre>
     *
     * <p>在這種情況下,你應該使用<code>matchingFallbacks</code> 來指定匹配 "free" 產品風格的代替方案,如下所示:   
     * <pre>
     * 
     * // In the app's build.gradle file.
     * 
     * android {
     *     flavorDimensions 'tier'
     *     productFlavors {
     *         paid {
     *             dimension 'tier'
     *             // 因為依賴中已經包含了一個 “paid” 風格,該風格被包含在 
     *             // “tier” 維度中(dimension) ,你不必再為 “paid” 風格提供
     *             // 特定的屬性了。
     *         }
     *         free {
     *             dimension 'tier'
     *             // 當依賴匹配維度中不包含一個 “free” 風格時,此處指定回退風格
     *             // 的排序列表,你想列舉多少就列舉多少,插件會選擇排序列表中的
     *             // 第一個風格,使用在依賴的 “tier” 維度中
     * 
     *             // 這里就是一個數組而已,如果依賴中與我們列舉的 flavor 出現不
     *             // 匹配的情況,那么就默認選擇此處數組的第一個作為替代方案
     *             matchingFallbacks = ['demo', 'trial']
     *         }
     *     }
     * }
     * </pre>
     *
     * <p>注意,對于應用程序及其依賴庫中存在的給定 風格維度 ,當庫中包含app不包含的
     *  產品風格時,不會出現問題。  
     *
     * <p>如果你正在嘗試解決:依賴庫包含一個風格維度但是應用程序中不包含的問題。使用
     * missingDimensionStrategy() 方法。
     * @return 返回使用的 產品風格 名稱 倒序排列
     */
    public List<String> getMatchingFallbacks() {
        if (matchingFallbacks == null) {
            return ImmutableList.of();
        }
        return matchingFallbacks;
    }

    @Override
    @NonNull
    protected DimensionRequest computeRequestedAndFallBacks(@NonNull List<String> requestedValues) {
        // 這個沒看懂
                VariantManager.getModifiedName(getName()), ImmutableList.copyOf(requestedValues));
    }

    @Override
    protected void _initWith(@NonNull BaseConfig that) {
        super._initWith(that);

        if (that instanceof ProductFlavor) {
            matchingFallbacks = ((ProductFlavor) that).matchingFallbacks;
        }
    }
}

2. 源集

和構建類型類似,product flavor 也可以擁有它們自己的源集目錄。為一個特殊的 flavor 創建一個文件夾就和創建一個有 flavor 名稱的文件一樣簡單。
甚至可以為一個特定構建類型(buildtype)和 flavor 的結合體創建一個文件夾。該文件夾的名稱將是 flavor名稱+構建類型的名稱。
例如,讓 blue flavorrelease 版本有一個不同的 應用圖標,那么文件夾將會被叫做 blueRelease
合并文件夾的組成將比構建類型文件夾和 product flavor 文件夾擁有更高優先級。

3. 多種定制的版本

在某些情況下,我們可能需要創建 product flavor 的結合體。例如:客戶 A 和客戶 B 在他們的 APP 中都想要免費版和付費版,并且是基于相同的代碼、不同的品牌。創建四種不同的 flavor 意味著需要像這樣設置多個拷貝,這并不是最佳做法。使用 flavor dimension 是結合 flavor 的有效方式:

android{
    flavorDimensions "color","price"
    productFlavors{
        red{
            dimension "color"
        }
        blue{
            dimension "color"
        }
        free{
            dimension "price"
        }
        paid{
            dimension "price"
        }
    }
}

如果為 flavor 添加了維度,Gradle 會希望我們為每個 flavor 都添加維度。如果你忘了,那么你會收到一個帶有錯誤接受的構建錯誤。 flavorDimensions 數組定義了維度,維度的順序非常重要。當結合兩個 flavor 時,它們可能定義了相同的屬性或資源。在這種情況下,flavor 維度數組的順序就決定了哪個 flavor 配置將被覆蓋。在上一個例子中,color 維度覆蓋了 price 維度,該順序也決定了 構建 variant 的名稱。

假設默認的構建配置是 debugrelease 構建類型(buildtype),那么上述例子中定義的 flavor 將會生成下面這些 構建 variant

  • blueFreeDebug and blueFreeRelease
  • bluePaidDebug and bluePaidRelease
  • redFreeDebug and redFreeRelease
  • redPaidDebug and redPaidRelease

三、構建variant

構建 variant構建類型product flavor 結合的結果。不論什么時候創建一個 構建類型product flavor,新的 variant 都會被創建。
例子:如果你有標注的 debugrelease 構建類型,并且你創建了一個 redblueproduct flavor ,那么會生成下圖所示的 構建 variant

image

公式: flavorDimensions 中每個維度含有的 flavor 的乘積 再乘以 buildtype 的數量。
( color 含有 2 個 flavor ,price 含有 2 個 flavor ,2 * 2 = 4 。再乘以 buildtype 的數量 * 2 = 8)

在上述窗口選擇 variant 后,點擊

image
就會運行對應的 variant。(如果沒有 product flavor,則 variant 只包含構建類型)。

image

1. 任務

GradleAndroid 會為我們配置的每一個構建 variant 創建任務。

一個新的 Android 應用默認有 debugrelease 兩種構建類型,所以你可以用 assembleDebugassembleRelease 來分別構建兩個 APK。即用單個命令 assemble 來創建兩個 APK

當添加一個新的構建類型時,新的任務也將被創建。一旦你開始添加 flavor,那么整個全新的任務系列將會被創建,因為每個構建類型的任務會和每個 product flavor 相結合。這意味著,僅用一個構建類型和一個 flavor 做一個簡單設置,你就會有三個任務用于創建全部 variant

  • assembleBlue:使用 blue flavor 配置和組裝 BlueReleaseBlueDebug
    • image
    • image
  • assembleDebug:使用 debug 構建配置類型,并為每個 product flavor 組裝一個 debug 版本
    • image
    • image
  • assembleBlueDebug:用構建配置類型(buildtype)集合 flavor 配置,并且 flavor 設置將會覆蓋構建類型(buildtype)設置
    • image
    • task的名稱是根據 buildVariant 窗口確定的
    • image
    • 所以應該輸入的是:assemble{顏色}{價格}{buildType} ,一定要按照順序!

新的 tasks 是為每個 構建類型、每個 product flavor、每個 構建類型和product flavor結合體 創建的。

2. 源集

構建variant,是一個 buildType 和 一個或多個 product flavor 的結合體,其可以有自己的 源集文件夾
例如,由 debug buildType blue flavorfree flavor 創建的 variant 可以有其自己的源集 src/blueFreeDebug/java/。其可以通過使用 sourceSet 代碼塊來覆蓋文件夾的位置。

3. 源集合并資源和 manifest

源集的引入額外增加了構建進程的復雜性。
GradleAndroid 插件在打包應用之前將 main 源集和 buildType 源集合并在一起。此外,library 項目也可以提供額外的資源,這些也需要被合并。這同樣適用于 manifest 文件。
例如:在你應用的 debug variant 中可能需要額外的 Android 權限來存儲 log 文件。但你并不想在 main 源集中申請該權限,因為這樣會嚇跑潛在客戶。相反,你可以在 debug buildType 的源集中額外添加一個 manifest 文件來申請額外的權限。
資源和 manifest 的優先順序:

image

如果資源在 flavormain 源集中都有申明,那么 flavor 中的資源將被賦予更高的優先級,那么就會打包 flavor 中的資源。

4. variant 過濾器

在你的構建中,可以完全忽略某些 variant。通過這種方式,你就可以通過 assemble 命令來加快構建所有 variant 的進程,并且你的任務列表不會被任何無須執行的任務污染。
variant 過濾器也可確保在 Android Studio 的構建 variant 切換器中不會出現過濾的構建 variant

android{
    android.variantFilter { variant ->
        if (variant.buildType.name == 'release') {
            //說明是發布版
            variant.getFlavors().each { flavor ->
                //遍歷發布版的風格
                if(flavor.name == 'blue'){
                    //說明是 藍色發布版
                    //那么就把它給過濾了
                    variant.setIgnore(true)
                }
            }
        }
    }
}

現象:
image

四、簽名配置

我們在發布應用之前,會使用 私鑰 給它簽名。
如果你有一個付費版和免費版或針對不同用戶的不同應用,那么你需要為每個 flavor 使用不同的 私鑰 簽名。這就是簽名配置出現的原因:

android{
       //簽名配置
    signingConfigs{
        demo.initWith(signingConfigs.debug)

        //構建文件中的對象都是從上到下順序創建的
        //所以下面這個構建類型release中使用到的signingConfigs.release對象
        //要先創建,才能被使用
        //所以要將signingConfigs{}放在buildTypes{}上面
        release{
            storeFile file("release.keystore")
            storePassword "woshimima"
            keyAlias "gradleforandroid"
            keyPassword "secretpassword"
        }
    }
}

Android 插件使用了一個通用 keystore 和一個已知密碼自動創建了 debug 配置,所以就沒必要為該構建類型再創建一個簽名配置了。
demo 配置時,使用了 initWith(),其會從另一個簽名配置中復制所有屬性,這意味著 demo 構建是通過 debug 密鑰簽名的,并不是我們自定義的。
release 配值時,使用了 storeFile 來指定 keystore 文件的路徑,之后定義了密鑰別名和兩個密碼。

住: 最好不要在 Gradle 構建文件中存儲憑證,因為 Gradle 的構建文件會被上傳到版本控制系統,最好的方式是使用 Gradle 配值文件。

第七章會完整介紹如何存儲簽名配置密碼

在定義簽名配置之后,需要將它們應用到 構建類型 或 flavor 中。構建類型 和 flavor 都有一個叫做 signingConfig 的屬性:

android{
    buildTypes{
        release {
            signingConfig signingConfigs.release
        }
    }
}

上述例子使用了構建類型,如果你想為每個你所創建的 flavor 使用不同的憑證,那么你就需要創建不同的簽名配置,也就是說給每個 flavorsigningConfig 屬性賦值。

使用簽名配置這種方式會造成很多問題,當為一個 flavor 分配一個配置時,實際上它是覆蓋了 buildType 的簽名配置。如果你不行這樣,那么在使用 flavor 時,就應該為每個 buildType 的每個 flavor 分配不同的密鑰,而這是可以直接在 buildType 中完成的:

android{
    //在signingConfigs中定義了兩個對象:red、blue
    //注:要把signingConfigs,寫在 buildTypes 前面
    signingConfigs{
        red{
        
        }
        
        blue{
        
        }
    }

    buildTypes{
         release {
            signingConfig signingConfigs.release

            //給release版本的 red flavor 配置簽名 red
            productFlavors.red.signingConfig signingConfigs.red
            
            //給release版本的 blue flavor 配置簽名 red
            productFlavors.blue.signingConfig signingConfigs.blue
        }
    }
}

上述例子展示了在不影響 debugdemo 構建類型的情況下,如何在 release 構建類型中為 red flavorblue flavor 使用不同的簽名配置。

五、總結

構建類型是什么( buildType )、product flavor 是什么。
buildTypeproduct flavor 的結合體:構建 variant
如果需要相同代碼的項目,在同一設備中安裝兩個應用,那么需要用到 product flavor
構建 variant 就是多渠道打包的關鍵概念。

簽名配置是什么。

一定要注意在構建文件中每個代碼塊的書寫順序!

下一章:管理多模塊構建。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,963評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,348評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,083評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,706評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,442評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,802評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,795評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,983評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,542評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,287評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,486評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,030評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,710評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,116評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,412評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,224評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,462評論 2 378