移动客户端编译打包方式 | Android篇


背景介绍

移动客户端的编译打包,是客户端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来获取。


如果你看的意犹未尽,如果你想随时随地充实自己,请扫描以下二维码,关注我们的公众账号,可以获取更多技术类干货,还有精彩活动与你分享~

大咖招募
欢迎App研发/测试方面的大牛来投稿,开设专栏。我们提供丰厚的稿酬,预约个人专访,帮助建立个人技术品牌!
立即投稿

我要评论

字数不能超过140字,谢谢!
提交

评论({{allComments.length}})

{{comment.create_time.substr(0,16)}}

显示所有评论
复制成功!