1. 程式人生 > >Android APP基於Android Studio多版本構建實戰

Android APP基於Android Studio多版本構建實戰

關於多版本構建,我們可以通過buildTypes來新增構建型別,一般而言這裡也不需要自行定義,預設會生成debug和release兩種型別。

重點在於使用productFlavors生成不同“風味”的版本,我們可以構建標準版和中性版APP,這在企業應用中非常普遍,中性版本不含有logo資訊,可再次貼牌等。同時應該在release版本中關閉log輸出。

我這裡的區別為中性版assets/defaultLogo/logo_default.png和標準版的資源是不一樣的,所以需要建立對應的productFlavor資料夾,放入需要替換的logo_default.png資源。同時不論是中性版還是標準版還有四種機型需要適配(F600P、F600、F200P、F200),也就是說我們需要生成8個版本的app。

1.目標

生成多個不同“風味”的版本很容易,只要定義productFlavors即可。但目標應該更進一步,通過只敲一次命令列生成所有的標準版和中性版release app,並且按照我們的命令格式重新命名apk檔案,最後只需要到build/outputs/apk拿出對應的安裝包。

2.使用Gradle命令列

(1).檢視gradle命令是否可用

預設情況下開啟Android Studio的Terminal面板,鍵入gradle -version會提示找不到這個命令,無法使用,這需要配置環境變數。
Android Studio的Terminal面板

(2).檢視Android Studio自帶Gradle安裝位置

在File-Settings中檢視Gradle home(Android Studio安裝後會自帶一個Gradle,就在其安裝目錄gradle下)所在位置。
File-Settings中檢視Gradle home

(3).配置Gradle環境變數

建議使用使用者變數,只有當前使用者可用。新增一個變數名為GRADLE_HOME、變數值為E:/as/gradle/gradle-2.14.1的環境變數。然後編輯path環境變數,新建一行,並填入%GRADLE_HOME%/bin/即可。
配置Gradle環境變數

(4).驗證

再次在Terminal中輸入gradle –version命令後回車。出現下面的gradle版本資訊,就說明gradle構建工具在命令列方式下已經正常工作。
驗證gradle –version命令

3.關閉log開關

當然前提是log可以全域性通過一個boolean值控制。
使用以下函式可以判斷是否debug版本,非debug版本也就是正式版關閉log即可。

/**
 * 判斷當前應用是否debug狀態
 *
 * @param context
 * @return
 */
