Android打包—Ant

作者: the_q | 来源:发表于2016-10-16 15:35 被阅读2553次

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

    Android打包

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

    打包流程

    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包功能异常。

    rename1.png

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

    rename2.png

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

    相关文章

      网友评论

      • captainary:还是有用的,了解打包的过程才能更好的配置打包,无论是 ant 还是 gradle 都只是一个工具而已
      • 10ec977eadee:赶紧转成as项目啊,as支持自动转换的啊,转换后稍微改一下就能运行了,还用什么落伍的ant,大家都开轿车,你还在踩自行车,而且你骑的还是一辆破自行车
        望北8261:@张利强 对,都是历史遗留问题
        张利强:大型项目历史负担,并不是说该就改的,公司的所有持续集成都已经用ant搞定了,然后你说Gradle更新就用?
        the_q:@罗伊叶 已转..但是持续集成用的还是ant..准备过段时间再迁过去..

      本文标题:Android打包—Ant

      本文链接:https://www.haomeiwen.com/subject/uidmyttx.html