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

大概分为以下几个步骤
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进行字节对齐优化操作
2. Ant打包
Ant是将软件编译、测试、部署等步骤联系在一起自动化构建工具,主要用在java工程的构建中,所以也可以用来进行android打包。
现在android开发工具基本上都用的AS,构建用gradle,而Ant打包是伴随着eclipse的打包方式,所以现在应该使用的已经不多。虽然工具不一样,但是整个构建原理和流程还是一样的。
Ant的默认构建文件为build.xml,输入ant命令后,ant会在当前目录下搜索是否有build.xml,如果有,则执行该文件,也可以自定义构建文件,通过ant -f test.xml即可指定test.xml为构建文件。
2.1 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>
2.2 打包成apk
build脚本中,一般android源工程打包成apk的执行步骤大体如下:
gen-R->aidl->compile->obfuscate->dex->package-res-and-assets->package->jarsigner->zipalign->release
2.2.1 gen-R
<target name="gen-R" depends="dirs">
<exec executable="${aapt}" failonerror="true">
<arg value="package" />
<arg value="-m" /> <!--使生成的包的目录放在-J参数指定的目录-->
<arg value="-J" />
<arg value="${gen-dir}" /> <!--指定生成的R.java的输出目录-->
<arg value="-M" />
<arg value="${manifest-xml}" /> <!--AndroidManifest.xml的路径-->
<arg value="-S" />
<arg value="${resource-dir}" /> <!--res文件夹路径-->
<arg value="-I" />
<arg value="${android-jar}" /> <!--某个版本平台的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
具体参数命令含义见注释
2.2.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>
2.2.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命令。
encoding指定编码格式为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包路径。
2.2.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目录下
2.2.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并行执行。
2.2.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 <输出的包目录+包名>
2.2.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 <本地库目录>
2.2.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}" /> <!--签名后的apk-->
<arg value="${out-unsigned-package-ospath}" /> <!--待签名的apk-->
<arg value="${keystore.key}" /> <!--密钥库别名-->
</exec>
</target>
jarsigner是对上面生成的apk文件进行签名操作
2.2.9 zipalign
<target name="zipalign" depends="jarsigner">
<exec executable="${zipalign}" failonerror="true">
<arg value="-v" /> <!--表示输出详细信息-->
<arg value="-f" /> <!--表示如果输出文件已存在 则直接覆盖-->
<arg value="4" /> <!--表示对齐为4个字节-->
<arg value="${out-signed-package-ospath}" />
<arg value="${zipalign-package-ospath}" />
</exec>
</target>
zipalign target通过zipalign工具对签名后的apk包进行字节对齐,好处是能够减少应用程序的RAM内存资源消耗
2.2.10 release
<target name="release" depends="zipalign">
<!-- 删除未签名apk -->
<delete file="${out-unsigned-package-ospath}"/>
......
</target>
至此打一个完整的带签名的可发布的包的流程就结束了。执行ant release命令即可完成打包。
2.3 打包成jar
由于jar包中不能包含资源文件,所以要通过jar包提供UI视图供第三方使用,可以通过如下方式实现:
- 使用硬编码来实现布局文件
- 布局中的资源文件需放在
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包功能异常。

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

3. gradle打包
3.1 基础知识
3.1.1 gradle设计规则
Gradle是一个框架,定义了自己一些规则,我们需要遵循她的设计规则:
- 在
Gradle中,每一个待编译的工程都叫做一个Project,如:Android Project目录下的各种lib引入库,都是一个Project。 - 在每个
Project在构建时,又包含了一系列Task,比如:Android APK的编译包含:java源代码编译Task、Android资源编译Task、签名Task等等; - 一个
Project有多少个task,由其编译脚本指定的插件决定,什么是插件?插件就是用来定义Task,并执行这些Task的东西;
Gradle负责定义流程和规则,而具体的编译工作则是通过插件的方式来完成,
如:编译Java的有java插件,编译Android lib 的有Android lib 的插件;
总之:Gradle中每一个待编译的工程,都是一个Project,而Project的编译的工作,是由其定义的一个一个Task来定义与执行的;
3.1.2 build.gradle & settings.gradle文件
在Android工程中,每一个Library、每一个App都是单独的Project,同时在每一个Project的根目录下,都有一个build.gradle文件,表示Project的编译脚本;而在工程的根目录,有一个settings.gradle文件,它负责配置子Project的,settings.gradle文件指出该工程包含多少个子Project;

