Osheep

时光不回头,当下最重要。

Android打包—Ant

最近在项目中遇到了一些打包的问题,顺便去了解了下打包的一些知识点。这里主要介绍和总结了一下ant、build.xml的知识点以及构建apk和jar包的一些注意事项。

Android打包

对工程代码和资源文件使用打包工具进行编译、混淆、签名、优化对齐等一系列步骤之后生成可发布到应用市场的apk的构建过程。

打包流程

《Android打包—Ant》

build.png

大概分为以下几个步骤
1、使用aapt工具将res资源文件生成R.java文件
2、使用aidl工具将aidl文件生成对应java文件
3、使用javac命令编译工程源代码和上面两步生成的文件,生成class文件
4、通过dex工具将class文件和第三方jar包打成dex文件
5、用aapt工具将res下的资源文件编译成二进制文件,然后将其和上一步中的dex文件以及assets中的文件通过apkbuilder工具打包成apk文件
6、通过jarsigner对apk进行签名
7、利用zipalign工具对apk进行字节对齐优化操作

打包方式 — Ant

Ant是将软件编译、测试、部署等步骤联系在一起自动化构建工具,主要用在java工程的构建中,所以也可以用来进行android打包。

现在android开发工具基本上都用的AS,构建用gradle,由于某些原因,项目组中用的还是eclipse+ant方式,所以暂时只介绍ant的构建方式。虽然工具不一样,但是整个构建原理和流程还是一样的。

Ant的默认构建文件为build.xml,输入ant命令后,ant会在当前目录下搜索是否有build.xml,如果有,则执行该文件,也可以自定义构建文件,通过ant -f test.xml即可指定test.xml为构建文件。

build.xml脚本

先看一个简单的build.xml

<?xml version="1.0" encoding="GBK"?>

//ant默认构建文件即build.xml文件中需定义一个唯一的项目(Project标签),Project下可以定义若干个目标(target标签)
//project名称为MyApp, default表示默认的运行target,为必须属性,如果ant命令没有指定target时,则运行default属性中的target
//如MyApp工程目录下直接输入ant命令,则会直接打debug包。 basedir表示项目的基准目录
<project name="MyApp" default="debug" basedir=".">

//property标签用来设置属性值,可以通过file标签来指定要加载的属性文件的路径,加载后属性文件中的指定的属性可以直接引用。
//为了方便配置,可以将环境变量声明在build.properties中,并通过file引入到build.xml中
  <property file="build.properties"></property>

//property中的name表示属性的名称 value表示属性值 在其他地方可以通过${属性名}进行引用, 类似于定义一个变量
  <property name="outdir" value="bin" />

//tartget,表示一个构建目标,也可以看成一个构建步骤, 一次构建过程中会执行一个或者多个构建步骤。
//target中的depends属性表示target之间的依赖关系,一个target可以依赖其他的target标签,depends属性也指定了target的执行顺序。
//ant会按照depends属性中target的顺序来依次执行每个target。所以本文中target的执行顺序为 targetone -> targettwo -> debug
  <target name="targetone">
  //创建目录
    <mkdir dir="${outdir}"/>
  </target>

//task是target中的子元素,一个target中可以有多个task,类似于target的子任务,常用的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的执行步骤大体如下:
gen-R->aidl->compile->obfuscate->dex->package-res-and-assets->package->jarsigner->zipalign->release

gen-R之前还有一些clean清除上次打包产生的文件的操作,这里不再赘述
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 执行aapt命令来编译资源文件生成R.java文件 arg中的参数就是aapt中的命令行参数,该target其实执行的就是如下命令
aapt package -m -J gen -M AndroidManifest.xml -S res -I android.jar
具体参数命令含义如下:
-m 使生成的包的目录放在-J参数指定的目录。
-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文件对应的java文件
使用apply标签可以进行批量运行task,此步骤即用build-tools下的aidl工具对src文件夹下的所有aidl文件进行批量转换成java文件。
<apply>是作为<exec>的一个子类而被实现,所以<exec>任务的所有属性,都可以用于<apply>

3、compile

    <target name="compile" depends="dirs, gen-R, aidl">
     //javac标签用于编译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}" />
             //表示依赖库的路径 内嵌在<javac>、<java>中
            <classpath>
                <fileset dir="${external-libs}" includes="*.jar" />
            </classpath>
        </javac>
    </target>

