阅读 252

Android Gradle Plugin源码解析

笔者最近在做公司项目的模块化重构,做的过程中一直在思考以下几个问题:

  • 一个apk文件和一个aar文件有什么区别?
  • 什么样的工程会导出一个apk,什么样的工程可以导出aar?
  • 一个apk的诞生伴随着哪些配置的过程,aar呢?
  • 他们俩之间可以快速的进行交换吗?

以上的这些疑问都在Google大大给我们开发的两个plugin中得到答案:

  • com.android.application
  • com.android.library

这是我们开发安卓应用时最常用的两个plugin,作为一个Android开发者,怎么可能不对它的实现不感兴趣呢,所以接下来我将用两到三个博客的内容,谈一谈读Android Gradle Plugin源码的一些心得。今天主要讲一些基础的部分。

源码下载方式

Android Gradle Plugin 一个版本的源码大概有30多个G,如果你的磁盘资源充足,可以使用repo的方式下载到本地,下面是2.3.0分支的一个示例:

$ mkdir gradle_2.3.0
$ cd gradle_2.3.0
$ repo init -u https://android.googlesource.com/platform/manifest -b gradle_2.3.0
$ repo sync
复制代码

如果你没有翻墙的工具,可以使用国内的一些镜像:

repo的初始化可以参照 Google 教程 source.android.com/source/down…

下载后的源码用IntelliJ IDEA打开tools的base路径,目录结构如下:

项目结构

主要代码在红框内的三个module中。在看Android Gradle Plugin的源码之前,我们先简单的看一下一个自定义的Gradle Plugin是如何实现的。

Gradle plugin简介

关于自定义一个 Gradle Plugin 的教程很多,我们简单的做一个说明。使用 gradle init 命令可以在当前目录下新建一个简单的gradle工程,目录结构如下:

gradle工程

这是一个基于Gradle Wrapper的多工程Gradle项目。在settings.gradle中可以配置子项目的路径,像我们在Android项目中经常配置的:

include ':app'
include ':lib'
复制代码

说到这里可以多说一句在模块化开发中的经验,我们可以通过指定subproject的路径的方式,可以将本地任何路径下的代码导入工程中来,方便我们进行本地调试:

include ':lib'
project(':lib').projectDir = new File('xx/xx/xx/lib')
复制代码

在rootproject的build.gradle文件中创建一个最简单的gradle plugin:

class GreetingPluginExtension {
    String message
    String greeter
}

class GreetingPlugin implements Plugin<Project> {
    void apply(Project project) {
        def extension = project.extensions.create('greeting', GreetingPluginExtension)
        project.task('hello') {
            doLast {
                println "${extension.message} from ${extension.greeter}"
            }
        }
    }
}


apply plugin: GreetingPlugin

greeting {
    message = 'Hi'
    greeter = 'Gradle'
}
复制代码

这段代码中,我们定义了一个GreetingPlugin,他新增了一个名为 ‘hello’ 的task,在终端输出一行信息,这个信息可以通过 GreetingPluginExtension 进行配置,我们执行一下:

./gradlew hello
Starting a Gradle Daemon (subsequent builds will be faster)
Parallel execution with configuration on demand is an incubating feature.
:hello
Hi from Gradle

BUILD SUCCESSFUL

Total time: 4.0 secs
复制代码

其实也可以看出自定义一个plugin主要就是新增 Task 及所需参数进行配置的 Extension。Android Gradle Plugin定义了很多task,其中我们最常用的包括 clean build assemble 等,还有更多这些task运行时依赖的task,涉及到安卓编译打包的各个方面,我们在下一个博客中再具体阐述。今天主要Android Plugin的Extension的部分实现,这也是我们日常配置一个Android工程最主要的工作。

Extension机制

如何理解 Gradle 的 Extension,这涉及到Groovy的闭包委托特性。Groovy的闭包有this、owner、delegate三个属性,当你在闭包内调用方法时,由他们来确定使用哪个对象来处理。有关闭包的详情可以查看 Groovy Closures。利用Groovy的闭包委托特性,我们可以简单的实现Extension:

class Person {
    String personName = '李四'  
    int personAge = 18
    def printPerson(){
        println "name is ${personName},age is ${personAge}"
    }
}
def person(Closure<Person> closure){
    Person p = new Person();
    closure.delegate = p    //委托模式优先
    closure.setResolveStrategy(Closure.DELEGATE_FIRST);
    return closure
}

