Android App自動化之使用Ant編譯項目多渠道打包

隨著工程越來越復雜,項目越來越多,以及平臺的遷移(我最近就遷瞭2回),還有各大市場的發佈,自動化編譯android項目的需求越來越強烈,後面如果考慮做持續集成的話,會更加強烈。
經過不斷的嘗試,在ubuntu環境下,以花界為例,我將一步一步演示如何使用命令行,使用ant編譯android項目,打包多渠道APK。
要點:
(1). 編譯android的命令使用
(2). ant基本應用
(3). 多項目如何編譯(包含android library)
(4). 如何多渠道打包
ps:我將以最原始的方式來實現,而不是使用android自帶的ant編譯方式,並盡量詳細解釋,這樣有益於我們徹底搞懂android打包的基本原理。

1. Android編譯打包的整體過程
、 首先,我們假設我們現在已經有這樣的一個項目(多工程的,簡單的單工程就更簡單瞭):

world
├── baseworld //android library,基礎類庫,共享於其他主應用
├── floworld //android project,花界應用
├── healthworld //android project,健康視線應用
├── speciality //android project,其它應用
├── starworld //android project,其它應用
├── build.xml //ant編譯腳本,可用於整個項目的編譯,也可隻編譯某個工程
├── code_checks.xml
├── kaiyuanxiangmu_world.keystore //密鑰
└── README.md

一個大的項目world,下面有1個基礎Android Library和4個Android Project。我們要做的就是編譯這4個人project成對應的一系列各市場APK。
那麼我們在來看看baseworld和floworld的工程結構:
Android Library,baseworld:

baseworld
├── assets //assets目錄,其中文件可能會被主應用覆蓋
├── libs //存放第三方jar庫
├── res //類庫資源,其中文件可能會被主應用覆蓋
├── src //源碼,可直接供主應用使用
├── AndroidManifest.xml
├── lint.xml
├── proguard.cfg
├── project.properties
└── README.md

和Android Project,floworld:

floworld/
├── assets //assets目錄,主應用優先級高
├── build
├── data
├── libs //存放第三方jar庫
├── res //主應用資源,主應用優先級高
├── src //源碼,可直接供主應用使用
├── AndroidManifest.xml
├── build.xml //ant編譯腳本,可用於整個項目的編譯,也可隻編譯某個工程
├── default.properties
├── lint.xml
├── proguard.cfg
├── project.properties
└── README.md

結構已經出來瞭,那麼android打包主要是在做什麼?
說白瞭,先編譯java成class,再把class和jar轉化成dex,接著打包aaset和res等資源文件為res.zip(以res.zip示例),再把dex和res.zip合並為一個未簽名apk,再對它簽名,最終是一個帶簽名的apk文件。
當然這麼說忽略瞭很多細節。
下面我把這些步驟用一句話分別列舉如下,腦子裡先有一個整體的流程,後續再結合ant詳細展開:
(1). 生成用於主應用的R.java;
(2). 生成用於庫應用的R.java(如果有庫應用);
(3). 編譯所有java文件為class文件;
(4). 打包class文件和jar包為classes.dex;
(5). 打包assets和res資源為資源壓縮包(如res.zip,名字可以自己定義);
(6). 組合classes.dex和res.zip生成未簽名的APK;
(7). 生成有簽名的APK;
針對多項目同步發佈和多渠道打包問題,我們需要額外增加三個處理:
(1). 各個工程下建立一個build.xml,然後在整個項目的根目錄下建立一個build.xml,用於統一編譯各個工程的;
(2). 各個工程的build.xml,通過傳入市場ID和應用Version參數生成對應的版本
(3). 針對(1),(2)問題,建立一個批處理支持一鍵生成所有版本
大概流程即是如此。

2. 建立各個工程的ant腳本文件build.xml(位置:floworld/build.xml)
因為我們需要創建一些基本的文件目錄和清理上次生成的文件,所以我們簡單的定義一下幾個目標吧:init,main,clean。
代碼模板如下:


start initing …

finish initing.

3. 初始化
在正式打包之前,我們有必要說明一下可能需要用到的初始化變量和操作。
前面已經講述瞭打包的大概流程,現在,第一, 打包需要你使用哪個版本android.jar; 第二, 生成的R文件放到gen目錄下; 第三, 生成的classes文件放到bin目錄下; 第四, 生成的打包文件放到out目錄下; 第五, 生成的各市場版本放到build目錄下。目錄完全可以自定義。
所以,如下的初始化我們必須先要做好,不然後面會提示找不到目錄:


start initing …

finish initing.

… …

4. 生成R.java
Android Library和Android Project應用的R.java是來自不同的package的。比如:
(1). baseworld中導入的包是import com.tianxia.lib.baseworld.R;
(2). floworld中導入的包是import com.tianxia.lib.baseworld.R;
但是他們最終是調用統一的資源,所以這兩個R.java文件必須一致。
下面是主應用的R.java的生成腳本:

generating R.java for project to dir gen (using aapt) …