public static boolean isApkDebug(Context context) {
    ApplicationInfo info = context.getApplicationInfo();
    return (info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
}

我在自定義的Application(MainApp.java)中根據判斷是否開啟log開關。

// Log開關
Logger.DEBUG = AppUtils.isApkDebug(this);

4.配置正式簽名

android{
…
signingConfigs {
        releaseSign {
            storeFile file("E:\\xx\\xx\\xx.keystore")
            storePassword "xxxxx"
            keyAlias "xx"
            keyPassword "xxxxx"
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.releaseSign
            …
}
}
…
}

這段程式碼很好理解,通過在android閉包下配置signingConfigs,並在buildTypes的release下引用即可。

5.配置productFlavors

productFlavors {
    // 中性版本F600P
    neutralF600P {
        // 這裡替換了assets內logo_default.png資源
        manifestPlaceholders = getVersionNameMap('F600P')
    }
    // 標準版本F600P
    standardF600P {
        manifestPlaceholders = getVersionNameMap("F600P")
    }
    // 中性版本F600
    neutralF600 {
        manifestPlaceholders = getVersionNameMap("F600")
    }
    // 標準版本F600
    standardF600 {
        manifestPlaceholders = getVersionNameMap("F600")
    }
    // 中性版本F200P
    neutralF200P {
        manifestPlaceholders = getVersionNameMap("F200P")
    }
    // 標準版本F200P
    standardF200P {
        manifestPlaceholders = getVersionNameMap("F200P")
    }
    // 中性版本F200
    neutralF200 {
        manifestPlaceholders = getVersionNameMap("F200")
    }
    // 標準版本F200
    standardF200 {
        manifestPlaceholders = getVersionNameMap("F200")
    }
}

通過在android閉包下配置8個版本的flavor,並給中性版建立對應的資料夾,放入需要替換的logo_default.png資源。Flavor對應的資料夾位於src下,和main平級。
src->
main
neutralF600P
neutralF600
neutralF200P
neutralF200
目錄結構

再來看getVersionNameMap(key)函式。

/**
 * 根據key獲取version Map
 * @param key
 * @return
 */
def getVersionNameMap(key) {
    def versionNameMap = [:]
    if (key == "F600P") {
        versionNameMap.D_VERSION_NAME = "F600P"
    } else if (key == "F600") {
        versionNameMap.D_VERSION_NAME = "F600"
    } else if (key == "F200P") {
        versionNameMap.D_VERSION_NAME = "F200P"
    } else if (key == "F200") {
        versionNameMap.D_VERSION_NAME = "F200"
    } else {
        versionNameMap.D_VERSION_NAME = "F600P"
    }

    return versionNameMap
}

解釋一下:manifestPlaceholders是android gradle dsl中定義的屬性,用來替換其中的一些欄位,使用groovy GString在AndroidMainifest中佔位,通過給manifestPlaceholders賦值Map,將Map中的值替換到AndroidMainifest中。
AndroidMainfest.xml中定義如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="xxx" >
<application
        android:name="xx.MainApp"
        android:allowBackup="true"
        android:hardwareAccelerated="false"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"><meta-data
            android:name="D_VERSION"
            android:value="${D_VERSION_NAME}" /></application>
</manifest>

Java程式碼中通過meta-data的對應value去配置為不同的版本,如F600P或F600,當然這部分程式碼需要在Java中實現對應的邏輯。

/**
 * 通過key獲取Application標籤下meta-data的值
 *
 * @param context
 * @param key
 * @return
 */
public static String getApplicationMetaDataVal(Context context, String key) {
    ApplicationInfo appInfo = null;
    String msg = null;
    try {
        appInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
        msg = appInfo.metaData.getString(key);
    } catch (NameNotFoundException e) {
        e.printStackTrace();
    }
    return msg;
}

接下來再看轉化為版本Version的程式碼,則部分程式碼並非必須的,相信看到這裡已經明白了,我們是間接的用AndroidMainfest中的meta-data標籤傳遞在build.gradle配置的值,從而達到讓java程式碼載入不同版本的目的。

// 獲取不同版本的對應配置引數
Version version = Version.F600P;
String versionName = AppUtils.getApplicationMetaDataVal(this, "D_VERSION");

if (versionName.equals(Version.F600P.getName())) {
    version = Version.F600P;
} else if (versionName.equals(Version.F600.getName())) {
    version = Version.F600;
} else if (versionName.equals(Version.F200P.getName())) {
    version = Version.F200P;
} else if (versionName.equals(Version.F200.getName())) {
    version = Version.F200;
}

Log.d("version", "v:" + version.getName());

versionConfig = VersionFactory.getVersionConfig(version);

6.定製重新命名邏輯

android {
…
    android.applicationVariants.all { variant ->
        variant.outputs.each { output ->
            def outputFile = output.outputFile
            if (outputFile != null && outputFile.name.endsWith('.apk')) {
                String flavorName = variant.productFlavors[0].name
                //修改apk檔名
                def fileName = "xxx-${flavorName}-${defaultConfig.versionName}-${defaultConfig.versionCode}-signed-${releaseTime()}.apk"
                def fileParentDir
                if (flavorName.startsWith("standard")) {
                    fileParentDir = flavorName.substring("standard".length(), flavorName.length());
                } else if (flavorName.startsWith("neutral")) {
                    fileParentDir = flavorName.substring("neutral".length(), flavorName.length());
                }
                File dir = new File(outputFile.parent + File.separator + fileParentDir + File.separator)
                if (!dir.exists()) {
                    dir.mkdirs()
                }
                output.outputFile = new File(dir, fileName)
            }
        }
    }
…
}
xxx-${flavorName}-${defaultConfig.versionName}-${defaultConfig.versionCode}-signed-${releaseTime()}.apk

這一大段就是重新命名後的apk檔名,最終生成的檔名為:

xxx-neutralF600P-v1.0.5-2455-signed-20170502.apk

很好懂,用GString的寫法佔位。其中後面還有一段的邏輯為把同一型號下標準版和中性版的app放到同一目錄下,如F600P資料夾下,放入neutralF600P 和standardF600P app。

最後來看一下releaseTime()函式,作用為按照yyyyMMdd格式化當前時間,即格式化為年月日的形式。

def releaseTime() {
    return new Date().format("yyyyMMdd",TimeZone.getTimeZone("UTC"))
}

7.最後一步

以上步驟配置好了生成簽名版程式的所有要素,最後在android studio命令列中敲入:

gradle aR

回車後任務開始進行,全部finish後到xx工程\xx\build\outputs\apk下取出apk即可。

詳解:
之所以可以使用gradle aR命令是因為gradle支援這種簡短的駝峰式寫法,實際上和gradle assembleRelease命令是等價的。

另外,我們使用這個命令生成的apk全部為release版本,雖然通過gradle build和gradle assemble都可以生成我們想要的release版本,但gradle build同時生成了debug版本,這對於我們來說是多餘的。
最後一步