移动客户端编译打包方式 | Android篇
作者:赵赴畅 2016-11-03 {{allComments.length}} 106421 干货分享背景介绍
移动客户端的编译打包,是客户端CI的开始点。通过jenkins等持续集成平台,结合代码管理工具,搭建自动编译任务,按照约定的条件自动打包,能够节省人力,管理迭代提测版本,节省人力。
移动端编译打包,按照系统不同,可以分成Android编译和iOS编译两类,各自的编译工具和方式也各不相同,下面我们会分章节概述其主要的编译工具和方法。
随着Android系统的不断升级,Android端的主流编译方式,经历了ant、maven、gradle等几种方式,目前google官方推荐的方式是gradle方式。本节将首先概述Android apk编译的具体流程,然后针对几种主流编译方式进行说明。
Android端编译过程
Android端的整个编译过程,会调用很多编译工具,生成大量的中间文件,并最终生成一个apk文件。编译过程为:
1、打包资源文件,生成R.java文件:
使用aapt工具($ANDROID_SDK_HOME/platform-tools/aapt)打包好资源文件,生成R.java文件。
2、处理AIDL文件,生成对应的.java文件:
使用aidl工具,通过源码、aidl、framework.aidl等文件,生成对应的java文件,如果工程中无aidl文件,则该步骤将跳过。
3、编译.java文件,生成.class文件:
使用javac工具,将所有.java文件编译成.class文件
4、将.class文件转换成.dex文件,供Davik虚拟机加载使用
使用dex工具,将.class文件打包成dex文件(混淆操作在该步骤之前)
5、打包未签名的.apk文件
使用apkbuilder工具,将1-4步骤中的产物(以及libs下的库文件),打包成apk文件
6、给未签名的apk文件打签名
使用jarsigner工具,将签名文件(.keystore文件),打入apk文件中
7、apk文件对齐处理
使用zipalign工具,将步骤6中的产物进行优化和对齐。
可以参考如下的流程:
[http://developer.Android.com/sdk/installing/studio-build.html]
图1 Android编译流程图
Ant方式编译Android App
Ant工具,是Apache软件基金会JAKARTA目录下的一个自项目,工具使用纯java语言编写,因此可以跨平台执行。
使用Ant工具进行编译打包,需要一个build.xml文件来进行配置。对于比简单的Android项目,我们可以使用“Android update project –path 工程路径”命令来自动生成build.xml文件。但显然,对于比较复杂的客户端,可以自己编写一个build.xml文件来实现复杂的编译需求。
build.xml简介
节中,我们简单介绍了Android端编译的整个流程,结合这个流程,一个简单的自定义build.xml如下:
<?xml version=”1.0” encoding=”UTF-8” ?>
<project name=”XXX” default=”release”>
<!—定义各种文件和工具的位置-->
<property file=”default.properties” />
<propertyname=”sdk.dir” value=”${ANDROID_SDK_HOME}”>
……
<targetname=”clean”>
</target>
<target name=”dir” depends=”clean”>
<mkdirdir=”${in.resource.absolute.dir}” />
……
</target>
……
解释一下上面build.xml片段中的内容:
1、<project>标签:
是build.xml文件的根标签,每个标签对应一个项目。它可以由多个内在的属性,其主要属性如下:
default表示默认的运行目标,这个属性是必须的。
basedir表示项目的基准目录。
name表示项目名称。
description表示项目的描述。
2、<target>标签
build.xml的重要组成部分,整个build的过程都是由一个个target构成的。target之间通过depends属性建立依赖关系,target的depends需要先于该target执行。target的主要属性如下:
.name表示target名称,这个属性是必须的。
.depends表示依赖的对象,这个属性不适必须的,一个target可以由多个depends。
if表示仅当当前属性设置时才执行。
unless属性和if属性相反,表示当前属性没有设置的时候才执行。
description属性表示当前target的描述。
3、<mkdir>标签
用来创建一个目录。<mkdir dir=”路径名”>,则会新建一个路径名对应的目录。
4、<jar>标签
用来生成一个jar文件,主要属性如下:
destfile表示jar文件名称。
basedir表示被归档的文件名。
includes表示被归档的文件类型。
exclude表示不被归档的文件类型。
5、<javac>标签
用于编译java文件,其属性如下:
.srcdir 表示源程序的src目录。
.destdir表示.class文件的输出目录。
.include表示被编译的文件类型。
excludes表示被排除的文件类型。
.classpath表示所使用的类路径。
.debug表示包含的调试信息。
.optimize表示是否使用优化。
.verbose表示提供详细的输出信息。
.failonerror表示遇到错误的时候自动停止。
6、<exec>标签
用于执行本地环境中的可执行命令,命令参数使用<args value=”参数”>方式来添加,其主要属性如下:
executable表示可执行文件的名称。
fialonerror表示遇到错误的时候停止。
几个重要的配置
在Android项目中,我们可能需要配置混淆,添加libs以外的编译依赖,或者是添加对lib工程的编译依赖,此外,如果项目比较大的话,可能还会遇到oom等问题。下面分别描述各个问题的配置方式。
混淆的配置:
代码混淆主要是使用proguard.jar文件,对编译后的.class文件进行混淆。其具体的配置项如下:
<targetname="obfuscate" depends="compile">
<echo>obfuscate...</echo>
<exec executable="java" failonerror="true">
<arg value="-jar" />
<arg value="${proguard-home}/lib/proguard.jar" />
<arg value="-injars" />
<arg value="${outdir-classes}" />
<arg value="-outjars" />
<arg value="${outdir-obclasses}" />
<arg value="-ignorewarnings" />
<arg value="-libraryjars ${Android-jar}" />
<arg value="-printmapping${target-signed-package-dir}/mapping.txt" />
<arg value="@proguard.cfg" />
</exec>
</target>
这段配置等同于使用 java –jarproguard.jar –injars “.class文件所在路径” –outjars “混淆后的.class文件是路径” -ignorewarnings –libraryars Android.jar –printmapping mapping文件位置@proguard.cfg,这里需要注意的是,混淆后的.class文件要和混淆之前的隔开,并且后面生成混淆包的时候,dex要使用混淆后的.class文件来压缩。
解决OOM问题:
通过ant的执行日志,一般可以确定oom具体是发生在哪个target的执行中,在build.xml中解决这类问题,可以通过如下方式进行:
<target
name="obfuscate"
depends="compile" >
<echo>
obfuscate...
</echo>
<exec
executable="java"
failonerror="true" >
<argvalue="-Xms1024m" />
<argvalue="-Xmx4096m" />
<arg value="-jar" />
<arg value="${proguard-home}/lib/proguard.jar" />
如上文所示,可以通过arg标签添加java执行时的最大内存设置。如果无法判断是哪个target导致的oom,则可以在执行ant之前,通过
export ANT_OPTS=-Xmx2g
export JAVA_OPTS=-Xmx1g
的方式,统一修改jvm的内存配置。
添加libs以外的其他目录的编译依赖:
主要通过fileset标签,在compile过程中添加编译文件依赖,如下所示:
<classpath>
<fileset dir="${external-libs}"includes="*.jar" />
<fileset dir="jar"includes="*.jar"/>
</classpath>
如上文所示,可以通过添加多个fileset,来添加工程的编译依赖。
Gradle方式编译Android App
Gradle是以Groovy语言为基础,面向java应用为主,基于DSL(领域特定语言)语法的自动化构建工具。Gradle编译方式对多工程构建以及局部编译的支持非常出色,使用Gradle编译方式,可以有效的解决方法数超限的问题;同时,Gradle编译方式也是google官方推荐的编译方式,能够和AndroidStudio有效的结合起来。
和Ant类似,gradle编译方式依赖于build.gradle配置文件。本节我们将简单介绍build.gradle的编写方式,以及使用gradle编译方式的一些重要问题。
build.gradle简介
和Ant的配置文件不同,Gradle工程首先包含一个根配置文件,工程内的每一个module内也都有自己的配置文件,这些配置文件都命名为build.gradle。大多数时候你只需要配置module自己的build.gradle就够了。一个简单的build.gradle文件如下所示:
applyplugin: 'com.Android.application'
Android{
compileSdkVersion 19
buildToolsVersion "19.0.0
defaultConfig {
minSdkVersion 8
targetSdkVersion 19
versionCode 1
versionName "1.0"
}
signingConfigs {
release {
storeFilefile(debug.keystore')
storePassword'Android'
keyAlias'Androiddebugkey'
keyPassword 'Android'
}
}
buildTypes{
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-Android.txt'),
'proguard-rules.pro'
}
}
productFlavors {
demo {
applicationId"com.buildsystemexample.app.demo"
versionName "1.0-demo"
}
full {
applicationId"com.buildsystemexample.app.full"
versionName "1.0-full"
}
}
dependencies{
compile project(":lib")
compile 'com.Android.support:appcompat-v7:19.0.1'
compile fileTree(dir: 'libs', include: ['*.jar'])
}
解释配置文件中各项的含义:
1. apply plugin: 'com.Android.application':
配置gradle的构建插件为Android插件,gradle会将适用于Android构建的所有task都添加进来,通过Android{}块进行具体配置。
2. compileSdkVersion,buildToolsVersion:
配置当前编译使用的api版本,以及构建工具的版本,google建议使用大于等于api版本的构建工具。
3. defaultConfig:
配置AndroidManifest中的核心配置项,比如VersionCode,VersionName等等,gradle会自动从AndroidManifest中获取这些信息,注意,如果在build.gradle中配置了这些项,这些项会覆盖AndroidManifest中的内容。
4. signingConfigs:
配置app的签名信息,可以参考示例中的方式填写jarsigner签名的各项参数,示例中只给出了签名的release配置,如果apk需要有多个不同的签名配置方式,可以写多个signingConfig配置类型,方便在各自不同的buildtype中调用。
5. buildTypes:
配置各种包的编译方式,默认包含debug和release两种,其中,debug类型打出来的包,会打上debug签名,并开启debug开关,release包不会打签名,例子里面,在release编译类型中添加了混淆的配置。
6. dependencies:
dependencies标签在Android标签之后,编译依赖等配置会在这个标签下配置。一般来说,编译依赖一般有lib工程依赖,jar包依赖等,对lib工程的依赖,可以通过compile project(“:工程名”)来添加,对本地jar包的依赖,可以通过compile fileset(dir:jar包路径, include:[‘文件类型’])的方式来添加,如果要添加对仓库中jar包的依赖,则可以直接通过compile ‘maven库标记位’来添加,一般格式位[‘group:name:version’]的格式。
需要注意的是,同一个mudole\jar包,不能同时被多个module通过compile方式建立依赖关系,可以使用provided关键字添加,避免出现dexmerge失败的异常。
7. productFlavors:
使用produceFlavors标签,可以针对一个单一工程,构建出不同版本的apk,对于一个flavor类型,默认使用defaultConfig中的配置项进行配置,我们可以重写所有配置项以便生成最终需要的各种apk
root build.gradle的用法
上节中,我们详细介绍了每个module下的build.gradle文件各项配置的含义。实际上,在整个工程的根目录也会有一个build.xml(本节称之为root build.xml),用来配置整个工程,下面我们通过一个例子,描述root build.xml文件的作用。
project下的build.gradle:
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.Android.tools.build:gradle:1.2.3'
}
}
ext {
pCompileSdkVersion = 21
pBuildToolsVersion =“21.1.1”
pMinSdkVersion = 9
pTargetSdkVersion = 19
pSourceCompatibility =JavaVersion.VERSION_1_7
pTargetCompatibility =JavaVersion.VERSION_1_7
}
上述脚本中,buildscript中声明了gradle脚本执行本身需要的资源,其中repositories中声明了构建过程需要依赖的库文件,dependencies中声明了构建过程中使用的各种插件的版本。值得注意的是,如果本地的gradle仓库中没有这些插件,在构建期间,脚本会自动去远端仓库下载缺失的插件。
ext中定义了一些变量的值,当前project中每个module下的build.gradle文件中都可以继承这些属性,并直接使用。
我们还可以在build.gradle中添加自定义的方法,实现一些功能,比如,在build.gradle中可以创建如下类:
class TimingsListener implementsTaskExecutionListener, BuildListener {
private Clockclock
private timings = []
@Override
void beforeExecute(Task task) {
clock = new org.gradle.util.Clock()
}
@Override
void afterExecute(Task task, TaskState taskState) {
def ms = clock.timeInMs
timings.add([ms, task.path])
task.project.logger.warn"${task.path} took ${ms}ms"
}
@Override
void buildFinished(BuildResult result) {
println "Task timings:"
for (timing in timings) {
if (timing[0] >= 50) {
printf "%7sms %s\n", timing
}
}
}
……
}
gradle.addListenernew TimingsListener()
TimingsListener类实现了TaskExecutionListener, BuildListener接口,在每个Task的执行期间,统计各个task的耗时(只统计超过50ms的任务),此外,在整个build结束之后,还会将统计到的所有时间统一打印出来。
写好计时器的类之后,在脚本最后,通过gradle.addListener方法添加计时器,在整个脚本执行期间,将会打印出所有task的执行时间,如下所示:
53ms :Demo:compileReleaseAidl
140ms :Demo:processReleaseManifest
115ms :Demo:processReleaseResources
3379ms :Demo:compileReleaseJava
……
重要的gradle插件及其配置
上节中,简单介绍了gradle的配置文件——build.gradle文件的基本配置方式。在实际的工程中,大家比较关注的一般都是混淆配置、源文件配置、签名配置、multidex配置、覆盖率插件使用等,下面分别介绍如何配置这些功能。
1. 混淆配置:
gradle让混淆配置变得非常简单,开启混淆配置,只需要在某个buildType中添加minifyEnabled true即可,使用默认混淆文件和规则,配置proguardFiles getDefaultProguardFile('proguard-Android.txt'),'proguard-rules.pro' 即可,如果要使用自定义的混淆文件,通过proguardFile '自定义混淆文件' 即可,下面片段中的debug和release两个buildtype分别使用默认和自定义两种混淆配置文件。
……
Android {
…
buildTypes {
release {
minifyEnabledtrue
proguardFilesgetDefaultProguardFile(‘proguard-Android.txt’), ‘proguard-rules.pro’
}
custom {
minifyEnabled true
proguardFile ‘proguard.cfg’
}
}
}
……
2. 源文件配置
一般来说,如果不特殊配置,Android会使用默认配置来寻找src和res的位置,如果制定源文件位置,可以通过sourceSets来配置,一个例子如下:
Android {
sourceSets {
main {
manifest.srcFile'AndroidManifest.xml'
java.srcDirs = ['src']
resources.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
jniLibs.srcDirs = ['libs']
}
}
上述配置中,我们指定了manifest文件位置,java文件位置,aidl资源位置,assets文件位置,jni库位置等,自定义src路径可以参考上面的配置方法进行。
3. 签名配置
在2.1.3.1节的示例中,我们给出了一个signingConfig的基本配置方法,在实际编译过程中,使用signingConfig的配置对app进行签名,可以通过如下方式进行:
Android{
……
buildType{
debug {
……
signingConfig signingConfigs.release
}
}
……
}
4. Multidex配置
当工程代码量比较大的时候,很容易遇到著名的65536问题(dex方法数超限),gradle 提供了解决这个问题的方案,需要在defaultConfig、buildType或者Flavor中配置multiDexEnabled = true即可。另外,我们可能还需要指定每个dex的大小,可以在Android的块中使用如下配置:
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
if(dx.additionalParameters == null) {
dx.additionalParameters= ['--multi-dex', '--set-max-idx-number=50000']
} else {
dx.additionalParameters += '--multi-dex--set-max-idx-number=50000'
}
}
这段配置表明,当执行到dex开头的任务时,我们给dex参数添加上—multi-dex–set-max-idx-number=50000参数,限定每个dex的方法数为50000个。
5. 覆盖率配置:
gradle官方支持jacoco覆盖率的配置,我们只需要在buildType(或者flovrs中)添加testCoverageEnabled= true即可打出jacoco插桩的包,此外需要注意的是,在dependencies中需要添加一些编译依赖,整体配置如下所示:
buildscript {
……
dependencies {
classpath'com.Android.tools.build:gradle:1.2.3'
classpath'com.Android.tools.build:gradle:1.2.3'
classpath'org.jacoco:org.jacoco.agent:0.7.4.201502262128'
classpath'org.jacoco:org.jacoco.ant:0.7.4.201502262128'
classpath'org.jacoco:org.jacoco.core:0.7.4.201502262128'
classpath'org.jacoco:org.jacoco.report:0.7.4.201502262128'
classpath 'org.ow2.asm:asm-all:5.0.1'
classpath 'org.ow2:ow2:1.3'
}
}
Android{
……
buildType{
debug {
……
testCoverageEnabled = true
}
……
}
}
建议在root的build.xml中配置classpath,这些配置将会被各个子module继承,对于多module的工程,可以节省很多配置工作。
接下来就是收集jacoco覆盖率数据了,在代码中调用org.jacoco.agent.rt.RT. getExecutionData(getAgent(), false)方法即可获得覆盖率数据的流输出。
收集jacoco覆盖率数据,可以通过如下的配置来完成,你可以单独写一个build.gradle,也可以添加到module已有的build.gradle的Android block之外。 具体配置如下:
jacocoTestReport{
group = "Reporting"
description = "Generate Jacoco coverage reports after runningtests."
ignoreFailures = true
executionData =fileTree(dir: 'build/jacoco', include:'**/*.ec') reports {
xml{
enabled true
destination "${buildDir}/reports/jacoco/xml/jacoco.xml"
}
csv.enabled false
html {
enabled true
destination"${buildDir}/reports/jacoco/html"
}
}
}
具体执行时,运行 gradle jacocoReport即可。
grandle的命令行使用
通过jenkins等持续继承平台搭建编译任务,一般通过命令行来完成整个编译流程,因此,了解gradle编译的整个过程,以及一些简单的命令行,能够让整个编译任务变得简单高效。
查看当前可执行task,可以在project或者module下面执行gradletasks –all,gradle将会自动打印出所有可执行任务,以及任务的简要说明,如下图所示:
图2-2 当前所有task列表
在编译Android的时候,gradle clean 命令表示清除之前的编译产物,重新编译;gradle build表示编译所有的buildType,gradle assemble 类型表示只编译某一个类型的产物;gradle check表示执行规范检查等。各个命令之间可以混合使用,比如gradle clean Demo:assembleDebug –x lint check,表示清除上次编译产物,针对Demo module下的debug编译类型进行编译,过程中不执行lint扫描和check扫描。
另外,在一些特殊场景中,我们可以指定执行编译的某个中间步骤,比如使用findbugs代码检查的时候,需要.class文件,我们可以通过gradle clean Demo:compileDebugJava,只对Demo这个module下的debug 类型执行java编译,这样可以节省漫长的proguard和dex过程,提高效率,降低编译机器的压力。
更多的gradle 命令,可以参考gradle –help来获取。
如果你看的意犹未尽,如果你想随时随地充实自己,请扫描以下二维码,关注我们的公众账号,可以获取更多技术类干货,还有精彩活动与你分享~