compile执行的是javac命令,编码格式指定为utf-8,target指定生成的class文件与该版本的虚拟机兼容,保证在该版本的虚拟机上正常运行。
debug表示是否产生调试信息,默认为false。extdirs为扩展文件的路径,destdir指定了存放编译后的class文件的文件夹路径。bootclasspath指定了编译过程中需要导入的class文件。fork指定是否再外部启用一个新的JDK编译器来执行编译,如果为false,则javac命令和ant将在同一个进程中执行,并且javac命令被分配的内存只有64MB,可能会导致java.lang.OutOfMemoryError(OOM)错误,如果fork为true,则另起一个进程来执行javac命令,分配的内存大小将由memoryMaximumSize来指定。src指定了java源文件的路径,classpath指定了依赖的第三方jar包路径。

4、obfuscate

    <target name="obfuscate" depends="compile">
         //jar标签用来生成jar文件,basedir表示需要打包城jar文件的原文件目录, destfile表示生成的jar文件
        <jar basedir="${outdir-classes}" destfile="${outdir}/temp.jar"/>
         //java标签用来执行编译生成的class文件  fork表示再一个新的虚拟机中运行该类  failonerror表示当出现错误时是否自动停止
        <java jar="${proguard.home}/proguard.jar" fork="true" failonerror="true">
             //arg标签用来指定参数 value是命令行参数
            <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混淆先是执行了jar命令,将bin目录下的class文件打包成temp.jar。然后执行了proguard命令来压缩、优化和混淆操作。
-injars {class_path}指定要处理的应用程序jar和目录,即temp.jar
-outjars {class_path}指定处理完后要输出的jar和目录,即obfuscate.jar
-libraryjars {classpath}指定要处理的应用程序jar和目录所需要的程序库文件,即其他依赖的第三方jar包
混淆配置文件为proguard.config。混淆之后删除生成的临时文件,并解压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文件转换成classes.dex文件,即对上一步在bin/classes目录中生成的优化过的class文件以及依赖的第三方jar包进行dex操作,最后在bin目录下生成classes.dex文件。Parallel用于指定将多个task并行执行。

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中执行了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工具根据classes.dex文件和resources.ap_生成未混淆的apk包
apkbuilder <输出apk文件路径> -z <资源文件路径> -f <dex文件路径> -rf <源码目录> -rj <第三方jar包目录> -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 后面接的参数依次是 签名后的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包进行字节对齐,好处是能够减少应用程序的RAM内存资源消耗
-v 表示输出详细信息
-f 表示如果输出文件已存在 则直接覆盖
4 表示对齐为4个字节

10、release

    <target name="release" depends="zipalign">  
        <!-- 删除未签名apk -->  
        <delete file="${out-unsigned-package-ospath}"/>  
        ......  
    </target>

至此打一个完整的带签名的可发布的包的流程就结束了。执行ant release命令即可完成打包。

打包成jar

由于jar包中不能包含资源文件,所以要通过jar包提供UI视图供第三方使用,可以通过如下方式实现:

1、使用硬编码来实现布局文件
2、布局中的资源文件需放在assets文件夹中,然后打包到jar中,通过流的方式读取。这种方式将资源文件放在assets目录下和java代码一起打包为jar,其他工程依赖该jar包时,可以只引用jar包,不需要再额外导入资源文件,在该工程编译应用时会将jar包assets目录中的文件与该工程中的assets目录中的文件合并。注意assets目录中的文件名与所导入工程中的文件名称不能重复,否则在编译的时候会报错“Error generating final archive: Found duplicate file for APK”提示有重名文件。
另外,打包到jar中的资源文件必须是编译之后的资源文件,即编译成二进制文件,因为读取资源时是通过流的方式读取的,所以相关的资源文件必须在编译成二进制文件之后再放入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包的构建方式与apk的类似,执行步骤大概为
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混淆先是执行了jar命令,将bin目录下的class文件以及资源文件打包成jar包,然后执行proguard命令来压缩、优化和混淆操作。这里需要注意的是如果该工程还依赖了其他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文件指明打到jar包中的包路径,jar包中所有的class文件需要有明确的包路径,以防被第三方apk集成编译时,这些class文件无法-keep,被编译混淆之后找不到这些类,导致jar包功能异常。

《Android打包—Ant》

rename1.png

而增加了路径指定后,重命名的类就会被打到指定的包路径下,其他地方对这些类的调用也能正常进行。

《Android打包—Ant》

rename2.png

基本上要介绍的就这么多,可能会有理解错误的地方,欢迎一起讨论!

点赞