Android應用編譯時自動生成版本號

近期有一個工作任務:按照某個規則,給Android應用設置一個在編譯時自動生成的versionCode與versionName。

這一點倒是不奇怪,很多正式的應用都有自己的一套版本號管理。市面上什麼某某應用幾點零,就是這樣的一個產物。

我這個任務的難度除了自動生成,還有一個附加條件:在Android項目中編譯(通過Android.mk)和在Android Studio中編譯(通過build.gradle)時,規則要保持一致。

這樣就有意思多了。

versionCode與versionName

Android的APK會自帶一個版本描述,這就是versionCode和versionName。其中,versionCode是一個int,應該是一個隨着開發而持續增長的值,而versionName是一個String,這是給人看的版本號。

versionName在開發過程中可以起到辨識作用,方便開發工程師與其他團隊的人交流,也是用戶直接可見的。比如某個問題是某某版本上出現的,某個功能是某某版本添加的。

versionCode在人的圈子裏其實沒太大作用,但是它是Android系統真正使用的值。在更新時,新的版本versionCode不能比原來的小,否則更新會失敗。另外要注意,int是有上限的,不能太亂來。

versionCode和versionName,直接體現在APK中的AndroidManifest.xml文件中。可以通過以下命令查看:

aapt dump badging PATH/TO/YOUR.apk | grep version

aapt是Android Asset Packaging Tool,就是Android應用在命令行的打包工具,SDK裏就有。主要作用是把二進制文件打包爲APK,也附帶了一些相關的查詢功能。

修改versionCode和versionName,最簡單、直觀的方法,就是改源代碼中的AndroidManifest.xml。但是,這樣就需要頻繁修改AndroidManifest.xml,每次版本更新時改一次。如果你希望每次提交的版本號都不一樣,那麼每次提交都需要密集地修改那兩行,這顯然是不可取的。

此外,實際上無論是在Android項目內編譯,還是在IDE編譯,最終都是用aapt來打包。打包時,可以通過設置--version-code--version-name參數,來對這兩個字段設值,並且在最終打包時添加到APK的AndroidManifest.xml中。

在Android源碼中編譯時,如果一個模塊沒有指定versionCode與versionName,那麼將與平臺相同。比如在Android Marshmallow (API level 23)中編譯,那麼versionCode就是23,versionName就是PLATFORM_VERSION,比如6.0.1。

版本號規則

版本號規則可以自定義,每家都不太一樣。

很多不在意這些的小項目,直接默認versionName爲1.0,至死不變——這其實也算是一個規則。

比較常見的還有先標爲0.1,開發到一段時間再標爲1.0併發布,如果有bug修正後叫1.1,增加一堆新功能後叫2.0。

這些版本號規則,簡單實用,通過改文件的方式就可以實現,不需要自動化。

以下以一個比較複雜的來舉例。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionName="aa.bb.cccc.yymmdd.xxxxx"
    android:versionCode="aabbcccc"
    package="com.example.xxx">

其中,aa是大版本號,bb是小版本號,最多2位,不足不補0。它們由人來自定義,必要時手動修改。比如說,某某應用v2.0,那麼aa爲2,bb爲0。版本號設計裏,有一部分是需要保留手工控制的,不然產品經理會因控制慾無法滿足而抓狂。

cccc是Git庫的提交次數,xxxxx是Git庫HEAD的SHA1碼前5位。這些都是隨着Git提交而確定的,在編譯過程中可以、也應該自動生成。在shell中執行以下命令可以獲取:

git rev-list --count HEAD # This is cccc
git describe --always # This is short SHA1

yymmdd是日期,yy是年的後兩位,mm是月,dd是日,不足補0。

如果是用命令行手工打包,寫個腳本就行了。而如果是在IDE或項目裏編譯,則需要修改編譯文件。

Android.mk中自動生成版本號

由於有Git命令牽涉其中,相對來說,Android.mk裏是更容易實現的。

Android.mk的語法,其實就是最原始的makefile,添加了一些自定義的東西。目前對普通開發者來說,也就NDK開發時有可能會接觸。但是在Android源碼編譯時,每一個小模塊都有至少一個這東西。

aa = 1
bb = 0
cccc = $(shell cd $(LOCAL_PATH) && git rev-list --count HEAD)
version_code = $(shell expr $(aa) \* 1000000 + $(bb) \* 10000 + $(cccc))
version_name := $(aa).$(bb).$(cccc).$(shell date +%y%m%d).$(shell cd $(LOCAL_PATH) && git describe --always)
LOCAL_AAPT_FLAGS += --version-code $(version_code)
LOCAL_AAPT_FLAGS += --version-name $(version_name)

由於Android.mk的內置函數和變量裏,並沒有versionCode和versionName這兩個值的對應,所以需要通過LOCAL_AAPT_FLAGS來設置aapt的參數。

除此之外,就沒有Android.mk特有的東西了,全是makefile自有的語法。

build.gradle中自動生成版本號

Android Studio中,gradle.build裏的設定會覆蓋AndroidManifest.xml中的設置。

Gradle中使用的是Groovy語言,這是一種基於JVM的敏捷開發語言,還算是易學易用的。

在項目的build.gradle中,android.defaultConfig裏,把versionCode和versionName改成自定義函數getSelfDefinedVersion

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"

    defaultConfig {
        applicationId "com.example.xxx"
        minSdkVersion 22
        targetSdkVersion 23
        versionCode getSelfDefinedVersion("code")
        versionName getSelfDefinedVersion("name")
    }
    buildTypes {
        release {
            minifyEnabled false
        }
    }
}