註意res和../baseworld/res兩個順序不能搞反,寫在前面具有高優先級,我們當然優先使用主應用的資源瞭,這樣就能正確覆蓋庫應用的資源,實現重寫。
庫應用的R.java的生成腳本差不多,區別是指定庫應用的AndroidManifest.xml,以用於生成的是不同的包和目錄。
另外,aapt的使用中特別說明瞭,為瞭庫應用的資源更好的可重用,我們庫應用生成的R.java字段不需要修飾為final,加上參數–non-constant-id即可。

generating R.java for library to dir gen (using aapt) …

這樣的話我們就可以生成2個正確的R.java文件瞭(如果你引用瞭兩個庫,則需要生成3個R.java,以此類推)。
結果如下:

gen
└── com
└── tianxia
├── app
│ └── floworld
│ └── R.java
└── lib
└── baseworld
└── R.java
5. 編譯java文件為class文件
使用javac命令把src目錄,baseworld/src目錄,gen/*/R.java這些java編譯成class文件:
命令原型是:

?
1
2
//示例
javac -bootclasspath -s -s -s -d bin/classes *.jar
轉化成ant腳本為:

compiling java files to class files (include R.java, library and the third-party jars) …

6. 打包class文件為classes.dex
這步簡單,用dx命令把上步生成的classes和第三方jar包打包成一個classes.dex。
命令原型是:

//示例
//後面可以接任意個第三方jar路徑
dx –dex –output=out/classes.dex bin/classes libs/1.jar libs/2.jar
  轉化成ant腳本為:

packaging class files (include the third-party jars) to calsses.dex …



7. 打包res,assets為資源壓縮包(暫且命名為res.zip)
還是使用aapt命令,如生成R.java最大的不同是參數-F,意思是生成res.zip文件。
命令原型和ant腳本差不多:

packaging resource (include res, assets, AndroidManifest.xml, etc.) to res.zip …

8. 使用apkbuilder命令組合classes.dex,res.zip和AndroidManifest.xml為未簽名的apk
apkbuilder命令能把class類,資源等文件打包成一個未簽名的apk,原型命令和ant腳本類似:

building unsigned.apk …



  這個命令比較簡單。

9. 簽名未簽名的apk
使用jarsigner命令對上步中產生的apk簽名。這是個傳統的java命令,非android專用。
原型命令和ant腳本差不多:



signing the unsigned apk to final product apk …



至此,完整具有打包功能瞭,最後的build.xml為:

start initing …

finish initing.

generating R.java for project to dir gen (using aapt) …

generating R.java for library to dir gen (using aapt) …

compiling java files to class files (include R.java, library and the third-party jars) …

packaging class files (include the third-party jars) to calsses.dex …

packaging resource (include res, assets, AndroidManifest.xml, etc.) to res.zip …

building unsigned.apk …

signing the unsigned apk to final product apk …

done.

  在工程目錄下運行ant:

$ant
Buildfile: build.xml

init:
[echo] start initing …
[mkdir] Created dir: /home/openproject/world/floworld/build/latest
[echo] finish initing.

main:
[echo] generating R.java for project to dir gen (using aapt) …
[echo] generating R.java for library to dir gen (using aapt) …
[echo] compiling java files to class files (include R.java, library and the third-party jars) …
[javac] Compiling 75 source files to /home/openproject/world/floworld/bin/classes
[javac] 註意:某些輸入文件使用或覆蓋瞭已過時的 API。
[javac] 註意:要瞭解詳細信息,請使用 -Xlint:deprecation 重新編譯。
[echo] packaging class files (include the third-party jars) to calsses.dex …
[echo] packaging resource (include res, assets, AndroidManifest.xml, etc.) to res.zip …
[echo] building unsigned.apk …
[exec]
[exec] THIS TOOL IS DEPRECATED. See –help for more information.
[exec]
[echo] signing the unsigned apk to final product apk …
[echo] done.

BUILD SUCCESSFUL
Total time: 28 seconds
成功的在build/latest目錄下生成一個product_latest_dev.apk,這就是默認的生成的最終的APK,可以導入到手機上運行。

10. 多渠道打包
目前主流的多渠道打包方法是在AndroidManifest.xml中的Application下添加一個渠道元數據節點。
比如,我使用的是友盟統計,它配置AndroidManifest.XML添加下面代碼:

  通過修改不同的Channel ID值,標識不同的渠道,有米廣告提供瞭一個不錯的渠道列表:https://wiki.youmi.net/PromotionChannelIDs.
實現多渠道自動打包,就是實現自動化過程中替換Channel ID,然後編譯打包。
這個替換需要用到正則表達式實現。
ant中提供的replace方法,功能太簡單瞭,replaceregrex又需要添加另外的jar包,而且我們後面我們實現ant傳參需要寫另外的linux shell腳本,所以我幹脆使用我熟悉的sed-i命令來實現替換。
替換命令:

#-i 表示直接修改文件
#$market是Channel ID, 後面會講到,是來自循環一個數組
#\1,\3分別表示前面的第1,3個括號的內容,這樣寫很簡潔
sed -i “s/\(android:value=\)\”\(.*\)\”\( android:name=\”UMENG_CHANNEL\”\)/\1\”$market\”\3/g” AndroidManifest.xml
渠道修改的問題解決瞭。
還記得前面定義的${apk-version},${apk-name},${apk-market}嗎?
ant提供瞭額外的參數形式可以修改build.xml中定義的屬性的值:ant -Dapk-version=1.0,則會修改${apk-version}值為1.0,而不是latest瞭,其他屬性類似。
所以,在工程下面這條命令會生成:

#結合前面講打build.xml
#會在build/1.0/目錄下生成floworld_1.0_appchina.apk
ant -Dapk-name=floworld -Dapk-version=1.0 -Dapk-market=appchina
命令問題通過ant的參數傳值也解決瞭。
現在需要的是批量生產N個市場的版本,既替換AndroidManifest.xml,又生成對應的apk文件,我結合上面說的亮點,寫瞭一個shell腳本(位置:world/floworld/build.sh):

#定義市場列表,以空格分割
markets=”dev appchina gfan”
#循環市場列表,分別傳值給各個腳本
for market in $markets
do
echo packaging floworld_1.0_$market.apk …
#替換AndroidManifest.xml中Channel值(針對友盟,其他同理)
sed -i “s/\(android:value=\)\”\(.*\)\”\( android:name=\”UMENG_CHANNEL\”\)/\1\”$market\”\3/g” AndroidManifest.xml
#編譯對應的版本
ant -Dapk-name=floworld -Dapk-version=1.0 -Dapk-market=$market
done
好的,在工程目錄下執行build.sh:

# ./build.sh
packaging floworld_1.0_dev.apk …
Buildfile: build.xml
… …
packaging floworld_1.0_appchina.apk …
Buildfile: build.xml
… …
packaging floworld_1.0_gfan.apk …
Buildfile: build.xml
… …
  在build下生成瞭對應的apk文件:

build
├── 1.0
│ ├── floworld_1.0_appchina.apk
│ ├── floworld_1.0_dev.apk
│ └── floworld_1.0_gfan.apk
└── README.md
  成功生成!

11. 工程腳本的執行目錄問題
上面的腳本執行之後的確很cool,但是有一個問題,我必須在build.sh目錄下執行,才能正確編譯,這個和build.xml中定義的相對路徑有關。
我們必須在任何目錄執行工程目錄下的build.sh都不能出錯,改進build.sh為如下:

#!/bin/bash
#添加如下兩行簡單的代碼
#1. 獲取build.sh文件所在的目錄
#2. 進入該build.sh所在目錄,這樣執行起來就沒有問題瞭
basedir=$(cd “$(dirname “$0″)”;pwd)
cd $basedir

markets=”dev appchina gfan”
for market in $markets
do
echo packaging floworld_1.0_$market.apk …
sed -i “s/\(android:value=\)\”\(.*\)\”\( android:name=\”UMENG_CHANNEL\”\)/\1\”$market\”\3/g” AndroidManifest.xml
ant -Dapk-name=floworld -Dapk-version=1.0 -Dapk-market=$market
done
現在你在項目根目錄下執行也沒有問題:./floworld/build.sh,不會出現路徑不對,找不到文件的錯誤瞭。

12. 建立整個項目的自動化編譯腳本(位置:world/build.sh)
單個工程的自動化打包沒有問題瞭,但是一個項目下有N個工程,他們往往需要同步發佈(或者daily build也需要同步編譯),所以有必要建立一個項目級別的編譯腳本:
build.sh(項目根目錄下,位置:/world/build.sh)
最簡單的傻瓜式的做法就是,遍歷項目下的工程目錄,如果包含工程編譯的build.sh,則編譯該工程.
shell腳本如下:

#!/bin/bash
#確保進入項目跟目錄
basedir=$(cd “$(dirname “$0″)”;pwd)
cd $basedir
#遍歷項目下各工程目錄
for file in ./*
do
if test -d $file
then
#進入工程目錄
cd $basedir/$file
#查找該工程目錄下是否存在編譯腳本build.sh
if test -f build.sh
then
echo found build.sh in project $file.
echo start building project $file …
./build.sh
fi
#重要,退出工程目錄到項目根目錄下
cd $basedir
fi
done
  執行該腳本:

# ./build.sh
found build.sh in project ./floworld.
start building project ./floworld …
packaging floworld_1.0_dev.apk …
Buildfile: build.xml

found build.sh in project ./healthworld.
start building project ./healthworld …
Buildfile: build.xml

成功自動尋找,並編譯打包。

13. 其他細節
為瞭盡量詳盡,我一再解說,但是還有一些細節未包括其中,如編譯後清理clean目標,apk對齊優化,java代碼混淆等,請參考其他資料,在此省略。
另外,我反編譯生成的apk,查看Androidmanifest.xml均正確對應,驗證通過。

發佈留言