1, 新建專案VariantTest
2, 生成keystore
可以看到, 默認的build variant只有debug一種

當我試圖選release的時候,發現報錯了

什么錯呢

大致意思是說我們的app沒有簽名
我們知道簽名需要一個keystore, 那么作為一個個人開發者,怎么獲取keystore呢?
studio給我們提供了創建keystore的方式:





現在我們已經有了keystore, 那么下一步就是給專案添加簽名資訊

加完這些以后同步一下, 我們看到已經可以build release app了

以為這就大功告成了嗎? 點擊installRelease,
....幾秒鐘之后, 我得到了一個error
Execution failed for task ':app:installRelease'.
> java.util.concurrent.ExecutionException: com.android.builder.testing.api.DeviceException: com.android.ddmlib.InstallException: INSTALL_FAILED_UPDATE_INCOMPATIBLE: Package com.example.varianttest signatures do not match the previously installed version; ignoring!
意思是說, 當前試圖安裝的應用(com.example.varianttese)的簽名和之前安裝的不匹配, (因為我之前已經安裝了一個debug app), 雖然這次裝的是release, 但因為沒改包名,所以被認為是同一個app,
這里也體現了android的應用簽名機制,
那就改下包名吧:
如果我們的app只有debug和release兩種, 那么完全可以在buildType/release下面宣告一個不同的applicationId
但是鑒于我們后面還需要添加多個variants, 因此我們新建一個gradle檔案來處理包名-
app_ids.gradle
android.applicationVariants.all { variant ->
def buildType = variant.buildType.name
def applicationId = "com.example.varianttest"
if (buildType.toLowerCase().contains("release")){
applicationId += ".release"
}
variant.mergedFlavor.setApplicationId(applicationId)
}
然后, 在app/build.gradle 檔案頭部去參考它:
apply from: '../app_ids.gradle'
同步一下, 再點擊installRelease, 很快我手機上就有了兩個app-- VariantTest

這當然是不能接受的, 因為它兩長得一模一樣,我完全分不清,
怎么去改app名字呢? 我們知道app名字定義在manifest中, 所以我們很容易想到新建一個manifest檔案for release

只需要在src下面新建release目錄, 放入manifest, 完全不需要其他的配置, 編譯release app時就會讀取release目錄目錄下的manifest并和默認manifest合并,
tools: replace的作用就是告訴編譯器,需要將該屬性替換
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.varianttest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name_release"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.VariantTest"
tools:replace="android:label">
</application>
</manifest>
如此操作之后, 我們得到了兩個名字不一樣的app, 同理也可以改app圖示, 這里不再演示,

3,從cert/prod的角度構建不同的app
現在為止我們得到debug/release兩個app, 通常來說,debug app不做混淆, 會顯示一些我們需要的log,并且可以斷點除錯
如果我們只是平時自己寫著玩,這個buildType就夠了,
但是對于絕大多數app來說,都不可避免地要使用網路和server互動,同一條請求,測驗環境和生產環境要用到不同的domain,傳入不同的引數, 或者有的功能我們希望只在測驗app中開放
這個時候老板就希望我們能給build variants加上cert/ production兩種
同時在開發程序中, app端和server端往往同步開工, 那么在api沒有ready的情況下我們也希望有個mock環境能供我們除錯native UI
那就開始搞吧
新建一個gradle檔案-- environment_flavors.gradle: 定義了cert, prod, mock三種環境
android {
productFlavors {
cert {
dimension 'environment'
}
production {
dimension 'environment'
}
mock {
dimension 'environment'
}
}
}
然后在app/build.gradle首部添加
apply from: '../environment_flavors.gradle'
并且申明 flavors: environment:

同步一下, 現在我們已經可以看到這些variants:

