不同的Gradle多渠道配置總結2

歡迎關注微信公衆號:FSA全棧行動 👋html

文章中可能涉及的到一些 groovy 語法點,可在以下文章中找到&學習:java

1、問題

Android Gradle plugin 給 Android apk 打包擴展了更多的可能性,其中多渠道打包是平常開發中最爲經常使用的配置,經過前篇文章《不同的 Gradle 多渠道配置總結》能夠了解到, Android Gradle plugin 可以讓資源合併、代碼整合、甚至指定各類源文件目錄等等,但你是否意識到,這些功能基本上都是對本來的配置進行了擴充,而不是覆蓋?而今天我就遇到了須要覆蓋的狀況,先來看看如下配置:android

android {
    defaultConfig {
        ndk {
            abiFilters = ['armeabi-v7a']
        }
    }

    productFlavors {
        xiaomi {
            buildConfigField "String", "CHANNEL", '"xiaomi"'
        }
        oppo {
            buildConfigField "String", "CHANNEL", '"oppo"'
        }
        mumu {
            buildConfigField "String", "CHANNEL", '"mumu"'
        }
        yeshen {
            ndk {
                abiFilters = ['x86']
            }
            buildConfigField "String", "CHANNEL", '"yeshen"'
        }
        googleplay {
            ndk {
                abiFilters = ['armeabi', 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64']
            }
            buildConfigField "String", "CHANNEL", '"googleplay"'
        }
    }
}

dependencies {
    implementation 'tv.danmaku.ijk.media:ijkplayer-armv5:0.8.8'
    implementation 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.8'
    implementation 'tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8'
    implementation 'tv.danmaku.ijk.media:ijkplayer-x86:0.8.8'
    implementation 'tv.danmaku.ijk.media:ijkplayer-x86_64:0.8.8'
}
複製代碼

我這個配置的意圖很明顯,默認全部渠道的 apk 只保留 armeabi-v7a 的 so 庫,而 yeshen 渠道則是隻保留 x86 的 so 庫,那麼,以如今 yeshen 的渠道配置,打包出來的 apk 最終是否只會保留 x86 的 so 庫呢?api

提示:PC 上的 Android 模擬器大多數是 x86 的,爲了讓這類包含 so 庫的 app 能正常運行,同時作到 apk 體積最小,因此須要只保留 x86 的 so 庫。微信

固然不會,否則也不會有這篇文章了,來看 apk 解包後的截圖:markdown

顯然,這達不到我想要的預期效果,那麼如何解決這個問題呢?要解決問題,得先知道問題出現的緣由是什麼,下面就進入探索環節。閉包

提示:若是你趕時間,想直接知道結果,那麼請拉到文末查看最終解決方案。app

2、探索

經過 apk 解包結果以及 gradle 配置這二者的綜合考慮,顯然是 defaultConfigproductFlavors 中配置的 ndk abiFilters 被合併了,爲何會合並呢?又是怎麼合併的?有以下兩種可能性:maven

  • defaultConfigproductFlavors 修改了同一個 ndk abiFilters 配置。
  • defaultConfigproductFlavors 有各自單獨的 ndk abiFilters 配置,但最終被 Android Gradle plugin 經過某種手段整合了。

一、ndk abiFilters 源碼分析

按住ctrl 點擊 abiFilters,會自動跳轉到 Android Gradle pluginNdkOptions 的源碼中:ide

public class NdkOptions implements CoreNdkOptions, Serializable {
    private Set<String> abiFilters;
    ...
    @NonNull
    public NdkOptions setAbiFilters(Collection<String> filters) {
        if (filters != null) {
            if (abiFilters == null) {
                abiFilters = Sets.newHashSetWithExpectedSize(filters.size());
            } else {
                abiFilters.clear();
            }
            abiFilters.addAll(filters);
        } else {
            abiFilters = null;
        }
        return this;
    }
}
複製代碼

也就是說 build.gradle 中 abiFilters = ['x86'] 其實調用了 NdkOptions#setAbiFilters(Collection<String> filters) 方法, 即 abiFilters = ['x86'] 等同於 setAbiFilters(['x86']);經過閱讀其源碼能夠知道,該方法會先將 abiFilters 集合清空再添加新的 filters 集合,那麼這就能夠排除掉 defaultConfigproductFlavors 修改同一個 ndk abiFilters 配置的可能性。

提示:事實上,defaultConfigproductFlavors 對應的也不是同個 NdkOptions 對象。

二、關聯 Android Gradle plugin 源碼

在繼續往下探索前,咱們須要先關聯 Android Gradle plugin 源碼,只須要在 app 下 build.gradle 中添加以下依賴便可:

提示:爲何要關聯 Android Gradle plugin 源碼?由於雖然你如今能夠經過 按住ctrl + 點擊 的方式索引到對應的源碼,但你是沒辦法查看變量或方法在哪些地方被調用了的,這不利於代碼追蹤(Find Usages),閱讀源碼的效率就會很低下。

dependencies {
    // 版本號跟項目根目錄build.gralde中 classpath "com.android.tools.build:gradle:3.2.0" 的版本號一致便可
    api 'com.android.tools.build:gradle:3.2.0'
}
複製代碼

這是一種很討巧的關聯源碼方式,至關於把 Android Gradle plugin 看成是一個第三方庫依賴進來,這樣就能夠像工程源碼那樣,進行各類源碼跳轉追蹤了,很實用。

注意:api 'com.android.tools.build:gradle:3.2.0' 僅僅只是用於查看 Android Gradle plugin 源碼使用,真正在進行 apk 打包的時候須要註釋掉這行依賴。

三、Set<String> abiFilters 追蹤

前面已經排除了一種合併的可能,如今要驗證是否會是第二種。目前咱們知道了 ndk abiFilters 配置確定和 NdkOptions 源碼有關,經過 Find Usages 快捷鍵查看 NdkOptions#setAbiFilters() 方法全部調用處,顯然這個方法不是關鍵:

提示:AS 經過 Find Usages 快捷鍵,能夠列舉出變量或方法的全部調用處,若是你的 Keymap 是 Eclipse,那麼對應的快捷鍵是 Ctrl+G,其餘 Keymap 須要本身確認。

再經過 按住ctrl + 鼠標左鍵 查看 Set<String> abiFilters 變量的調用處,發現了線索:

根據名字,天然能夠想到這個 MergedNdkConfig 應該就是 ndk abiFilters 配置合併的關鍵了:

public class MergedNdkConfig implements CoreNdkOptions {

    private Set<String> abiFilters;

    @Override
    @Nullable
    public Set<String> getAbiFilters() {
        return abiFilters;
    }

    public void append(@NonNull CoreNdkOptions ndkConfig) {
        // override
        if (ndkConfig.getModuleName() != null) {
            moduleName = ndkConfig.getModuleName();
        }

        if (ndkConfig.getStl() != null) {
            stl = ndkConfig.getStl();
        }

        if (ndkConfig.getJobs() != null) {
            jobs = ndkConfig.getJobs();
        }

        // append
        if (ndkConfig.getAbiFilters() != null) {
            if (abiFilters == null) {
                abiFilters = Sets.newHashSetWithExpectedSize(ndkConfig.getAbiFilters().size());
            }
            abiFilters.addAll(ndkConfig.getAbiFilters());
        }

        if (cFlags == null) {
            cFlags = ndkConfig.getcFlags();
        } else if (ndkConfig.getcFlags() != null && !ndkConfig.getcFlags().isEmpty()) {
            cFlags = cFlags + " " + ndkConfig.getcFlags();
        }

        if (ndkConfig.getLdLibs() != null) {
            if (ldLibs == null) {
                ldLibs = Lists.newArrayListWithCapacity(ndkConfig.getLdLibs().size());
            }
            ldLibs.addAll(ndkConfig.getLdLibs());
        }
    }
}
複製代碼

MergedNdkConfig#append() 方法中使用到了 NdkOptions#abiFilters 變量,再以一樣的方法,能夠定位到 MergedNdkConfig#append() 方法的調用處就在 GradleVariantConfiguration#mergeOptions() 方法中:

public class GradleVariantConfiguration extends VariantConfiguration<CoreBuildType, CoreProductFlavor, CoreProductFlavor> {
    ...
    /** * Merge Gradle specific options from build types, product flavors and default config. */
    private void mergeOptions() {
        ...
        computeMergedOptions(
                mergedNdkConfig,
                CoreProductFlavor::getNdkConfig,
                CoreBuildType::getNdkConfig,
                MergedNdkConfig::reset,
                MergedNdkConfig::append);
        ...
    }

    private <CoreOptionT, MergedOptionT> void computeMergedOptions( @NonNull MergedOptionT option, @NonNull Function<CoreProductFlavor, CoreOptionT> productFlavorOptionGetter, @NonNull Function<CoreBuildType, CoreOptionT> buildTypeOptionGetter, @NonNull Consumer<MergedOptionT> reset, @NonNull BiConsumer<MergedOptionT, CoreOptionT> append) {
        reset.accept(option);

        CoreOptionT defaultOption = productFlavorOptionGetter.apply(getDefaultConfig());
        if (defaultOption != null) {
            append.accept(option, defaultOption);
        }

        // reverse loop for proper order
        final List<CoreProductFlavor> flavors = getProductFlavors();
        for (int i = flavors.size() - 1 ; i >= 0 ; i--) {
            CoreOptionT flavorOption = productFlavorOptionGetter.apply(flavors.get(i));
            if (flavorOption != null) {
                append.accept(option, flavorOption);
            }
        }

        CoreOptionT buildTypeOption = buildTypeOptionGetter.apply(getBuildType());
        if (buildTypeOption != null) {
            append.accept(option, buildTypeOption);
        }
    }
}
複製代碼

結合 GradleVariantConfiguration#computeMergedOptions() 方法,大概也能知道,其實 GradleVariantConfiguration#mergeOptions() 就是把 defaultConfigproductFlavors 各自的 ndk abiFilters 配置結果給整合了。

3、方案

到此爲止,咱們肯定了 ndk abiFilters 的合併原理,能夠理解爲在 build.gradle 中, defaultConfigproductFlavors 根本就是兩個不想幹的配置,二者是沒有辦法經過某種手段直接干涉 ndk abiFilters 合併結果的。

補充:固然了,你也能夠嘗試繼續追蹤 GradleVariantConfiguration#mergeOptions() 的調用時機,看看是否能經過某個 task 來干涉 ndk abiFilters 的合併結果。反正,我目前沒未找到突破點。

那麼,如今惟一的解決辦法就是,defaultConfigproductFlavors 中只配一處,但是又要知足 默認只保留 'armeabi' 的前提條件,怎麼辦?可使用 groovy函數 + 閉包 來解決這個問題,讓你見識一下什麼是真正的騷操做。

一、閉包抽離

做爲第三代配置腳本的 gradle 與第二代的 maven 在格式上有很大的不一樣,gradle 採用的是 dsl 格式,在某個配置項右邊會攜帶一個 {},其實這一個 {} 表示的就是 groovy 語言中的一個閉包(clouser),每一個配置頂能夠理解爲是一個接收閉包的方法,咱們能夠來驗證一下,好比:

// 轉換前
defaultConfig {
    applicationId "com.charylin.gradleconfigmergedemo"
    ...
}
// 轉換後
defaultConfig({
    applicationId "com.charylin.gradleconfigmergedemo"
    ...
})
複製代碼

這二者有差嗎?沒有,一樣能編譯經過,而且能被正常識別。那麼如今回過頭了看看這個多渠道配置:

yeshen {
    ndk {
        abiFilters = ['x86']
    }
}
複製代碼

是否是有什麼大膽的想法?沒錯,咱們徹底能夠把這個閉包抽出來,改爲一個返回值是閉包的函數,再結合函數可選參數指定默認值便可:

android {
    defaultConfig {
        // ndk {
        // abiFilters = ['armeabi-v7a']
        // }
    }

    productFlavors {
        ...
        xiaomi profileLqr()
        oppo profileLqr()
        mumu profileLqr()
        yeshen profileLqr(['x86'])
        googleplay profileLqr(['armeabi', 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'])
    }
}

def profileLqr(abiList = null) {
    return {
        ndk {
            abiFilters = abiList == null ? ['armeabi-v7a'] : abiList
        }
    }
}
複製代碼

注意:defaultConfig 中的 ndk abiFilters 須要註釋掉或刪除。

二、閉包組合

上面運用 函數 + 閉包 這種方式很巧妙實現了 "默認配置覆蓋",實現了 默認只保留 'armeabi' 這個要求,而且後期能夠動態替換。但感受彷佛擴展性不強,由於原生的多渠道配置是能夠很靈活的,你能夠很隨意的在不一樣的渠道中修改各類配置,例如:buildConfigFieldapplicationId 以及其餘未知的配置項。然而,咱們如今面向的不是閉包,而是一個函數,難道我每遇到一個未知的配置項,就要修改一次 profileLqr() 函數嗎?這顯然不可取,好在 groovy 的閉包非常強大,支持閉包組合!閉包組合有 2 個經常使用 api:

  • Closure#rightShift:向前組合
  • Closure#leftShift:反向組合

詳細使用可查看官方的 API 文檔:docs.groovy-lang.org/docs/groovy…

使用閉包組合 api,渠道配置能夠改寫成這樣:

yeshen profileLqr(['x86']).rightShift {
    buildConfigField "String", "CHANNEL", '"yeshen"'
}
複製代碼

然而,rightShift 還能夠進一步簡寫成 >>,因而,渠道配置還能改寫成這樣:

yeshen profileLqr(['x86']) >> {
    buildConfigField "String", "CHANNEL", '"yeshen"'
}
複製代碼

就問你這操做騷不騷~ 固然了,這裏對於 函數 + 閉包 在多渠道配置中的運用僅僅只是拋磚引玉,運動腦子,能夠建立更多的可能性。

三、最終腳本配置

最後,把最終的 gradle 配置貼一下,完結,撒花:

android {
    defaultConfig {
        ...
        // ndk {
        // abiFilters = ['armeabi-v7a']
        // }
    }

    productFlavors {
        xiaomi profileLqr() >> {
            buildConfigField "String", "CHANNEL", '"xiaomi"'
        }
        oppo profileLqr() >> {
            buildConfigField "String", "CHANNEL", '"oppo"'
        }
        mumu profileLqr() >> {
            buildConfigField "String", "CHANNEL", '"mumu"'
        }
        yeshen profileLqr(['x86']) >> {
            buildConfigField "String", "CHANNEL", '"yeshen"'
        }
        googleplay profileLqr(['armeabi', 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64']) >> {
            buildConfigField "String", "CHANNEL", '"googleplay"'
        }
    }
}

dependencies {
    // 依賴源碼
    // api 'com.android.tools.build:gradle:3.2.0'
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'tv.danmaku.ijk.media:ijkplayer-armv5:0.8.8'
    implementation 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.8'
    implementation 'tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8'
    implementation 'tv.danmaku.ijk.media:ijkplayer-x86:0.8.8'
    implementation 'tv.danmaku.ijk.media:ijkplayer-x86_64:0.8.8'
}

def profileLqr(abiList = null) {
    return {
        ndk {
            abiFilters = abiList == null ? ['armeabi-v7a'] : abiList
        }
    }
}
複製代碼

若是文章對您有所幫助, 請不吝點擊關注一下個人微信公衆號:FSA全棧行動, 這將是對我最大的激勵. 公衆號不只有Android技術, 還有iOS, Python等文章, 可能有你想要了解的技能知識點哦~

相關文章
相關標籤/搜索