def closure = person {
    personName = '张三'
    personAge = 20
    printPerson()
}

task configClosure {
    doLast {
        closure()
    }
}
复制代码

首先我们定义一个Person对象,然后定义一个person的方法,它接受一个闭包作为参数,修改闭包委托模式优先,执行person方法,定义一个task执行这个闭包,运行结果如下:

/gradlew configClosure
Parallel execution with configuration on demand is an incubating feature.
:configClosure
name is 张三,age is 20

BUILD SUCCESSFUL

Total time: 0.981 secs
复制代码

Gradle中大量使用了Extension这一特性,引用gradle一句原话:

Many Gradle objects are extension aware. This includes; projects, tasks, configurations, dependencies etc.

安卓插件使用的Extension都继承自BaseExtension:

 * <ul>
 * <li>Plugin <code>com.android.application</code> uses {@link AppExtension}
 * <li>Plugin <code>com.android.library</code> uses {@link LibraryExtension}
 * <li>Plugin <code>com.android.test</code> uses {@link TestExtension}
 * <li>Plugin <code>com.android.atom</code> uses {@link AtomExtension}
 * <li>Plugin <code>com.android.instantapp</code> uses {@link InstantAppExtension}
 * </ul>
复制代码

com.android.application插件使用的是AppExtension,com.android.library插件使用的是LibraryExtension。下面分别讲一下两个Extension的详细配置。

AppExtension

以下是AppExtension的所有配置,我按照使用频率进行一个简单的介绍,第一行是官方对于属性的介绍,我会针对每个属性做一些使用上的说明。

applicationVariants

The list of Application variants. Since the collections is built after evaluation, it should be used with Gradle's all iterator to process future items.

applicationVariants是AppExtension继承自BaseExtension唯一拓展的成员变量,它的参数类型是DefaultDomainObjectSet,这是不同buildType及Flavor的集合, applicationVariants最常用的是它的all方法,例如一个简单的修改apk名字的代码:

def buildTime() {
    return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
}
android {
	applicationVariants.all { variant ->
		variant.outputs.each { output ->
			def outputFile = output.outputFile
			if (outputFile != null && outputFile.name.endsWith('.apk')) {
				def fileName = "${variant.buildType.name}-${variant.versionName}-${buildTime()}.apk"
				output.outputFile = new File(output.outputFile.parent,fileName)
			}
		}
	}
}
复制代码

buildToolsVersion

Required. Version of the build tools to use.

defaultConfig

Default config, shared by all flavors.

所有Flavor的默认设置,它是一个 ProductFlavor 对象,可以做以下设置:

defaultConfig {
	applicationId	 '**.**.**' //The application ID.
	
	applicationIdSuffix '.two' //applicationId的后缀,可以用在想同时安装运行两个Flavor包的时候,比如同时安装debug包和Release包做一些对比。

    minSdkVersion 14

    targetSdkVersion 25

    versionCode  1

    versionName "1.0"

    versionNameSuffix ".0"  // versionName后缀
	
	consumerProguardFiles 'proguard-rules.pro' //用于Library中,可以将混淆文件输出到aar中,供Application混淆时使用。
	
	dimension 'api' //给渠道一个分组加维度的概念,比如你现在有三个渠道包,分成免费和收费两种类型,可以添加一个dimension, 打渠道包的时候会自动打出6个包,而不需要添加6个渠道,详细的说明可见 https://developer.android.com/studio/build/build-variants.html#flavor-dimensions。
	                     
	externalNativeBuild {   //ndk的配置,AS2.2之后推荐切换到cmake的方式进行编译。
        cmake {
        	  cppFlags "-frtti -fexceptions"
              arguments "-DANDROID_ARM_NEON=TRUE"
        	  buildStagingDirectory "./outputs/cmake"
            path "CMakeLists.txt"
            version "3.7.1"
        }
        ndkBuild {
            path "Android.mk"
            buildStagingDirectory "./outputs/ndk-build"
        }
    }
    
    javaCompileOptions {
            annotationProcessorOptions {    //注解的配置。
                includeCompileClasspath true //需要使用注解功能。
                arguments = [ eventBusIndex : 'org.greenrobot.eventbusperf.MyEventBusIndex' ]   //AbstractProcessor中可以读取到该参数。
                classNames 
            }
        }

    manifestPlaceholders = [key:'value']     //manifest占位符 定义参数给manifest调用,如不同的渠道id。
	
    multiDexEnabled true      //开启 multiDex

    multiDexKeepFile  file('multiDexKeep.txt')    //手动拆包,将具体的类放在主DEX。

    multiDexKeepProguard   file('multiDexKeep.pro')  //支持Proguard语法,进行一些模糊匹配。

    ndk {
        abiFilters 'x86', 'x86_64', 'armeabi'   //只保留特定的api输出到apk文件中。
    }

    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' //混淆文件的列表,如默认的android混淆文件及本地proguard文件,
                                                                                      //切记不要遗漏android混淆文件,否则会导致一些默认的安卓组件无法找到。

    signingConfig {
            //签名文件的路径
            storeFile file('debug.keystore')
            //签名文件密码
            storePassword 'android'
            //别名
            keyAlias 'androiddebygkey'
            //key的密码
            keyPassword 'android'
    }

    buildConfigField('boolean','IS_RELEASE','false')   //代码中可以通过BuildConfig.IS_RELEASE 调用。

    resValue('string','appname','demo')  //在res/value 中添加<string name="appname" translatable="false">demo</string>。

    resConfigs "cn", "hdpi"  //指定特定资源,可以结合productFlavors实现不同渠道的最小的apk包。
}
复制代码

productFlavors

All product flavors used by this project. 渠道包的列表,可以覆盖defaultConfig的参数配置,形成自己的风味

flavorDimensionList

The names of flavor dimensions. 添加维度的定义,维度的使用上面defaultConfig已经有说明了。

resourcePrefix

A prefix to be used when creating new resources. Used by Android Studio. 在模块化开发中比较重要,给每个模块指定一个特定的资源前缀,可以避免多模块使用相同的文件命名后合并冲突,在build.gradle中指定了这个配置后,AS会检查不合法的资源命名并报错。

buildTypes

Build types used by this project.

buildType的列表,默认有release和debug,可以自己自定义不同的buildtype,相应的构建task name是 assemble+buildTypeName, buildType部分配置和defaultConfig相同,不同配置使用说明如下:

debug {

       applicationIdSuffix   '.debug'        //同defaultConfig

       versionNameSuffix '.1'     //同defaultConfig

       debuggable true  //生成的apk是否可以调试 debug默认是true   release默认false

       jniDebuggable true   //是否可以调试NDK代码 使用lldb进行c和c++代码调试

       crunchPngs   true  //是否开启png优化,会对png图片做一次最优压缩,影响编译速度,debug默认是false   release默认true

       embedMicroApp true  //Android Wear的支持

       minifyEnabled  true //是否开启混淆

       renderscriptDebuggable false   //是否开启渲染脚本

       renderscriptOptimLevel 5    //渲染脚本等级 默认是5

       zipAlignEnabled true   //是否zip对齐优化 默认就是true app对齐
 }
复制代码

ndkDirectory

The NDK directory used. NDK路径,也可以在local.properties 中配置 ndk.dir=/Users/xxxx/Library/Android/sdk

sdkDirectory

The SDK directory used. 同ndkDirectory,目前一般配置在local.properties。

aaptOptions

Options for aapt, tool for packaging resources. aapt是一个资源打包工具,可以对资源优化做一些动态配置:

aaptOptions{
        additionalParameters '--rename-manifest-package',
                'cct.cn.gradle.lsn13','-S','src/main/res2','--auto-add-overlay'  //aapt执行时的额外参数 

        cruncherEnabled true    //对png进行优化检查

        ignoreAssets '*.jpg'  //对res目录下的资源文件进行排除 把res文件夹下面的所有.jpg格式的文件打包到apk中

        noCompress '.jpg'   //对所有.jpg文件不进行压缩

    }
复制代码

adbExecutable

The adb executable from the compile SDK. adb工具的文件路径,可以配置在环境变量中。

adbOptions

Adb options. adb命令的一些配置

    adbOptions   {
        installOptions '-r' '-d'  //调用adb install命令时默认传递的参数

        timeOutInMs 1000   //执行adb命令的超时时间
    }