我們也希望它們有不同的包名, 這樣我可以在一臺device上同時安裝多個variants
因此我們修改app_ids.gradle, 修改后的代碼如下:(紅色為本次修改的部分)
android.applicationVariants.all { variant ->
def buildType = variant.buildType.name
def applicationId = "com.example.varianttest"
def environmentName = variant.productFlavors[0].name
if (environmentName == "cert") {
applicationId += ".cert"
}
if (environmentName == "production") {
applicationId += ".prod"
}
if (environmentName == "mock") {
applicationId += ".mock"
}
if (buildType.toLowerCase().contains("release")){
applicationId += ".release"
}
variant.mergedFlavor.setApplicationId(applicationId)
}
包名不同保證了我們可以同時安裝, 此外我們也希望這些app有不同的名字,否則裝在一起我們完全不知道誰是誰
這時我們已經不大可能為每個variant都去創建一個manifest了, 怎么辦呢?我們可以使用占位符來解決
manifest檔案中:
android:label="@string/app_name${appNameEnv}${appNameBuildType}"
再修改app/build.gradle:
defauleConfig{
...
manifestPlaceholders = [appNameEnv: "", appNameBuildType: ""]
}
buildTypes {
release {
...
manifestPlaceholders.appNameBuildType = '_release'
}
}
然后 environment_flavors.gradle:
android {
productFlavors {
cert {
dimension 'environment'
manifestPlaceholders.appNameEnv = '_cert'
}
production {
dimension 'environment'
manifestPlaceholders.appNameEnv = '_prod'
}
mock {
dimension 'environment'
manifestPlaceholders.appNameEnv = '_mock'
}
}
}
同步一下, 現在我們已經可以得到6個build了

但是到目前為止, cert/prod/mock的內容完全一樣,根本體現不出應有的價值,那么接下來就是最關鍵的操作了, 怎么讓不同的build去關聯不同的環境呢?
我們很容易想到通過BuildConfig在代碼中獲取到當前的build flavors, 然后可以據此判斷,設定不同的環境,如下面的代碼:

當不同環境之間只有極少數區別且不涉及頻繁改動的時候, 這種方式當然也可以, 但缺點是耦合性太高,不利于后期的維護和擴展
因此在專案中, 我更偏向于使用一個json檔案來描述不同的配置, 比如我們之前在environment_flavors.gradle中宣告了3種flavors: cert/mock/production
那么對應的,我們可以在app/src下面創建3個assets檔案夾,分別放入apiConfig.json

apiConfig.json (mock和production中host的值分別對應.mock和.production)

定義data class ApiConfiguration
data class ApiConfiguration(
val host: String
)
創建一個工具類讀取assets中的json檔案并轉換為ApiConfiguration物件
interface AssetsLoader {
fun getApiConfiguration() : ApiConfiguration
}
class ApplicationAssetsLoader(private val configLoader: ConfigurationLoader) : AssetsLoader {
override fun getApiConfiguration(): ApiConfiguration {
return loadConfig("apiConfig.json")
}
private inline fun <reified T : Any> loadConfig(fileName: String): T {
return configLoader.requireConfig(fileName)
}
}
interface ConfigurationLoader {
fun <T : Any> loadConfig(fileName: String, type: KClass<T>): T?
}
inline fun <reified T : Any> ConfigurationLoader.requireConfig(
fileName: String
): T {
return loadConfig(fileName, T::class)
?: throw IllegalStateException("$fileName config file does not exist")
}
class JsonConfigurationLoader(
val gson: Gson,
val assets: AssetManager
) : ConfigurationLoader {
override fun <T : Any> loadConfig(fileName: String, type: KClass<T>): T? {
return try {
BufferedReader(InputStreamReader(assets.open(fileName)))
.use { reader -> gson.fromJson(reader, type.java) }
} catch (e: IOException) { // Exception is thrown if file is missing or couldn't be read
null
}
}
}
這里其實可以寫得很簡單, 本例中因為考慮到后面不同variants可能還要讀取一些不同的檔案型別, 所以抽象出了介面,
因為我們之前在app/build.gradle中已經宣告了:
flavorDimensions 'environment'
所以只要上面我們新建的那三個檔案夾的名字和environment_flavors.gradle中宣告的一致,就不再需要其他的任何配置, 每個buildVariants都可以讀到正確的json檔案
簡單測驗一下,代碼如下
本例中使用了MVVM, 資料驅動UI,分別跑一下cert/mock/prod app, 可以看到它們都拿到了正確的環境配置