在build.gradle文件底部,統一實現版本號自動生成管理:

def getSelfDefinedVersion(type) {
    int aa = 1
    int bb = 0
    Process process = "git rev-list --count HEAD".execute()
    process.waitFor()
    int cccc = process.getText().toInteger()

    if ("code".equals(type)) {
        aa * 1000000 + bb * 10000 + cccc
    } else if ("name".equals(type)) {
        String today = new Date().format("yyMMdd")
        process = "git describe --always".execute()
        process.waitFor()
        String sha1 = process.getText().trim()
        "$aa.$bb.$cccc.$today.$sha1"
    }
}

這樣雖然比較簡陋,但功能是有了。

可以編譯後用aapt查看結果,也可以在gradle sync後,查看app/build/目錄下自動生成的BuildConfig.java文件。

package com.example.xxx;

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "com.example.xxx";
  public static final String BUILD_TYPE = "debug";
  public static final String FLAVOR = "";
  public static final int VERSION_CODE = 1000172;
  public static final String VERSION_NAME = "1.0.172.160713.08d62e0";
}

這個文件是Android Studio自動生成的,會打包進APK中,可以在Java源碼中直接引用。它受到編譯開關控制,不同編譯條件下會有不同的BuildConfig.java文件產生。

./app/build/generated/source/buildConfig/androidTest/debug/com/example/xxx/test/BuildConfig.java
./app/build/generated/source/buildConfig/debug/com/example/xxx/BuildConfig.java
./app/build/generated/source/buildConfig/release/com/example/xxx/BuildConfig.java

不過,它對我來說是無用的,因爲BuildConfig.java在項目裏用Android.mk編譯時不會產生。如果像我一樣有兼容兩邊的需求,調用它就是死路一條。要兼容兩套編譯方法還有其它的坑,這裏就不詳述了。

隱患

目前的實現方案,其實有一個已知的隱患。

當cccc大於10000時,在versionCode會發生進位,bb會變相增加;而在versionName,則不會發生進位,bb仍然是預設值。

假如對cccc進行除餘操作(cccc %= 10000),那麼在每次跨越10000時,versionCode的自增性會被破壞。所以每次發生這種情況,都要手動增加一下bb。所以,目前暫時採用前一種方式,至少保證無人工干預時,versionCode的自增性不變。

這個問題暫時不用擔心,可以等真的發生這個情況時再考慮解決。

目前,世界上第一個大型Git庫——git/git它自己,目前(2016年7月14日)的提交總次數爲43560。所以,假如cccc換成5位、甚至6位,這個問題可以拖延到海枯石爛。

當然,如果是我來設計,那麼versionCode等於提交次數就好了,不用搞什麼前綴。

總結

最後談一談技術之外的事:如何迅速在完全不懂的領域學以致用。

在做這個任務前(甚至之後),我對Makefile與Groovy的語法細節都沒有太多的瞭解。不過我知道兩點就夠了:

  1. Android源碼是靠Android.mk把各個小模塊組織起來的,而它本質上是Makefile
  2. Android Studio用的構建工具是Gradle,而Gradle實際上用的是Groovy來實現的

此後就是順藤摸瓜。

這說明,在這些冷門的技術領域,如果只是偶爾才使用到,那麼廣度遠比深度重要。沒有必要事事精通,因爲大多用不上;但至少要廣泛地建立索引,有切入點才便於按需學習。

而在Makefile與Groovy裏,對versionCode與versionName做那些不復雜卻很麻煩的拼接時,我在shell與Java裏的深入瞭解,幫了很大的忙。

Makefile本身設計簡單、功能有限,連加減乘除都要靠shell幫忙。如果懂shell,則可快速上手;如果不知shell爲何物(似乎一不小心又鄙視到了純Windows程序員),那就麻煩多了。正如張無忌學乾坤大挪移,沒有多年的九陽神功基礎,哪有幾個時辰後的神功大成、光明頂上耀武揚威。

Groovy與Java有很多共通之處,比如獲取Git提交次數的手段:

    Process process = "git rev-list --count HEAD".execute()
    process.waitFor()
    int cccc = process.getText().toInteger()

就與Java是同樣的思路:

        Runtime runtime = Runtime.getRuntime();
        try {
            Process process = runtime.exec("git rev-list --count HEAD");
            process.waitFor();
            InputStream inputStream = process.getInputStream();
            byte[] bytes = new byte[128];
            inputStream.read(bytes, 0, 128);
            String countStr = new String(bytes).trim();
            int cccc = Integer.parseInt(countStr);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }

當然,Java顯然要麻煩得多。只要精通Java,Groovy自然也手到擒來。

也許有人會懷疑:即使之前對Android.mk與Gradle沒有一點了解,也一樣可以通過搜索引擎解決這個問題。這似乎沒錯,只是慢了點而已。

不過,在評估這個任務的可行性、而不是去做時,這卻不成立。如果沒有預先的瞭解,根本就不會往這個方面去想。實際上,堅持用Android Studio開發系統應用,讓一個項目在Gradle下開發、在Android.mk下編譯使用,在我的工作環境與所知的網絡世界中,至今也只見我一人。

參考鏈接

英文還是要啃的,官網還是要上的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章