源码网商城,靠谱的源码在线交易网站 我的订单 购物车 帮助

源码网商城

Android笔记之:App自动化之使用Ant编译项目多渠道打包的使用详解

  • 时间:2022-02-22 13:18 编辑: 来源: 阅读:
  • 扫一扫,手机访问
摘要:Android笔记之:App自动化之使用Ant编译项目多渠道打包的使用详解
随着工程越来越复杂,项目越来越多,以及平台的迁移(我最近就迁了2回),还有各大市场的发布,自动化编译android项目的需求越来越强烈,后面如果考虑做持续集成的话,会更加强烈。     经过不断的尝试,在ubuntu环境下,以花界为例,我将一步一步演示如何使用命令行,使用ant编译android项目,打包多渠道APK。     要点:     (1). 编译android的命令使用     (2). ant基本应用     (3). 多项目如何编译(包含android library)     (4). 如何多渠道打包     ps:我将以最原始的方式来实现,而不是使用android自带的ant编译方式,并尽量详细解释,这样有益于我们彻底搞懂android打包的基本原理。 [b]1. Android编译打包的整体过程[/b]     使用ant,ant的参考文档:http://ant.apache.org/manual/index.html     首先,假设现在已经有这样的一个项目(多工程的,简单的单工程就更简单了): 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)问题,建立一个批处理支持一键生成所有版本     大概流程即是如此。 [b]2. 建立各个工程的ant脚本文件build.xml(位置:floworld/build.xml)[/b]     因为需要创建一些基本的文件目录和清理上次生成的文件,所以我们简单的定义一下几个目标吧:init,main,clean。     代码模板如下:
[u]复制代码[/u] 代码如下:
<project default="main" basedir=".">      <!-- 初始化:创建目录,清理目录等 -->      <target name="init">            <echo>start initing ... </echo>             <!-- ... ... -->            <echo>finish initing. </echo>      </target>      <!-- 打包过程,默认值 -->      <target name="main" depends="init">      </target>      <!-- 清理不需要的生成文件等-->      <target name="clean">      </target> </project>
[b]3. 初始化 [/b]    在正式打包之前,有必要说明一下可能需要用到的初始化变量和操作。     前面已经讲述了打包的大概流程,现在,第一, 打包需要你使用哪个版本android.jar; 第二, 生成的R文件放到gen目录下; 第三, 生成的classes文件放到bin目录下; 第四, 生成的打包文件放到out目录下; 第五, 生成的各市场版本放到build目录下。目录完全可以自定义。     所以,如下的初始化必须先要做好,不然后面会提示找不到目录:
[u]复制代码[/u] 代码如下:
<project default="main" basedir=".">     <!-- 这个是android.jar路径,具体情况具体配置 -->     <property name="android-jar" value="/usr/lib/android-sdk/platforms/android-10/android.jar" />     <!-- 用于生成多渠道版本的APK文件名,提供了默认值,后面会讲到 -->     <property name="apk-name" value="product" />     <property name="apk-version" value="latest" />     <property name="apk-market" value="dev" />     <target name="init">         <echo>start initing ... </echo>         <mkdir dir="out" />         <delete>             <fileset dir="out"></fileset>         </delete>         <mkdir dir="gen" />         <delete>             <fileset dir="gen"></fileset>         </delete>         <mkdir dir="bin/classes" />         <delete>             <fileset dir="bin/classes"></fileset>         </delete>         <!-- ${apk-version}表示版本,后面会详细讲到 -->         <mkdir dir="build/${apk-version}" />         <echo>finish initing. </echo>     </target>      ... ... </project>
[b]4. 生成R.java [/b]    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的生成脚本:
[u]复制代码[/u] 代码如下:
<echo>generating R.java for project to dir gen (using aapt) ... </echo> <exec executable="aapt">     <arg value="package" /> <!-- package表示打包-->     <arg value="-m" /> <!--m,J,gen表示创建包名的目录和R.java到gen目录下 -->     <arg value="-J" />     <arg value="gen" />     <arg value="-M" /> <!-- M指定AndroidManifest.xml文件-->     <arg value="AndroidManifest.xml" />     <arg value="-S" /> <!-- S指定res目录,生成对应的ID,可多个-->     <arg value="res" />     <arg value="-S" />     <arg value="../baseworld/res" /><!-- 注意点:同时需要调用Library的res-->     <arg value="-I" /> <!-- I指定android包的位置-->     <arg value="${android-jar}" />     <arg value="--auto-add-overlay" /> <!-- 这个重要,覆盖资源,不然报错--> </exec>
注意res和../baseworld/res两个顺序不能搞反,写在前面具有高优先级,我们当然优先使用主应用的资源了,这样就能正确覆盖库应用的资源,实现重写。     库应用的R.java的生成脚本差不多,区别是指定库应用的AndroidManifest.xml,以用于生成的是不同的包和目录。     另外,aapt的使用中特别说明了,为了库应用的资源更好的可重用,库应用生成的R.java字段不需要修饰为final,加上参数--non-constant-id即可。
[u]复制代码[/u] 代码如下:
<echo>generating R.java for library to dir gen (using aapt) ... </echo> <exec executable="aapt">     <arg value="package" />     <arg value="-m" />     <arg value="--non-constant-id" /> <!-- 加了这个参数-->     <arg value="--auto-add-overlay" />     <arg value="-J" />     <arg value="gen" />     <arg value="-M" />     <arg value="../baseworld/AndroidManifest.xml" /> <!-- 库应用的manifest-->     <arg value="-S" />     <arg value="res" />     <arg value="-S" />     <arg value="../baseworld/res" />     <arg value="-I" />     <arg value="${android-jar}" /> </exec>
这样的话就可以生成2个正确的R.java文件了(如果你引用了两个库,则需要生成3个R.java,以此类推)。     结果如下:
[u]复制代码[/u] 代码如下:
gen └── com     └── tianxia         ├── app         │   └── floworld         │       └── R.java         └── lib             └── baseworld                 └── R.java
[b]5. 编译java文件为class文件 [/b]    使用javac命令把src目录,baseworld/src目录,gen/*/R.java这些java编译成class文件:     命令原型是:
[u]复制代码[/u] 代码如下:
//示例 javac -bootclasspath <android.jar> -s <src> -s <src> -s <gen> -d bin/classes *.jar
转化成ant脚本为:
[u]复制代码[/u] 代码如下:
<!-- 第三方jar包需要引用,用于辅助编译 --> <path id="project.libs">     <fileset dir="libs">         <include name="*.jar" />     </fileset> </path> <echo>compiling java files to class files (include R.java, library and the third-party jars) ... </echo> <!-- 生成的class文件全部保存到bin/classes目录下 --> <javac destdir="bin/classes" bootclasspath="${android-jar}">     <src path="../baseworld/src" />     <src path="src" />     <src path="gen" />     <classpath refid="project.libs" /> </javac>
[b]6. 打包class文件为classes.dex [/b]    这步简单,用dx命令把上步生成的classes和第三方jar包打包成一个classes.dex。     命令原型是:
[u]复制代码[/u] 代码如下:
//示例 //后面可以接任意个第三方jar路径 dx --dex --output=out/classes.dex bin/classes libs/1.jar libs/2.jar
转化成ant脚本为:
[u]复制代码[/u] 代码如下:
<echo>packaging class files (include the third-party jars) to calsses.dex ... </echo> <exec executable="dx">     <arg value="--dex" />     <arg value="--output=out/classes.dex" /><!-- 输出 -->     <arg value="bin/classes" /> <!-- classes文件位置 -->     <arg value="libs" /> <!-- 把libs下所有jar打包 --> </exec>
[b]7. 打包res,assets为资源压缩包(暂且命名为res.zip) [/b]    还是使用aapt命令,如生成R.java最大的不同是参数-F,意思是生成res.zip文件。     命令原型和ant脚本差不多:
[u]复制代码[/u] 代码如下:
<echo>packaging resource (include res, assets, AndroidManifest.xml, etc.) to res.zip ... </echo> <exec executable="aapt">     <arg value="package" />     <arg value="-f" /> <!-- 资源覆盖重写 -->     <arg value="-M" />     <arg value="AndroidManifest.xml" />     <arg value="-S" />     <arg value="res" />     <arg value="-S" />     <arg value="../baseworld/res" />     <arg value="-A" /> <!-- 与R.java不同,需要asset目录也打包 -->     <arg value="assets" />     <arg value="-I" />     <arg value="${android-jar}" />     <arg value="-F" /> <!-- 输出资源压缩包 -->     <arg value="out/res.zip" />     <arg value="--auto-add-overlay" /> </exec>
[b]8. 使用apkbuilder命令组合classes.dex,res.zip和AndroidManifest.xml为未签名的apk [/b]    apkbuilder命令能把class类,资源等文件打包成一个未签名的apk,原型命令和ant脚本类似:
[u]复制代码[/u] 代码如下:
<echo>building unsigned.apk ... </echo> <exec executable="apkbuilder">     <arg value="out/unsigned.apk" /> <!-- 输出 -->     <arg value="-u" /> <!-- u指创建未签名的包-->     <arg value="-z" /> <!-- 资源压缩包 -->     <arg value="out/res.zip" />     <arg value="-f" /> <!-- dex文件 -->     <arg value="out/classes.dex" /> </exec>
  这个命令比较简单。 [b]9. 签名未签名的apk [/b]    使用jarsigner命令对上步中产生的apk签名。这是个传统的java命令,非android专用。     原型命令和ant脚本差不多:
[u]复制代码[/u] 代码如下:
<!-- 生成apk文件到build目录下 --> <!-- 其中${apk-version/name/market}用户多渠道打包,后面会讲到 --> <echo>signing the unsigned apk to final product apk ... </echo> <exec executable="jarsigner">     <arg value="-keystore" />     <arg value="../xxx.keystore" />     <arg value="-storepass" />     <arg value="xxx" />  <-- 验证密钥完整性的口令,创建时建立的 -->     <arg value="-keypass" />     <arg value="xxx" /> <-- 专用密钥的口令,就是key密码 -->     <arg value="-signedjar" />     <arg value="build/${apk-version}/${apk-name}_${apk-version}_${apk-market}.apk" /> <!-- 输出 -->     <arg value="out/unsigned.apk" /> <!-- 未签名的apk -->     <arg value="xxx" /> <!-- 别名,创建时建立的 --> </exec>
至此,完整具有打包功能了,最后的build.xml为:
[u]复制代码[/u] 代码如下:
<project default="main" basedir=".">     <property name="apk-name" value="product" />     <property name="apk-version" value="latest" />     <property name="apk-market" value="dev" />     <property name="android-jar" value="/usr/lib/android-sdk/platforms/android-10/android.jar" />     <target name="init">         <echo>start initing ... </echo>         <mkdir dir="out" />         <delete>             <fileset dir="out"></fileset>         </delete>         <mkdir dir="gen" />         <delete>             <fileset dir="gen"></fileset>         </delete>         <mkdir dir="bin/classes" />         <delete>             <fileset dir="bin/classes"></fileset>         </delete>         <mkdir dir="build/${apk-version}" />         <echo>finish initing. </echo>     </target>     <target name="main" depends="init">         <echo>generating R.java for project to dir gen (using aapt) ... </echo>         <exec executable="aapt">             <arg value="package" />             <arg value="-m" />             <arg value="-J" />             <arg value="gen" />             <arg value="-M" />             <arg value="AndroidManifest.xml" />             <arg value="-S" />             <arg value="res" />             <arg value="-S" />             <arg value="../baseworld/res" />             <arg value="-I" />             <arg value="${android-jar}" />             <arg value="--auto-add-overlay" />         </exec>         <echo>generating R.java for library to dir gen (using aapt) ... </echo>         <exec executable="aapt">             <arg value="package" />             <arg value="-m" />             <arg value="--non-constant-id" />             <arg value="--auto-add-overlay" />             <arg value="-J" />             <arg value="gen" />             <arg value="-M" />             <arg value="../baseworld/AndroidManifest.xml" />             <arg value="-S" />             <arg value="res" />             <arg value="-S" />             <arg value="../baseworld/res" />             <arg value="-I" />             <arg value="${android-jar}" />         </exec>         <path id="project.libs">             <fileset dir="libs">                 <include name="*.jar" />             </fileset>         </path>         <echo>compiling java files to class files (include R.java, library and the third-party jars) ... </echo>         <javac destdir="bin/classes" bootclasspath="${android-jar}">             <src path="../baseworld/src" />             <src path="src" />             <src path="gen" />             <classpath refid="project.libs" />         </javac>         <echo>packaging class files (include the third-party jars) to calsses.dex ... </echo>         <exec executable="dx">             <arg value="--dex" />             <arg value="--output=out/classes.dex" />             <arg value="bin/classes" />             <arg value="libs" />         </exec>         <echo>packaging resource (include res, assets, AndroidManifest.xml, etc.) to res.zip ... </echo>         <exec executable="aapt">             <arg value="package" />             <arg value="-f" />             <arg value="-M" />             <arg value="AndroidManifest.xml" />             <arg value="-S" />             <arg value="res" />             <arg value="-S" />             <arg value="../baseworld/res" />             <arg value="-A" />             <arg value="assets" />             <arg value="-I" />             <arg value="${android-jar}" />             <arg value="-F" />             <arg value="out/res.zip" />             <arg value="--auto-add-overlay" />         </exec>         <echo>building unsigned.apk ... </echo>         <exec executable="apkbuilder">             <arg value="out/unsigned.apk" />             <arg value="-u" />             <arg value="-z" />             <arg value="out/res.zip" />             <arg value="-f" />             <arg value="out/classes.dex" />         </exec>         <echo>signing the unsigned apk to final product apk ... </echo>         <exec executable="jarsigner">             <arg value="-keystore" />             <arg value="xxx.keystore" />             <arg value="-storepass" />             <arg value="xxxx" />             <arg value="-keypass" />             <arg value="xxx" />             <arg value="-signedjar" />             <arg value="build/${apk-version}/${apk-name}_${apk-version}_${apk-market}.apk" />             <arg value="out/unsigned.apk" />             <arg value="xxx" />         </exec>         <echo>done.</echo>     </target> </project>
在工程目录下运行ant:
[u]复制代码[/u] 代码如下:
$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,可以导入到手机上运行。 [b]10. 多渠道打包 [/b]    目前主流的多渠道打包方法是在AndroidManifest.xml中的Application下添加一个渠道元数据节点。     比如,我使用的是友盟统计,它配置AndroidManifest.XML添加下面代码:
[u]复制代码[/u] 代码如下:
<application ……>     <meta-data android:value="Channel ID" android:name="UMENG_CHANNEL"/>     <activity ……/> </application>
通过修改不同的Channel ID值,标识不同的渠道,有米广告提供了一个不错的渠道列表:http://wiki.youmi.net/PromotionChannelIDs.     实现多渠道自动打包,就是实现自动化过程中替换Channel ID,然后编译打包。     这个替换需要用到正则表达式实现。     ant中提供的replace方法,功能太简单了,replaceregrex又需要添加另外的jar包,而且我们后面我们实现ant传参需要写另外的linux shell脚本,所以我干脆使用我熟悉的sed-i命令来实现替换。     替换命令:
[u]复制代码[/u] 代码如下:
#-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了,其他属性类似。     所以,在工程下面这条命令会生成:
[u]复制代码[/u] 代码如下:
#结合前面讲打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):
[u]复制代码[/u] 代码如下:
#定义市场列表,以空格分割 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:
[u]复制代码[/u] 代码如下:
# ./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文件:
[u]复制代码[/u] 代码如下:
build ├── 1.0 │   ├── floworld_1.0_appchina.apk │   ├── floworld_1.0_dev.apk │   └── floworld_1.0_gfan.apk └── README.md
 成功生成! [b]11. 工程脚本的执行目录问题 [/b]    上面的脚本执行之后的确很cool,但是有一个问题,我必须在build.sh目录下执行,才能正确编译,这个和build.xml中定义的相对路径有关。     我们必须在任何目录执行工程目录下的build.sh都不能出错,改进build.sh为如下:
[u]复制代码[/u] 代码如下:
#!/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,不会出现路径不对,找不到文件的错误了。 [b]12. 建立整个项目的自动化编译脚本(位置:world/build.sh) [/b]   单个工程的自动化打包没有问题了,但是一个项目下有N个工程,他们往往需要同步发布(或者daily build也需要同步编译),所以有必要建立一个项目级别的编译脚本:    build.sh(项目根目录下,位置:/world/build.sh)    最简单的傻瓜式的做法就是,遍历项目下的工程目录,如果包含工程编译的build.sh,则编译该工程.    shell脚本如下:
[u]复制代码[/u] 代码如下:
#!/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
执行该脚本:
[u]复制代码[/u] 代码如下:
# ./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 ...
 成功自动寻找,并编译打包。 [b]13. 其他细节 [/b]    为了尽量详细,我一再解说,但是还有一些细节未包括其中,如编译后清理clean目标,apk对齐优化,java代码混淆等,请参考其他资料,在此省略。     另外,我反编译生成的apk,查看Androidmanifest.xml均正确对应,验证通过。 [b]14. 小结 [/b]    自动化编译多渠道打包这个功能是Android产品发布的重要环节,能大大节省人力和出错的概率。
  • 全部评论(0)
联系客服
客服电话:
400-000-3129
微信版

扫一扫进微信版
返回顶部