ant、gradle打包相关

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)错误,如果forktrue,则另起一个进程来执行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.jarbin目录下

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命令,来将resassets目录下的资源文件打包到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视图供第三方使用,可以通过如下方式实现:

  1. 使用硬编码来实现布局文件
  2. 布局中的资源文件需放在assets文件夹中,然后打包到jar中,通过流的方式读取。这种方式将资源文件放在assets目录下和java代码一起打包为jar,其他工程依赖该jar包时,可以只引用jar包,不需要再额外导入资源文件,在该工程编译应用时会将jarassets目录中的文件与该工程中的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包功能异常。

image.png

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


image.png

3. gradle打包

3.1 基础知识
3.1.1 gradle设计规则

Gradle是一个框架,定义了自己一些规则,我们需要遵循她的设计规则:

  1. Gradle中,每一个待编译的工程都叫做一个Project,如:Android Project目录下的各种lib引入库,都是一个Project
  2. 在每个Project在构建时,又包含了一系列Task,比如:Android APK的编译包含:java源代码编译Task、Android资源编译Task、签名Task等等;
  3. 一个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 相关命令
  1. gradle projects 查看工程信息;
  2. gradle tasks 查看任务信息;
  3. gradle task-name 执行任务,如:gradle cleangradle properties
3.1.4 task 的依赖关系

tasktask之间可能有关系,如:某task的执行,需要其他task先执行完成,这就是依赖关系;如:assemble task就依赖其他task先执行,assemble 才能执行;
可以指定assemble依赖于自己定义的task,这样,自定义的task会优先执行;

3.1.5 gradle工作流程(生命周期)
  1. 初始化阶段:对于多projectbuild而言,就是执行settings.gradle;
  2. Configuration阶段:解析每个project中的build.gradle文件,在这2个阶段之间,可加入一些定制化的hook;
  3. 预执行阶段:现在整个buildproject及内部的task关系已确定;
  4. 执行任务阶段;
3.1.6 gradle编程模型

gradle执行的时候,会把脚本转化成Java对象,gradle主要3种对象,并与三种不同的脚本文件对应:

  1. Gradle对象:执行gradle xxxgradle会从默认配置脚本中构造出一个Gradle对象,整个执行过程中,只有这么一个对象,类型是Gradle
  2. Project对象:由build.gralde转;
  3. Settings对象:由settings.gradle转;
Project对象

Project包含若干个TasksProject对应具体工程,需要为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属性支持ProjectGradle对象,意思是为ProjectGradle对象设置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
}

问题

  1. project是谁?
    当一个project apply 一个gradle文件时,这个gradle文件会转化成一个script对象;
    script中有一个delegate对象,这个delegate,默认加载(即调用apply)它的project对象;
    apply函数中,除了from参数,还有个to参数,通过to参数,可改变delegate对象为其他;
    delegate就是当在script中,操作一些不是script自己定义的变量,或者函数时,gradle会到scriptdelegate对象中去找,看有没有定义这些变量or函数;
    ==》这样project就是加载utils.gradleProject
  2. ext是谁的ext
    ==》 project对应的ext了;此处为 Project 添加了一些 closure。那么,在 Project
    就可以调用 getVersionNameAdvanced 函数了

JavaGroovy中:可能会把常用的函数放到一个辅助类中,通过import他们,并调用;
但在Gradle中,更正规的方式在 xxx.gradle中定义插件,然后通过Task的方式来完成工作;

3.1.7 Task介绍

taskGradle中的一种数据类型,表示一些要执行的工作,不同的插件可添加不同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,注意:

  1. 一个Task包含若干action,所以 TaskdoFirstdoLast二个函数,用于添加需要最先执行的Action和最后需要执行的Actionaction是一个闭包;
  2. Task创建的时候可指定Type,通过 type:名字表达,就是告诉gradle,这个新建的Task对象会从哪个基类Task派生,如:CopyGradle中的一个类,当 task myTask(type:Copy)的时候,创建的Task是一个Copy Task
  3. 当使用task myTask {XXX}的时候,花括号是一个闭包,这会导致gradle在创建此task之后,返回给用户之前,会先执行了 闭包内容;
  4. 当使用task myTask << {XXX}的时候,创建task对象,并把closure作为一个action加到此taskaction队列中,并且告诉他“最后才执行这个closure”;
3.1.8 Script Block

gradle文件中包含一些 Script Block,她的作用是让我们来配置相关信息的,不同的SB有不同的配置;
如:

buildscript {   // 这是一个Script Block
    repositories {
        jcenter()
    }

每个SB后面都需要跟一个花括号,闭包;
https://docs.gradle.org/current/javadoc/ ,可输入SB名字,进行查找;
解释几个SB

  1. subprojects:它会遍历 工程 中的 每个子 Project,在其closure中,默认参数是子project对应的Project对象,由于其他SB都在subprojects中,所以相当于对每个Project都配置了一些信息;
  2. buildscript:它的 closure 是在一个类型为 ScriptHandler 的对象上执行的。主意用来所依赖的 classpath 等信息。通过查看 ScriptHandler API 可知,在 buildscript SB 中,你可以调用 ScriptHandler 提供的 repositories(Closure )dependencies(Closure)函数。这也是为什么 repositoriesdependencies
    两个 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)
            }
        }
    }

使用技巧


  • app2main目录结构是一样的,那是不是意味着,app2main是平级的?切换到app2分支的时候就会走app2java代码和res的资源呢?
    不! app2main并不是平级,相反的,app2main的附属
    main是公共代码资源库,app2的所有缺失的javares资源都会去main下找公共资源,所以我们切换到app2渠道下,可以直接运行app,除了applicationId不同之外,app不会有任何变化。
    main是公共代码资源库,这句话的意思是说,无论有多少个渠道,main下的javares都是最基本的存在,类似于所有其他的渠道都在引用main这个库的意思。这和我们开发引用一个库是类似的原理,只是完全反转过来,我们开发一个库,是app来引用这个库,而多渠道下都在一个app下,其他渠道以类似引用的方式来使用main下的javares

  • 切换到app2分支的时候就会走app2的java代码和res的资源呢?
    如果理解了第一个问题,那第二个问题也就比较好理解了。app2作为main的附属,切换到app2分支后,会将app2下的java代码和res合并到main下编译运行。

  • 随之又会有一个新的问题,java代码和res资源是如何合并的?

  1. java代码的合并
    只要不同的module没有路径+名称完全相同的类即不会报错

  2. drawable的合并
    只需要命名一致,并对比main项目中图片放置的位置放到app2项目的对应位置即可完成替换。


    图片替换要注意两点:
    一.目前和命名一致;二.main下有几套图片,app2下就要有几套图片,可以多但不能少。
    app2下新增一个main没有的图片,代码中去引用了的话,切换到main渠道下会报错找不到该资源文件,这个问题稍后讲解。

  3. layout合并
    layout布局文件跟drawable图片合并一样,也是要求命名一致,但涉及到布局文件中的id的处理,要求比较严格,如果相同的功能只是布局位置,字体大小,色值等调整,那么id必须一致,因为同一个java文件引用不同渠道下的layout布局,如果id不同,切换渠道肯定报错;如果app2中新增一个id,而又在java代码中引用了,那么切换到main渠道下也会报错,因为main渠道下的layout没有这个id,这块的处理稍后再说。

  4. string,color合并
    string和color等类似独一份的资源文件合并又有所不同,简单的说就是,相同命名的string和color会被替换,不同命名的会新增。如图:



    相同的app_name就会被替换成MyApp2的名称。
    不同命名的会新增,也会有layout布局id类似的问题,如果mainstring.xml没有相同命名的资源,同时又在java代码中引用了,一样会出问题,这块稍后一起讲解。

  5. 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);
    }
}

差异化不多的情况下,这种写法是最方便的,也是最效率的,唯一的坏处就是在于要多判断。
注:这种差异化处理是将mainapp2分别当做一个独立的渠道,但因为main还是公共代码库,所以切换到app2下进行编译,会同时编译app2main下的java代码,这种情况下main代码中引用app2的类是没有问题的。
但如果切换到main渠道下去编译,你会发现编译后提示找不到app2下类的错误,那是因为切换到main渠道下,只会编译mainjava代码,不会编译app2java代码,自然就找不到对应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/javaapp2/java,就可以了。

二. 分离公共代码库,每个app创建对应的渠道
在前文中,我们都是把main当做一个单独的app渠道,app2作为第二个渠道,现在的方式就是,将main的渠道单独分离出来,创建app1渠道。将app1app2差异的类从main下剪切出来同时复制到对应的app1app2下,单独去开发对应的渠道代码,互相不干扰。
这样,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代码。
app1app2下相同的类也不会报错:


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

你可能感兴趣的:(ant、gradle打包相关)