复制代码

compileOptions

Compile options. 编译配置

compileOptions{
        encoding 'UTF-8'  //java源文件的编码格式 默认UTF-8

       
        incremental true    //java编译是否使用gradle新的增量模式 

        sourceCompatibility JavaVersion.VERSION_1_7    //java源文件编译的jdk版本 
        
        targetCompatibility JavaVersion.VERSION_1_7   //编译出的class的版本
    }
复制代码

dataBinding

Data Binding options. DataBinding的使用细节可以查看 Google文档,可以在build.gradle中开启DataBinding:

dataBinding  {
      enabled = true   //开启databinding
      version = "1.0"
      addDefaultAdapters = true
}
复制代码

defaultPublishConfig

Name of the configuration used to build the default artifact of this project. 指定发布的渠道及BuildType类型。在Library中使用,默认Release。

signingConfigs

Signing configs used by this project. 一个签名配置的列表,可以供不同渠道和buildType使用。

lintOptions

Lint options. Lint可以检查出代码中一些不规范的使用,如果想保留一些苟且的代码,可以参考以下配置(简友lyzaijs同学对这一块有详细说明,下面引用自他的博客):

lintOptions { 
        quiet true // 设置为 true时lint将不报告分析的进度
       
        abortOnError false    // 如果为 true,则当lint发现错误时停止 gradle构建
        
        ignoreWarnings true   // 如果为 true,则只报告错误
       
        absolutePaths true   // 如果为 true,则当有错误时会显示文件的全路径或绝对路径 
        
        checkAllWarnings true  // 如果为 true,则检查所有的问题,包括默认不检查问题
        
        warningsAsErrors true  // 如果为 true,则将所有警告视为错误
        
        disable 'TypographyFractions','TypographyQuotes'  // 不检查给定的问题id
       
        enable 'RtlHardcoded','RtlCompat', 'RtlEnabled'   // 检查给定的问题 id
     
        check 'NewApi', 'InlinedApi'    // * 仅 * 检查给定的问题 id
       
        noLines true  // 如果为true,则在错误报告的输出中不包括源代码行
        
        showAll true  // 如果为 true,则对一个错误的问题显示它所在的所有地方,而不会截短列表,等等。
        
        lintConfig file("default-lint.xml")  // 重置 lint 配置(使用默认的严重性等设置)。
       
        textReport true  // 如果为 true,生成一个问题的纯文本报告(默认为false)
        
        textOutput 'stdout' // 配置写入输出结果的位置;它可以是一个文件或 “stdout”(标准输出)
        
        xmlReport false // 如果为真,会生成一个XML报告,以给Jenkins之类的使用
       
        xmlOutput file("lint-report.xml")  // 用于写入报告的文件(如果不指定,默认为lint-results.xml)
        
        htmlReport true // 如果为真,会生成一个HTML报告(包括问题的解释,存在此问题的源码,等等)
       
        htmlOutput file("lint-report.html")  // 写入报告的路径,它是可选的(默认为构建目录下的 lint-results.html )

        checkReleaseBuilds true   // 设置为 true, 将使所有release 构建都以issus的严重性级别为fatal(severity=false)的设置来运行lint,并且,如果发现了致命(fatal)的问题,将会中止构建(由上面提到的 abortOnError 控制.

        fatal 'NewApi', 'InlineApi'  //设置给定问题的严重级别(severity)为fatal (这意味着他们将会在release构建的期间检查 (即使 lint 要检查的问题没有包含在代码中)
        
        error 'Wakelock', 'TextViewEdits'   // 设置给定问题的严重级别为error
        
        warning 'ResourceAsColor'   // 设置给定问题的严重级别为warning
       
        ignore 'TypographyQuotes'    // 设置给定问题的严重级别(severity)为ignore (和不检查这个问题一样)
    }

}
复制代码

###dexOptions Dex options. Android dx工具是将java的classes文件编译为字节码dex文件,工具位于android sdk platform-tools目录,我们在做热修复做差分包的时候可能会用到这个工具,在android打包过程中,dx的可以做以下配置:

dexOptions {
        additionalParameters '--minimal-main-dex','--set-max-idx-number=10000'  //dx命令附加参数

        javaMaxHeapSize '2048m'    //执行dx时java虚拟机可用的最大内存大小

        jumboMode true   //开启大模式,所有的class打到一个dex中,可以忽略65535方法数的限制,低于14版本不可运行

        keepRuntimeAnnotatedClasses true //在dex中是否保留Runtime注解 默认是true

        maxProcessCount 4   //默认dex中的进程数  默认是4 

        threadCount 4  //默认的线程数

        preDexLibraries true  //对library预编译 提高编译效率 但是clean的时候比较慢  默认开启的
    }
复制代码

packagingOptions

Packaging options. 打包配置,尤其是在一些多模块开发工程中,涉及到的一些资源合并取舍的策略:

packagingOptions   {
       
        pickFirsts = ['META-INF/LICENSE']    //pickFirsts做用是 当有重复文件时 打包会报错 这样配置会使用第一个匹配的文件打包进入apk

        merge 'META-INF/LICENSE'  //重复文件会合并打包入apk

        exclude 'META-INF/LICENSE'    //打包时排除匹配文件
    }
复制代码

sourceSets

All source sets. Note that the Android plugin uses its own implementation of source sets, AndroidSourceSet.An AndroidSourceSet represents a logical group of Java, aidl and RenderScript sources as well as Android and non-Android (Java-style) resources.

所有Android资源的集合,包括Java代码,aidl以及RenderScript。默认配置如下,有一些自定义路径的情况下需要修改:

sourceSets{
        main  {
            res.srcDirs 'src/main/res' 
            jniLibs.srcDirs = ['libs'] 
            aidl.srcDirs 'src/main/aidl'
            assets.srcDirs 'src/main/assets'
            java.srcDirs 'src/main/java'
            jni.srcDirs 'src/main/jni'
            renderscript.srcDirs 'src/main/renderscript' 
            resources.srcDirs 'src/main/resources'
            manifest.srcFile 'src/main/AndroidManifest.xml'
        }

        free  {   //除了main,也可以给不同的渠道指定不同的配置

        }
    }
复制代码

splits

APK splits options. 这个特性非常有用,不过国内应用基本使用不上,可以根据CPU架构和屏幕像素密度打出最小的apk包,再配合Google Play的市场分发机制,让你可以下载到适合你使用的apk。有abi、density、language三个维度进行过滤:

splits  {
        abi {
            enable true  //开启abi分包

            universalApk true  //是否创建一个包含所有有效动态库的apk

            reset()  //清空defaultConfig配置

            include 'x86','armeabi' //打出包含的包 这个是和defaultConfig累加的

            exclude 'mips'   //排除指定的cpu架构
        }

        density {
            
            enable true  //开启density分包

            reset()  //清空所有默认值

            include 'xhdpi','xxhdpi'   //打出包含的包 这个是和默认值累加的

            exclude 'mdpi' //排除指定
        }

        language {
              enable true  //开启language分包
              include 'en','cn'  // 指定语言
        }
}
复制代码

variantFilter

Callback to control which variants should be excluded. 上面我们通过flavor及buildType构建出大量的apk,这里可能有你不需要的,android plugin也考虑到这一点,可以动态设置忽略一些产出:

variantFilter { variant ->

        def buildTypeName = variant.buildType.name
        def flavorName = variant.flavors.name

        if (flavorName.contains("360") && buildTypeName.contains("debug")) {
            // Tells Gradle to ignore each variant that satisfies the conditions above.
            setIgnore(true)
        }
    }
复制代码

LibraryExtension

libraryVariants

The list of library variants. Since the collections is built after evaluation, it should be used with Gradle's all iterator to process future items.

libraryVariants也有类似于applicationVariants的all闭包,可以获取到所有的应用回调,其中可以做一些特定的设置:

android.libraryVariants.all { variant ->
    def mergedFlavor = variant.getMergedFlavor()
    // Defines the value of a build variable you can use in the manifest.
    mergedFlavor.manifestPlaceholders = [hostName:"www.example.com"]
}
复制代码

以上是对AppExtension及LibraryExtension的一个详细说明,这个Android Plugin所有task运行的基础配置,下一节将关注我们常用的task实现以及其中的依赖关系。

参考文献

google.github.io/android-gra…

docs.gradle.org/current/dsl…

关注下面的标签,发现更多相似文章
评论