有了这2个文件,在项目的根目录进行编译时,可以把项目中的所有project都编译好;
3.1.3 gradle 相关命令
-
gradle projects查看工程信息; -
gradle tasks查看任务信息; -
gradle task-name执行任务,如:gradle clean,gradle properties
3.1.4 task 的依赖关系
task和task之间可能有关系,如:某task的执行,需要其他task先执行完成,这就是依赖关系;如:assemble task就依赖其他task先执行,assemble 才能执行;
可以指定assemble依赖于自己定义的task,这样,自定义的task会优先执行;
3.1.5 gradle工作流程(生命周期)
- 初始化阶段:对于
多project的build而言,就是执行settings.gradle; - Configuration阶段:解析每个
project中的build.gradle文件,在这2个阶段之间,可加入一些定制化的hook; - 预执行阶段:现在整个
build的project及内部的task关系已确定; - 执行任务阶段;
3.1.6 gradle编程模型
gradle执行的时候,会把脚本转化成Java对象,gradle主要3种对象,并与三种不同的脚本文件对应:
-
Gradle对象:执行gradle xxx,gradle会从默认配置脚本中构造出一个Gradle对象,整个执行过程中,只有这么一个对象,类型是Gradle; -
Project对象:由build.gralde转; -
Settings对象:由settings.gradle转;
Project对象
Project包含若干个Tasks,Project对应具体工程,需要为Project加载所需要的插件,如:为java工程加入Java插件;
- 加载插件 :调用
apply方法, https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html
apply plugin: 'com.android.library'
apply plugin: 'com.android.application'
Groovy支持函数调用时,通过参数名1:参数值1,参数名2:参数值2来传递参数;
// 加载自定义的插件(这里为一个工具文件)
apply from: rootProject.getRootDir().getAbsolutePath() + "/utils.gradle" - 设置属性
gradle可能包含不止一个build.gradle文件,考虑在多个脚本中设置属性:
gradle提供名为extra property的方法,表示额外属性,在第一次定义该属性时需通过ext前缀来标示他是一个额外的属性,后面的存在,就不需要ext前缀了,ext属性支持Project和Gradle对象,意思是为Project和Gradle对象设置ext属性;
ext {
local = 'Hello groovy'
}
task printProperties {
println local // Local extra property
if (project.hasProperty('cmd')) {
println cmd // Command line property
}
}
如果在utils.gradle中定义了一些函数,然后想在其他build.gradle中调用这些函数。那该怎么做呢?
[utils.gradle]
//utils.gradle 中定义了一个获取 AndroidManifests.xml versionName 的函数
def getVersionNameAdvanced(){
// 下面这行代码中的 project 是谁?
def xmlFile = project.file("AndroidManifest.xml")
def rootManifest = new XmlSlurper().parse(xmlFile)
return rootManifest['@android:versionName']
}
//现在,想把这个 API 输出到各个 Project。由于这个 utils.gradle 会被每一个 Project Apply,所以
//我可以把 getVersionNameAdvanced 定义成一个 closure,然后赋值到一个外部属性
// 下面的 ext 是谁的 ext?
ext{ //此段花括号中代码是闭包
//除了 ext.xxx=value 这种定义方法外,还可以使用 ext{}这种书写方法。
//ext{}不是 ext(Closure)对应的函数调用。但是 ext{}中的{}确实是闭包。
getVersionNameAdvanced = this.&getVersionNameAdvanced
}
问题
-
project是谁?
当一个projectapply一个gradle文件时,这个gradle文件会转化成一个script对象;
script中有一个delegate对象,这个delegate,默认加载(即调用apply)它的project对象;
在apply函数中,除了from参数,还有个to参数,通过to参数,可改变delegate对象为其他;
delegate就是当在script中,操作一些不是script自己定义的变量,或者函数时,gradle会到script的delegate对象中去找,看有没有定义这些变量or函数;
==》这样project就是加载utils.gradle的Project; -
ext是谁的ext?
==》project对应的ext了;此处为Project添加了一些closure。那么,在Project中
就可以调用getVersionNameAdvanced函数了
在Java和Groovy中:可能会把常用的函数放到一个辅助类中,通过import他们,并调用;
但在Gradle中,更正规的方式在 xxx.gradle中定义插件,然后通过Task的方式来完成工作;
3.1.7 Task介绍
task是Gradle中的一种数据类型,表示一些要执行的工作,不同的插件可添加不同task,每一个task需要和一个project关联;
Task 的 API 文档位于 https://docs.gradle.org/current/dsl/org.gradle.api.Task.html
[build.gradle]
// Task 是和Project关联的,所以,需要利用Project的task函数来创建一个Task
task myTask // 新建task名字
task myTask {} // 闭包
task myType << { task action } // << 符号是 doLast缩写
task myTask(type:SomeType)
task myTask(type:SomeType) { }
上面都用到了Project的一个函数,task,注意:
- 一个
Task包含若干action,所以Task有doFirst和doLast二个函数,用于添加需要最先执行的Action和最后需要执行的Action,action是一个闭包; -
Task创建的时候可指定Type,通过type:名字表达,就是告诉gradle,这个新建的Task对象会从哪个基类Task派生,如:Copy是Gradle中的一个类,当task myTask(type:Copy)的时候,创建的Task是一个Copy Task; - 当使用
task myTask {XXX}的时候,花括号是一个闭包,这会导致gradle在创建此task之后,返回给用户之前,会先执行了 闭包内容; - 当使用
task myTask << {XXX}的时候,创建task对象,并把closure作为一个action加到此task的action队列中,并且告诉他“最后才执行这个closure”;
3.1.8 Script Block
gradle文件中包含一些 Script Block,她的作用是让我们来配置相关信息的,不同的SB有不同的配置;
如:
buildscript { // 这是一个Script Block
repositories {
jcenter()
}

每个SB后面都需要跟一个花括号,闭包;
https://docs.gradle.org/current/javadoc/ ,可输入SB名字,进行查找;
解释几个SB:
-
subprojects:它会遍历 工程 中的 每个子Project,在其closure中,默认参数是子project对应的Project对象,由于其他SB都在subprojects中,所以相当于对每个Project都配置了一些信息; -
buildscript:它的closure是在一个类型为ScriptHandler的对象上执行的。主意用来所依赖的classpath等信息。通过查看ScriptHandler API可知,在buildscript SB中,你可以调用ScriptHandler提供的repositories(Closure )、dependencies(Closure)函数。这也是为什么repositories和dependencies
两个SB为什么要放在buildscript的花括号中的原因。这就是所谓的行话,得知道规矩。不知道
规矩你就乱了。记不住规矩,又不知道查 SDK,那么就彻底抓瞎,只能到网上到处找答案了!
3.2 相关task
在AS新建一个Android工程的默认task如下(android gradle plugin:3.0.0)
:app:preBuild UP-TO-DATE
:app:preDebugBuild UP-TO-DATE
:app:compileDebugAidl UP-TO-DATE
:app:compileDebugRenderscript UP-TO-DATE
:app:checkDebugManifest UP-TO-DATE
:app:generateDebugBuildConfig UP-TO-DATE
:app:prepareLintJar UP-TO-DATE
:app:generateDebugResValues UP-TO-DATE
:app:generateDebugResources UP-TO-DATE
:app:mergeDebugResources UP-TO-DATE
:app:createDebugCompatibleScreenManifests UP-TO-DATE
:app:processDebugManifest
:app:splitsDiscoveryTaskDebug UP-TO-DATE
:app:processDebugResources
:app:generateDebugSources
:app:javaPreCompileDebug
:app:compileDebugJavaWithJavac
:app:compileDebugNdk NO-SOURCE
:app:compileDebugSources
:app:mergeDebugShaders
:app:compileDebugShaders
:app:generateDebugAssets
:app:mergeDebugAssets
:app:transformClassesWithDexBuilderForDebug
:app:transformDexArchiveWithExternalLibsDexMergerForDebug
:app:transformDexArchiveWithDexMergerForDebug
:app:mergeDebugJniLibFolders
:app:transformNativeLibsWithMergeJniLibsForDebug
:app:processDebugJavaRes NO-SOURCE
:app:transformResourcesWithMergeJavaResForDebug
:app:validateSigningDebug
:app:packageDebug
:app:assembleDebug
一般情况下,这些task就会按照打包流程图上的步骤进行打包,无需做修改(有时间要研习相关源码,推荐这个系列博文https://www.jianshu.com/p/e73510605c56
),一般开发者更多需要关注的,是AS开发环境下,主工程下打包脚本build.gradle的相关配置和修改。
3.3 build.gradle
关于Gradle下android{}的配置字段说明如下:
defaultConfig{} //默认配置,是ProductFlavor类型。它共享给其他ProductFlavor使用
sourceSets{ } //源文件目录设置,是AndroidSourceSet类型。
buildTypes{ } //BuildType类型
signingConfigs{ } //签名配置,SigningConfig类型
productFlavors{ } //产品风格配置,ProductFlavor类型
testOptions{ } //测试配置,TestOptions类型
aaptOptions{ } //aapt配置,AaptOptions类型
lintOptions{ } //lint配置,LintOptions类型
dexOptions{ } //dex配置,DexOptions类型
compileOptions{ } //编译配置,CompileOptions类型
packagingOptions{ } //PackagingOptions类型
jacoco{ } //JacocoExtension类型。 用于设定 jacoco版本
splits{ } //Splits类型
Android的build.gradle(实际构建开始的地方)
// 定义全局的相关属性,使用 jcenter作为仓库
buildscript {
repositories {
jcenter()
}
// 定义构建过程
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0'
}
}
// 用来定义各个模块的默认属性,在所有模块中的可见
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
3.4 多渠道打包之productFlavors
// 多渠道打包
productFlavors {
// 个性化定制
xiaomi {
applicationId "groovy.better.com.groovytest.xiaomi"
minSdkVersion 11
manifestPlaceholders = [
WX_KEY : "*************",
]
}
huawei {
applicationId "groovy.better.com.groovytest.huawei"
minSdkVersion 14
manifestPlaceholders = [
WX_KEY : "%%%%%%%%%%%%",
]
}
baidu {
applicationId "groovy.better.com.groovytest.baidu"
minSdkVersion 16
manifestPlaceholders = [
WX_KEY : "&&&&&&&&&&&&",
]
}
}
// apk名称修改
applicationVariants.all { variant ->
if (variant.buildType.name.equals('release')) {
variant.outputs.each { output ->
def appName = 'demo'
def oldFile = output.outputFile
def buildName
def releaseApkName
variant.productFlavors.each { product ->
buildName = product.name
}
releaseApkName = appName + getVersionByMainfest() + '_' + buildName + '_' + getNowTime() + '.apk'
output.outputFile = new File(oldFile.parent, releaseApkName)
}
}
}
使用技巧