4, Mock環境搭建
看到這里, 聰明的小伙伴們肯定會有個疑問, mock環境通常供開發者除錯ui使用, 并不涉及和api的互動, 所以自然也就不需要api domain之類的東西
那么怎么實作mock呢?
比如,現在我們有一條網路請求getMoney,要去server拿response顯示在home頁面
于是我們根據api同事預先提供的回傳資料格式寫了資料類
data class GetMoneyResponse(
val name: String,
val count: Int,
val type: String,
val currency: String
)
介面GetMoneyRepository:
interface GetMoneyRepository {
fun getMoney(): GetMoneyResponse
}
介面實作類:
class GetMoneyRepositoryImpl() : GetMoneyRepository {
override fun getMoney(): GetMoneyResponse {
//這里應該要去call api
//本例省去了這個步驟
return GetMoneyResponse("name", 0, "type", "currency")
}
}
然后在viewModel中呼叫
private val _response = MutableLiveData<GetMoneyResponse>().apply {
value = https://www.cnblogs.com/haigs/archive/2021/09/24/GetMoneyRepositoryImpl().getMoney()
}
val response: LiveData<GetMoneyResponse> = _response
在fragment 顯示
private fun getData(){
homeViewModel.response.observe(viewLifecycleOwner, {
responseView.text = it.name + "通過:" + it.type + "賺到了:"+ it.count + it.currency
})
}
至此, native部分就寫完了,可是在api遲遲沒有ready的情況下, 我們怎么用mock資料來測驗呢?
上文中, 我們已經為mock環境創建了mock檔案夾,并放入了mock build會用到的assets檔案
現在我們在該檔案夾下新建兩個子目錄with和without
將類GetMoneyRepositoryImpl移到without目錄下, 我們希望真實環境(cert/prod)下可以編譯這個檔案
然后在with目錄下再創建一個GetMoneyRepositoryImpl供mock環境使用
class GetMoneyRepositoryImpl() : GetMoneyRepository {
override fun getMoney(): GetMoneyResponse {
//因為這個類供mock使用, 因此我們可以直接回傳我們想要的任何response
//通常的做法是在mock/assets下加入我們想要的response檔案,如 getMoneyResponse.json, 然后讀取assets
//本例中簡化了這一步
return GetMoneyResponse("張三", 500, "搬磚", "人民幣")
}
}
所以現在的目錄就變成了這樣

注意, 這里的兩個實作類GetMoneyRepositoryImpl擁有完全相同的類名和包名,只是方法實作不同
因此, 我們會發現viewModel里面報錯了, 因為編譯器不允許同時存在兩個一樣的類
所以下一步,我們就需要告訴編譯器,什么時候該用哪個類
在app/build.gradle 下面添加如下描述:
android {
String mockSources = "src/mock/with"
String noMockSources = "src/mock/without"
sourceSets {
main {
java.srcDirs += ['src/main/kotlin']
}
cert {
java.srcDirs += [noMockSources]
}
mock {
java.srcDirs += [mockSources]
}
production {
java.srcDirs += [noMockSources]
}
}
}
這段的作用就是告訴編譯器,mock環境就編譯“src/mock/with”下面的代碼, 否則就編譯“src/mock/without”下的代碼
大功告成, 我們分別安裝cert和mock app驗證一下:

5, 多維變體
就當我覺得可以松一口氣的時候,老板又提出了新需求, 隨著公司業務的不斷擴展, 我們的app在全球范圍內都有了客戶群,各種風格/功能上的差異已經不僅僅是改改copy就能解決的了,所以老板希望我們能再增加一個國家的維度, 給不同的國家提供不通的app
本質上講, 這和上文說到的environment變體并沒有什么不同, 只是新增一個維度而已, 下面我們來看具體實作
新建country_flavors.gradle, 為了簡單,我們只宣告了china和uk兩個國家
android {
productFlavors {
china {
dimension 'country'
}
uk {
dimension 'country'
}
}
}
在app/build.gradle中參考這個檔案
apply from: '../country_flavors.gradle'
并修改flavorDimensions, 增加country維度
flavorDimensions 'country', 'environment'
修改app_ids.gradle,讓不同的國家擁有不同的包名
android.applicationVariants.all { variant ->
def buildType = variant.buildType.name
def countryName = variant.productFlavors[0].name.toUpperCase()
def environmentName = variant.productFlavors[1].name
def appIdCountry = AppId.valueOf(countryName)
def applicationId = appIdCountry.appId
if (environmentName == "cert") {
applicationId += appIdCountry.certSuffix
}else if (environmentName == "production") {
applicationId += appIdCountry.productionSuffix
} else if (environmentName == "mock") {
applicationId += appIdCountry.mockSuffix
}
if (buildType.toLowerCase().contains("release")){
applicationId += appIdCountry.RELEASE_SUFFIX
}
variant.mergedFlavor.setApplicationId(applicationId)
}
enum AppId {
CHINA("com.variant.china"),
UK("com.variant.uk")
public final String appId
private final static String MOCK_SUFFIX = ".mock"
private final static String CERT_SUFFIX = ".cert"
public final static String RELEASE_SUFFIX = ".release"
private final static String PROD_SUFFIX = ".prod"
public final String certSuffix
public final String mockSuffix
public final String productionSuffix
AppId(String appId, String certSuffix = CERT_SUFFIX, String prodSuffix = PROD_SUFFIX, String mockSuffix = MOCK_SUFFIX) {
this.appId = appId
this.certSuffix = certSuffix
this.mockSuffix = mockSuffix
this.productionSuffix = prodSuffix
}
}
同時, 在app/src目錄下新建china/res/values/strings.xml :
<resources>
<string name="app_name_cert">Variant China Cert Debug</string>
<string name="app_name_cert_release">Variant China Cert Release</string>
<string name="app_name_mock">Variant China Mock Debug</string>
<string name="app_name_mock_release">Variant China Mock Release</string>
<string name="app_name_prod">Variant China Prod Debug</string>
<string name="app_name_prod_release">Variant China Prod Release</string>
<string name="title_home">主頁</string>
<string name="title_dashboard">活動</string>
<string name="title_notifications">通知</string>
</resources>
和 uk/res/values/strings.xml:
<resources>
<string name="app_name_cert">Variant UK Cert Debug</string>
<string name="app_name_cert_release">Variant UK Cert Release</string>
<string name="app_name_mock">Variant UK Mock Debug</string>
<string name="app_name_mock_release">Variant UK Mock Release</string>
<string name="app_name_prod">Variant UK Prod Debug</string>
<string name="app_name_prod_release">Variant UK Prod Release</string>
<string name="title_home">Home</string>
<string name="title_dashboard">Dashboard</string>
<string name="title_notifications">Notifications</string>
</resources>
這樣,不同的app也可以讀到不同的copy,顯示不同的包名
注意這里和android 的copy 國際化不太一樣, 沒有根據local來確定copy, 而是根據我們自己設定的build variant, 處理更加靈活
6, 現在我們已經可以從country的維度來build出不同的app了, 那么接下來, 怎么讓不同的country有不同的功能呢?
類似于上文第4步, 在app/src/china以及app/src/uk目錄下新建assets檔案夾, 加入featureConfig.json (China配置為true, uk配置false)
{
"showImage": true
}
我們根據該config來決定要不要顯示首頁的一張圖片
private fun initImageView(){
homeViewModel.showImage.observe(viewLifecycleOwner, {showImage ->
if (showImage){
imageView.visibility = View.VISIBLE
} else {
imageView.visibility = View.GONE
}
})
homeViewModel.getFeatureConfiguration(requireContext())
}
json檔案的讀取也與第4步相似,不再贅述,我們直接來看結果, 下圖中左邊是china, 右邊是uk

