近期有一個工作任務:按照某個規則,給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的語法細節都沒有太多的瞭解。不過我知道兩點就夠了:
- Android源碼是靠Android.mk把各個小模塊組織起來的,而它本質上是Makefile
- 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下編譯使用,在我的工作環境與所知的網絡世界中,至今也只見我一人。
參考鏈接
英文還是要啃的,官網還是要上的。
- Android.mk:http://android.mk/
- Makefile語法:http://www.gnu.org/software/make/manual/
- Groovy官方文檔:http://groovy-lang.org/documentation.html
- Groovy執行shell命令:http://www.joergm.com/2010/09/executing-shell-commands-in-groovy/