最近在項目中遇到了一些打包的問題,順便去了解了下打包的一些知識點。這里主要介紹和總結(jié)了一下ant、build.xml的知識點以及構(gòu)建apk和jar包的一些注意事項。
Android打包
對工程代碼和資源文件使用打包工具進行編譯、混淆、簽名、優(yōu)化對齊等一系列步驟之后生成可發(fā)布到應(yīng)用市場的apk的構(gòu)建過程。
打包流程

大概分為以下幾個步驟
1、使用aapt工具將res資源文件生成R.java文件
2、使用aidl工具將aidl文件生成對應(yīng)java文件
3、使用javac命令編譯工程源代碼和上面兩步生成的文件,生成class文件
4、通過dex工具將class文件和第三方j(luò)ar包打成dex文件
5、用aapt工具將res下的資源文件編譯成二進制文件,然后將其和上一步中的dex文件以及assets中的文件通過apkbuilder工具打包成apk文件
6、通過jarsigner對apk進行簽名
7、利用zipalign工具對apk進行字節(jié)對齊優(yōu)化操作
打包方式 — Ant
Ant是將軟件編譯、測試、部署等步驟聯(lián)系在一起自動化構(gòu)建工具,主要用在java工程的構(gòu)建中,所以也可以用來進行android打包。
現(xiàn)在android開發(fā)工具基本上都用的AS,構(gòu)建用gradle,由于某些原因,項目組中用的還是eclipse+ant方式,所以暫時只介紹ant的構(gòu)建方式。雖然工具不一樣,但是整個構(gòu)建原理和流程還是一樣的。
Ant的默認(rèn)構(gòu)建文件為build.xml,輸入ant命令后,ant會在當(dāng)前目錄下搜索是否有build.xml,如果有,則執(zhí)行該文件,也可以自定義構(gòu)建文件,通過ant -f test.xml即可指定test.xml為構(gòu)建文件。
build.xml腳本
先看一個簡單的build.xml
<?xml version="1.0" encoding="GBK"?>
//ant默認(rèn)構(gòu)建文件即build.xml文件中需定義一個唯一的項目(Project標(biāo)簽),Project下可以定義若干個目標(biāo)(target標(biāo)簽)
//project名稱為MyApp, default表示默認(rèn)的運行target,為必須屬性,如果ant命令沒有指定target時,則運行default屬性中的target
//如MyApp工程目錄下直接輸入ant命令,則會直接打debug包。 basedir表示項目的基準(zhǔn)目錄
<project name="MyApp" default="debug" basedir=".">
//property標(biāo)簽用來設(shè)置屬性值,可以通過file標(biāo)簽來指定要加載的屬性文件的路徑,加載后屬性文件中的指定的屬性可以直接引用。
//為了方便配置,可以將環(huán)境變量聲明在build.properties中,并通過file引入到build.xml中
<property file="build.properties"></property>
//property中的name表示屬性的名稱 value表示屬性值 在其他地方可以通過${屬性名}進行引用, 類似于定義一個變量
<property name="outdir" value="bin" />
//tartget,表示一個構(gòu)建目標(biāo),也可以看成一個構(gòu)建步驟, 一次構(gòu)建過程中會執(zhí)行一個或者多個構(gòu)建步驟。
//target中的depends屬性表示target之間的依賴關(guān)系,一個target可以依賴其他的target標(biāo)簽,depends屬性也指定了target的執(zhí)行順序。
//ant會按照depends屬性中target的順序來依次執(zhí)行每個target。所以本文中target的執(zhí)行順序為 targetone -> targettwo -> debug
<target name="targetone">
//創(chuàng)建目錄
<mkdir dir="${outdir}"/>
</target>
//task是target中的子元素,一個target中可以有多個task,類似于target的子任務(wù),常用的task有echo、mkdir、delete、javac、java等等
<target name="targettwo" depends="targetone">
//刪除目錄
<delete dir="${name}"/>
</target>
<target name="debug" depends="targettwo">
//輸出日志信息
<echo>debug target perform...</echo>
</target>
</project>
打包成apk
build腳本中,一般android源工程打包成apk的執(zhí)行步驟大體如下:
gen-R->aidl->compile->obfuscate->dex->package-res-and-assets->package->jarsigner->zipalign->release
gen-R之前還有一些clean清除上次打包產(chǎn)生的文件的操作,這里不再贅述
1、gen-R
<target name="gen-R" depends="dirs">
<exec executable="${aapt}" failonerror="true">
<arg value="package" />
<arg value="-m" />
<arg value="-J" />
<arg value="${gen-dir}" />
<arg value="-M" />
<arg value="${manifest-xml}" />
<arg value="-S" />
<arg value="${resource-dir}" />
<arg value="-I" />
<arg value="${android-jar}" />
</exec>
</target>
gen-R 執(zhí)行aapt命令來編譯資源文件生成R.java文件 arg中的參數(shù)就是aapt中的命令行參數(shù),該target其實執(zhí)行的就是如下命令
aapt package -m -J gen -M AndroidManifest.xml -S res -I android.jar
具體參數(shù)命令含義如下:
-m 使生成的包的目錄放在-J參數(shù)指定的目錄。
-J 指定生成的R.java的輸出目錄
-M AndroidManifest.xml的路徑
-S res文件夾路徑
-I 某個版本平臺的android.jar的路徑
2、aidl
<target name="aidl" depends="dirs">
<apply executable="${aidl}" failonerror="true">
<arg value="-p${android-framework}" />
<arg value="-I${srcdir}" />
<arg value="-o${gen-dir}" />
<fileset dir="${srcdir}">
<include name="**/*.aidl" />
</fileset>
</apply>
</target>
此步驟主要是生成aidl文件對應(yīng)的java文件
使用apply標(biāo)簽可以進行批量運行task,此步驟即用build-tools下的aidl工具對src文件夾下的所有aidl文件進行批量轉(zhuǎn)換成java文件。
<apply>是作為<exec>的一個子類而被實現(xiàn),所以<exec>任務(wù)的所有屬性,都可以用于<apply>
3、compile
<target name="compile" depends="dirs, gen-R, aidl">
//javac標(biāo)簽用于編譯java文件生成class文件 destdir表示生成class文件的目錄
<javac encoding="UTF-8" target="1.5" debug="true" extdirs="" destdir="${outdir-classes}"
bootclasspath="${android-jar}" fork="true" memoryMaximumSize="512m" >
<src path="${srcdir-ospath}" />
<src path="${gen-dir-ospath}" />
//表示依賴庫的路徑 內(nèi)嵌在<javac>、<java>中
<classpath>
<fileset dir="${external-libs}" includes="*.jar" />
</classpath>
</javac>
</target>
compile執(zhí)行的是javac命令,編碼格式指定為utf-8,target指定生成的class文件與該版本的虛擬機兼容,保證在該版本的虛擬機上正常運行。
debug表示是否產(chǎn)生調(diào)試信息,默認(rèn)為false。extdirs為擴展文件的路徑,destdir指定了存放編譯后的class文件的文件夾路徑。bootclasspath指定了編譯過程中需要導(dǎo)入的class文件。fork指定是否再外部啟用一個新的JDK編譯器來執(zhí)行編譯,如果為false,則javac命令和ant將在同一個進程中執(zhí)行,并且javac命令被分配的內(nèi)存只有64MB,可能會導(dǎo)致java.lang.OutOfMemoryError(OOM)錯誤,如果fork為true,則另起一個進程來執(zhí)行javac命令,分配的內(nèi)存大小將由memoryMaximumSize來指定。src指定了java源文件的路徑,classpath指定了依賴的第三方j(luò)ar包路徑。
4、obfuscate
<target name="obfuscate" depends="compile">
//jar標(biāo)簽用來生成jar文件,basedir表示需要打包城jar文件的原文件目錄, destfile表示生成的jar文件
<jar basedir="${outdir-classes}" destfile="${outdir}/temp.jar"/>
//java標(biāo)簽用來執(zhí)行編譯生成的class文件 fork表示再一個新的虛擬機中運行該類 failonerror表示當(dāng)出現(xiàn)錯誤時是否自動停止
<java jar="${proguard.home}/proguard.jar" fork="true" failonerror="true">
//arg標(biāo)簽用來指定參數(shù) value是命令行參數(shù)
<arg value="-injars ${outdir}/temp.jar"/>
<arg value="-outjars ${outdir}/obfuscate.jar"/>
<arg value="-libraryjars ${android-jar}"/>
<arg value="-libraryjars ${third_part_jar}"/>
<arg value="@${proguard.config}"/>
</java>
<delete file="${outdir}/temp.jar"/>
<delete dir="${outdir-classes}"/>
<mkdir dir="${outdir-classes}"/>
<unzip src="${outdir}/obfuscate.jar" dest="${outdir-classes}"/>
<delete file="${outdir}/obfuscate.jar"/>
</target>
obfuscate混淆先是執(zhí)行了jar命令,將bin目錄下的class文件打包成temp.jar。然后執(zhí)行了proguard命令來壓縮、優(yōu)化和混淆操作。
-injars {class_path}指定要處理的應(yīng)用程序jar和目錄,即temp.jar
-outjars {class_path}指定處理完后要輸出的jar和目錄,即obfuscate.jar
-libraryjars {classpath}指定要處理的應(yīng)用程序jar和目錄所需要的程序庫文件,即其他依賴的第三方j(luò)ar包
混淆配置文件為proguard.config?;煜髣h除生成的臨時文件,并解壓obfuscate.jar到bin目錄下
5、dex
<target name="dex" depends="compile, obfuscate">
<apply executable="${dx}" failonerror="true" parallel="true">
<arg value="--dex" />
<arg value="--output=${intermediate-dex-ospath}" />
<arg path="${outdir-classes-ospath}" />
<fileset dir="${external-libs}" includes="*.jar" />
</apply>
</target>
dex就是用dx.bat工具將class文件轉(zhuǎn)換成classes.dex文件,即對上一步在bin/classes目錄中生成的優(yōu)化過的class文件以及依賴的第三方j(luò)ar包進行dex操作,最后在bin目錄下生成classes.dex文件。Parallel用于指定將多個task并行執(zhí)行。
6、package-res-and-assets
<target name="package-res-and-assets">
<exec executable="${aapt}" failonerror="true">
<arg value="package" />
<arg value="-f" />
<arg value="-M" />
<arg value="AndroidManifest.xml" />
<arg value="-S" />
<arg value="${resource-dir}" />
<arg value="-A" />
<arg value="${asset-dir}" />
<arg value="-I" />
<arg value="${android-jar}" />
<arg value="-F" />
<arg value="${resources-package}" />
</exec>
</target>
package-res-and-assets中執(zhí)行了aapt命令,來將res、assets目錄下的資源文件打包到resources.ap_
aapt package -f -M <AndroidManifest.xml路徑> -S <res路徑> -A <assert路徑> -I <android.jar路徑> -F <輸出的包目錄+包名>
7、package
<target name="package" depends="dex,package-res-and-assets">
<exec executable="${apk-builder}" failonerror="true">
<arg value="${out-unsigned-package-ospath}" />
<arg value="-u" />
<arg value="-z" />
<arg value="${resources-package-ospath}" />
<arg value="-f" />
<arg value="${dex-ospath}" />
<arg value="-rf" />
<arg value="${srcdir-ospath}" />
<arg value="-rj" />
<arg value="${external-libs-ospath}" />
<arg value="-nf" />
<arg value="${native-libs-dir-ospath}" />
</exec>
</target>
通過apkbuilder.bat工具根據(jù)classes.dex文件和resources.ap_生成未混淆的apk包
apkbuilder <輸出apk文件路徑> -z <資源文件路徑> -f <dex文件路徑> -rf <源碼目錄> -rj <第三方j(luò)ar包目錄> -nf <本地庫目錄>
8、jarsigner
<target name="jarsigner" depends="package">
<exec executable="${jarsigner}" failonerror="true">
<arg value="-verbose" />
<arg value="-storepass" />
<arg value="${password}" />
<arg value="-keystore" />
<arg value="${keystore.path}" />
<arg value="-signedjar" />
<arg value="${out-signed-package-ospath}" />
<arg value="${out-unsigned-package-ospath}" />
<arg value="${keystore.key}" />
</exec>
</target>
jarsigner是對上面生成的apk文件進行簽名操作
-verbose 簽名時輸出詳細信息
-storepass 密鑰庫密碼
-keystore 密鑰庫位置
-signedjar 后面接的參數(shù)依次是 簽名后的apk、待簽名的apk、密鑰庫別名
9、zipalign
<target name="zipalign" depends="jarsigner">
<exec executable="${zipalign}" failonerror="true">
<arg value="-v" />
<arg value="-f" />
<arg value="4" />
<arg value="${out-signed-package-ospath}" />
<arg value="${zipalign-package-ospath}" />
</exec>
</target>
zipalign target通過zipalign工具對簽名后的apk包進行字節(jié)對齊,好處是能夠減少應(yīng)用程序的RAM內(nèi)存資源消耗
-v 表示輸出詳細信息
-f 表示如果輸出文件已存在 則直接覆蓋
4 表示對齊為4個字節(jié)
10、release
<target name="release" depends="zipalign">
<!-- 刪除未簽名apk -->
<delete file="${out-unsigned-package-ospath}"/>
......
</target>
至此打一個完整的帶簽名的可發(fā)布的包的流程就結(jié)束了。執(zhí)行ant release命令即可完成打包。
打包成jar
由于jar包中不能包含資源文件,所以要通過jar包提供UI視圖供第三方使用,可以通過如下方式實現(xiàn):
1、使用硬編碼來實現(xiàn)布局文件
2、布局中的資源文件需放在assets文件夾中,然后打包到j(luò)ar中,通過流的方式讀取。這種方式將資源文件放在assets目錄下和java代碼一起打包為jar,其他工程依賴該jar包時,可以只引用jar包,不需要再額外導(dǎo)入資源文件,在該工程編譯應(yīng)用時會將jar包assets目錄中的文件與該工程中的assets目錄中的文件合并。注意assets目錄中的文件名與所導(dǎo)入工程中的文件名稱不能重復(fù),否則在編譯的時候會報錯“Error generating final archive: Found duplicate file for APK”提示有重名文件。
另外,打包到j(luò)ar中的資源文件必須是編譯之后的資源文件,即編譯成二進制文件,因為讀取資源時是通過流的方式讀取的,所以相關(guān)的資源文件必須在編譯成二進制文件之后再放入assets打包。
讀取方式如下
//讀取圖片
InputStream inputStream = context.getAssets().open(path);
Drawable drawable = Drawable.createFromResourceStream(
context.getResources(), value, inputStream, name);
//讀取xml圖片資源
XmlResourceParser parser = context.getAssets().openXmlResourceParser(path);
Drawable draw = Drawable.createFromXml(context.getResources(), parser);
jar包的構(gòu)建方式與apk的類似,執(zhí)行步驟大概為
aidl->compile->copy_asset->obfuscate->jarsigner
與打包成apk流程相比少了gen-R、aapt、dex、package-res-and-assets、package、zipalign等操作,需要注意就是obfuscate混淆這一步,
打成jar包時obfuscate如下:
<!-- Obscure the package file. -->
<target name="obfuscate" depends="compile, copy-asset">
<echo>Obscure the class files....</echo>
<jar basedir="${outdir-classes}" destfile="${out_original_jar}">
</jar>
<java jar="${proguard.home}/proguard.jar" fork="true" failonerror="true">
<arg value="-injars ${out_original_jar}"/>
<arg value="-injars $other_jar}"/>
<arg value="-libraryjars ${android-jar}"/>
...
<arg value="-outjars ${outdir}/${out_obfuscate_jar}"/>
<arg value="@${proguard.config}"/>
</java>
</target>
obfuscate混淆先是執(zhí)行了jar命令,將bin目錄下的class文件以及資源文件打包成jar包,然后執(zhí)行proguard命令來壓縮、優(yōu)化和混淆操作。這里需要注意的是如果該工程還依賴了其他jar包(未混淆),則打成jar的同時需要將其他jar包也引入進來,因為最后對外提供的是該工程的jar包。
另外需要注意的是proguard.cfg混淆文件中需要為其他jar包的類文件指明重命名類的包路徑
# Specifies to repackage all class files that are renamed, by moving them into the single given package
-repackageclasses 'com.example.otherjar'
一定要為一些重命名的class文件指明打到j(luò)ar包中的包路徑,jar包中所有的class文件需要有明確的包路徑,以防被第三方apk集成編譯時,這些class文件無法-keep,被編譯混淆之后找不到這些類,導(dǎo)致jar包功能異常。

而增加了路徑指定后,重命名的類就會被打到指定的包路徑下,其他地方對這些類的調(diào)用也能正常進行。

基本上要介紹的就這么多,可能會有理解錯誤的地方,歡迎一起討論!