7, 按需打包
看到這里, 我們就掌握了多維app構建的基本方法,當然我們還可以增加更多的維度,比如按應用市場, baidu/huawei/xiaomi 等等, 但基本原理都是一樣的,
然而,就當我準備關電腦下班時, 老板又找到了我, 提出了新需求:
在我等加班??幾年的努力下, 我們的app功能不斷增多, 引入了大量的第三方庫, 導致的結果就是app size不斷增大, 眼看就要突破google設定的150M生死線, 所以給app瘦身就成了當前迫在眉睫的問題,
經粗略統計, 我們共引入了幾十個第三方庫, 但是并非所有的app都需要這些庫, 所以我們應該通過country來配置依賴
這里我們以 okhttp為例, 假設china 需要okhttp在首頁加載一張圖片, 但uk全程都不需要
那我們先新建country_implementations.gradle檔案, 并在app/build.gradle中參考
dependencies {
chinaImplementation "com.squareup.okhttp3:okhttp:4.4.0"
chinaImplementation "com.squareup.okhttp3:okhttp-urlconnection:4.4.0"
}
注意這里的 chinaImplementation 意思就是只給china 添加依賴,
不用擔心編譯器找不到這個方法, 因為我們之前已經宣告了名為 country的flavor, 包括了china和uk
所以編譯器完全可以識別這個命令, 就跟我們平常用的testImplementation, debugImplementation一樣
接下來就是怎么呼叫的問題了, 以前我們直接在整個工程下添加依賴, 這樣專案里的任何地方都可以獲取到該依賴
但現在,因為我們只給china 加了,所以不能在工程代碼里直接呼叫,否則,當你build uk app時根本找不到
比如,當我build uk variant時, 這行代碼是報錯的

怎么解決呢?
其實思路和上文中搭建mock環境一樣

在src下新建journey/okhttp/main/java目錄, 分別放入兩個同名的工具類HttpJourney
在with/main/java目錄下的檔案里, 我們實作了我們要用到的http journey的一些方法
在without/main/java目錄下的檔案里, 只需要定義空方法,或者直接throw exception
然后在fragment里呼叫
private fun initImageView(){
homeViewModel.showImage.observe(viewLifecycleOwner, {showImage ->
if (showImage){
HttpJourney().getImageViaHttp()
imageView.visibility = View.VISIBLE
} else {
imageView.visibility = View.GONE
}
})
homeViewModel.getFeatureConfiguration(requireContext())
}
注意這里 showImage 在uk config里的配置一定是false .
代碼部分加完了, 最后一步就是告訴編譯器, china和uk分別要編譯哪些journey的代碼
我們繼續在country_implementations.gradle中添加如下代碼
enum Journey {
HTTP_JOURNEY("okhttp")
public final String sourceSetName
Journey(String sourceSetName) {
this.sourceSetName = sourceSetName
}
}
static def addJourneySources(String country, List<Journey> journeys, sourceSets) {
def included = Journey.values().toList().intersect(journeys)
def excluded = Journey.values() - included
def sourceSet = sourceSets.findByName(country)
for (journey in included) {
sourceSet.java.srcDirs += "src/journey/${journey.sourceSetName}/with/main/java"
}
for (journey in excluded) {
sourceSet.java.srcDirs += "src/journey/${journey.sourceSetName}/without/main/java"
}
}
android {
sourceSets { container ->
china {
ArrayList journeys = new ArrayList([Journey.HTTP_JOURNEY])
addJourneySources(name, journeys, container)
}
uk {
ArrayList journeys = new ArrayList([])
addJourneySources(name, journeys, container)
}
}
}
代碼很簡單,相信大家都能看懂, 這里就不廢話了
看看結果,分別build和china和uk的cert app

umm, 效果還是有的??
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/302954.html
標籤:其他
上一篇:OC原始碼剖析物件的本質
下一篇:OC原始碼剖析物件的本質