app2和main目录结构是一样的,那是不是意味着,app2和main是平级的?切换到app2分支的时候就会走app2的java代码和res的资源呢?
不!app2和main并不是平级,相反的,app2是main的附属
main是公共代码资源库,app2的所有缺失的java和res资源都会去main下找公共资源,所以我们切换到app2渠道下,可以直接运行app,除了applicationId不同之外,app不会有任何变化。
main是公共代码资源库,这句话的意思是说,无论有多少个渠道,main下的java和res都是最基本的存在,类似于所有其他的渠道都在引用main这个库的意思。这和我们开发引用一个库是类似的原理,只是完全反转过来,我们开发一个库,是app来引用这个库,而多渠道下都在一个app下,其他渠道以类似引用的方式来使用main下的java和res。切换到app2分支的时候就会走app2的java代码和res的资源呢?
如果理解了第一个问题,那第二个问题也就比较好理解了。app2作为main的附属,切换到app2分支后,会将app2下的java代码和res合并到main下编译运行。随之又会有一个新的问题,java代码和res资源是如何合并的?
java代码的合并。
只要不同的module没有路径+名称完全相同的类即不会报错-
drawable的合并。
只需要命名一致,并对比main项目中图片放置的位置放到app2项目的对应位置即可完成替换。
图片替换要注意两点:
一.目前和命名一致;二.main下有几套图片,app2下就要有几套图片,可以多但不能少。
app2下新增一个main没有的图片,代码中去引用了的话,切换到main渠道下会报错找不到该资源文件,这个问题稍后讲解。 layout合并
layout布局文件跟drawable图片合并一样,也是要求命名一致,但涉及到布局文件中的id的处理,要求比较严格,如果相同的功能只是布局位置,字体大小,色值等调整,那么id必须一致,因为同一个java文件引用不同渠道下的layout布局,如果id不同,切换渠道肯定报错;如果app2中新增一个id,而又在java代码中引用了,那么切换到main渠道下也会报错,因为main渠道下的layout没有这个id,这块的处理稍后再说。-
string,color合并
string和color等类似独一份的资源文件合并又有所不同,简单的说就是,相同命名的string和color会被替换,不同命名的会新增。如图:
相同的app_name就会被替换成MyApp2的名称。
不同命名的会新增,也会有layout布局id类似的问题,如果main下string.xml没有相同命名的资源,同时又在java代码中引用了,一样会出问题,这块稍后一起讲解。 java代码的差异化处理
java代码的差异化处理是重中之重,再怎么相似的俩app,总有些个别地方逻辑不同的地方。我这边提供两种处理差异化代码的方式:
一. main下公共代码库差异化处理
两个app共用一套代码的前提下,在main下进行代码区分,这种情况需要做渠道区分,BuildConfig类中已经有渠道区分常量:BuildConfig.FLAVOR
那么在代码中就可以判断:
if ("main".equals(BuildConfig.FLAVOR)) {
// 处理main下逻辑
} else if ("app2".equals(BuildConfig.FLAVOR)) {
// 处理app2下逻辑
}
建议大家写一个工具类,不然每个差异化的地方都要这么判断很蠢的
public class FlavorUtils {
public static boolean isMain() {
return "main".equals(BuildConfig.FLAVOR);
}
public static boolean isApp2() {
return "app2".equals(BuildConfig.FLAVOR);
}
}
差异化不多的情况下,这种写法是最方便的,也是最效率的,唯一的坏处就是在于要多判断。
注:这种差异化处理是将main和app2分别当做一个独立的渠道,但因为main还是公共代码库,所以切换到app2下进行编译,会同时编译app2和main下的java代码,这种情况下main代码中引用app2的类是没有问题的。
但如果切换到main渠道下去编译,你会发现编译后提示找不到app2下类的错误,那是因为切换到main渠道下,只会编译main下java代码,不会编译app2的java代码,自然就找不到对应app2下的类了。
解决方式也有:
sourceSets {
main {
jniLibs.srcDirs = ['libs']
java.srcDirs = ['src/main/java', 'src/app2/java']
}
app2 {
java.srcDirs = ['src/app2/java']
}
}
配置main下的java.srcDirs编译目录,切换到main渠道后同时编译main/java和app2/java,就可以了。
二. 分离公共代码库,每个app创建对应的渠道
在前文中,我们都是把main当做一个单独的app渠道,app2作为第二个渠道,现在的方式就是,将main的渠道单独分离出来,创建app1渠道。将app1和app2差异的类从main下剪切出来同时复制到对应的app1和app2下,单独去开发对应的渠道代码,互相不干扰。
这样,main的功能性就只是公共代码资源库的职能,不能再作为一个单独的渠道去编译运行了。但同时,build也需要修改下:
sourceSets {
main {
jniLibs.srcDirs = ['libs']
java.srcDirs = ['src/main/java']
}
app1 {
java.srcDirs = ['src/app1/java']
}
app2 {
java.srcDirs = ['src/app2/java']
}
}
各自编译各自的java代码。
app1和app2下相同的类也不会报错:

原因很简单,因为编译了
app1渠道,没有编译app2渠道,自然不会出现类冲突的问题。注:这种
java代码的差异化处理需要注意,main只能引用app1和app2下路径和类名一致的java类,互相切换渠道才不会报错,如果main只引用了app1中有的类,而app2下没有这个类,那切换到app2渠道下肯定要报错了。


