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脚本
//ant默认构建文件即build.xml文件中需定义一个唯一的项目(Project标签),Project下可以定义若干个目标(target标签)
//project名称为MyApp, default表示默认的运行target,为必须属性,如果ant命令没有指定target时,则运行default属性中的target
//如MyApp工程目录下直接输入ant命令,则会直接打debug包。 basedir表示项目的基准目录
//property标签用来设置属性值,可以通过file标签来指定要加载的属性文件的路径,加载后属性文件中的指定的属性可以直接引用。
//为了方便配置,可以将环境变量声明在build.properties中,并通过file引入到build.xml中
//property中的name表示属性的名称 value表示属性值 在其他地方可以通过${属性名}进行引用, 类似于定义一个变量
//tartget,表示一个构建目标,也可以看成一个构建步骤, 一次构建过程中会执行一个或者多个构建步骤。
//target中的depends属性表示target之间的依赖关系,一个target可以依赖其他的target标签,depends属性也指定了target的执行顺序。
//ant会按照depends属性中target的顺序来依次执行每个target。所以本文中target的执行顺序为 targetone -> targettwo -> debug
//创建目录
//task是target中的子元素,一个target中可以有多个task,类似于target的子任务,常用的task有echo、mkdir、delete、javac、java等等
//删除目录
//输出日志信息
debug target perform...
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
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
此步骤主要是生成aidl文件
对应的java文件
使用apply
标签可以进行批量运行task
,此步骤即用build-tools
下的aidl工具
对src文件夹
下的所有aidl文件
进行批量转换成java文件
。
是作为
的一个子类而被实现,所以
任务的所有属性,都可以用于
2.2.3 compile
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
//jar标签用来生成jar文件,basedir表示需要打包城jar文件的原文件目录, destfile表示生成的jar文件
//java标签用来执行编译生成的class文件 fork表示再一个新的虚拟机中运行该类 failonerror表示当出现错误时是否自动停止
//arg标签用来指定参数 value是命令行参数
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
dex
就是用dx.bat
工具将.class
文件转换成classes.dex
文件,即对上一步在bin/classes
目录中生成的优化过的class文件
以及依赖的第三方jar包
进行dex
操作,最后在bin
目录下生成classes.dex
文件。Parallel
用于指定将多个task
并行执行。
2.2.6 package-res-and-assets
package-res-and-assets
中执行了aapt
命令,来将res
、assets
目录下的资源文件打包到resources.ap_
aapt package -f -M -S -A -I -F <输出的包目录+包名>
2.2.7 package
通过apkbuilder.bat
工具根据classes.dex
文件和resources.ap_
生成未混淆的apk
包
apkbuilder <输出apk文件路径> -z <资源文件路径> -f -rf <源码目录> -rj <第三方jar包目录> -nf <本地库目录>
2.2.8 jarsigner
jarsigner
是对上面生成的apk文件进行签名操作
2.2.9 zipalign
zipalign target通过zipalign工具对签名后的apk包进行字节对齐,好处是能够减少应用程序的RAM内存资源消耗
2.2.10 release
......
至此打一个完整的带签名的可发布的包的流程就结束了。执行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 class files....
...
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
是谁?
当一个project
apply
一个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
渠道下肯定要报错了。