使用Android Studio构建Cordova项目
现在Android开发的IDE用户渐渐转投到Android Studio下了。而在用Cordova开发时,虽然多数时候是web开发,但有些情况,比如开发自定义插件时,还是要进行Android开发。那么就需要在Android Studio中打开Cordova项目中的Android部分。
在我以前关于Cordova的例子中,如果遇到Android开发工作,IDE用的都是Eclipse。现在Cordova 5已经在创建项目时为我们生成Gradle脚本了。这样在Android Studio打开时就可以按现有的Android Studio项目打开。
而关于Gradle基础,本来想详细的写一下。但一位叫neu的作者的译作《Gradle for Android》(以下简称G文)已经写的很详细了,而且同名书也是一本好书,所以推荐,不再赘述。
用以前一篇叫《构建一个完整的Cordova应用》的代码为例,打开后项目结构如下图:
项目结构
从图1中可以看到导入的项目结构。可以看到和在Android Studio新建的安装项目的结构有些不同。它包括CordovaLib模块、android模块和一些gradle脚本。
一般Android项目结构和目录结构一样,是这样的:
MyApp
|--build.gradle |--settings.gradle |--app |-- build.gradle |-- build |-- libs |-- src |-- main |--java | |-- com.package.myapp |--res |-- drawable |-- layout |-- etc
由Cordova创建的项目的目录结构是这样的(项目结构见上图)
看起来很乱是不是。但是不影响使用,实际上也没必要非要改成标准格式。因为Gradle已经帮我们在配置脚本中写好了相关配置,它知道如何找到需要的文件。
按照标准的Gradle教程,项目即使没有任何模块(module),Android Studio也会为我们生成一个对应于项目的build.gradle脚本文件的。而观察图1,Android Studio打开的Cordova项目中却没有,编译构建工作也可以正常进行,说明这个文件不是必须的。
它是在andorid、CordovaLib模块中的build.gradle脚本中的android任务的sourceSets的main属性,其中定义了Android Studio项目目录结构和真实目录的对应关系。比如android模块下的build.gradle是这样的:
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'] } }
详细请参考G文第一篇 '保持旧的Eclipse文件结构' 部分。
可以看到,Android Studio中目录的逻辑结构和系统中的文件夹结构是分离的,依靠的就是Gradle的配置能力。对比标准的项目目录结构,Cordova项目把android模块直接放在了项目的根下,而不是其他模块那样(比如CordovaLib模块)作为子文件夹存在,这也说明了它为什么不需要项目的build.gradle脚本,因为项目的脚本就是android模块的脚本。之后添加的模块,都是作为根目录的子文件夹存在的。
此外还有一个多出来的cordova.gradle,以及和G文名称不一致的local.properties。
Gradle脚本的详细说明
上面部分让我们对用Android Studio打开的Cordova项目有了些感性了解。它有其自已的独特性:一方面Cordova创建的项目有自己的Gradle配置脚本,另一方面又保留了自己独特的结构。接下来让我们对Gradle脚本的各个部分作较为详细的了解,同样的基础知识请参考G文或《Gradle for Android》一书。
我们看到Gradle脚本相关的文件有build.gradle(CordovaLib模块)、build.gradle(android模块)、cordova.gradle(CordovaLib模块)、settings.gradle(项目)、local.properties(SDK Location)。看起来有些不一样。
不过没关系,我们知道Gradle构建时首先要去找build.gradle脚本的。通过上面的项目结构知道,首先执行的应该是逻辑上属于android模块,但实际上位于项目根目录下的build.gradle。就从这里开始。
android模块的build.gradle脚本
打开build.gradle。首先看到一些Cordova生成的一些注释。原文就不照抄了,大意是一些授权说明信息。最后的单行注释很关键:"生成文件!请不要编辑!"。 虽然不能编辑,但一则其他的文件并没有这么写,二则做为入口文件,我们还是需要对它做一个了解。
注释后紧接着就是buildscript方法。在它里面首先是repositories方法,它告诉我们使用的库是mavenCentral。接下来是依照已经在本地安装的gradle的版本选择使用的Gradle插件版本,并且语句上面有注释可以参考,以后几乎在每条语句上面都有注释帮助我们理解并告诉我们相关参考资料的位置。
// 列表1-1。 buildscript { repositories { mavenCentral() } // Switch the Android Gradle plugin version requirement depending on the // installed version of Gradle. This dependency is documented at // http://tools.android.com/tech-docs/new-build-system/version-compatibility // and https://issues.apache.org/jira/browse/CB-8143 if (gradle.gradleVersion >= "2.2") { dependencies { classpath 'com.android.tools.build:gradle:1.0.0+' } } else if (gradle.gradleVersion >= "2.1") { dependencies { classpath 'com.android.tools.build:gradle:0.14.0+' } } else { dependencies { classpath 'com.android.tools.build:gradle:0.12.0+' } } }
随后又使用了一个repositories任务。注释说:允许插件通过build-extras.gradle声明Maven依赖。通过extra这个名字,以及前面不要编辑的警告,它的作用可能是对这个build.gradle文件的修改或补充。其实在下面的代码中可以看到更多这方面的信息。
// 列表1-2。 repositories { mavenCentral() }
接下来有一个wrapper任务,查阅文档得知它用于定制Gradle Wrapper的。如果对Wrapper不了解,请参考《G》文的第一篇"使用Gradle Wrapper"部分。
// 列表1-3。 task wrapper(type: Wrapper) { gradleVersion = '2.2.1' }
接下来定义了一个ext属性,其中定义了一些额外属性。注释也说明了其中定义的属性需要通过环境变量、build-extras.gradle或gradle.properties设置。
ext属性中首先引用了cordova.gradle。这样就知道项目中这个文件用在哪里了。但下面并没有用到它。通过后面的代码推测它可能是供build-extras.gradle调用的。接下来一系列条件判断语句分别定义了一些属性并把它们初为null。这些属性是都以cdv开头,表示一些Cordova构建属性。接下来的代码中会看到它们的作用。最后定义了一个cdvPluginPostBuildExtras数组变量,用来向里面追加Gradle插件扩展。
// 列表1-4。 ext { apply from: 'CordovaLib/cordova.gradle' // The value for android.compileSdkVersion. if (!project.hasProperty('cdvCompileSdkVersion')) { cdvCompileSdkVersion = null; } // The value for android.buildToolsVersion. if (!project.hasProperty('cdvBuildToolsVersion')) { cdvBuildToolsVersion = null; } // Sets the versionCode to the given value. if (!project.hasProperty('cdvVersionCode')) { cdvVersionCode = null } // Sets the minSdkVersion to the given value. if (!project.hasProperty('cdvMinSdkVersion')) { cdvMinSdkVersion = null } // Whether to build architecture-specific APKs. if (!project.hasProperty('cdvBuildMultipleApks')) { cdvBuildMultipleApks = null } // .properties files to use for release signing. if (!project.hasProperty('cdvReleaseSigningPropertiesFile')) { cdvReleaseSigningPropertiesFile = null } // .properties files to use for debug signing. if (!project.hasProperty('cdvDebugSigningPropertiesFile')) { cdvDebugSigningPropertiesFile = null } // Set by build.js script. if (!project.hasProperty('cdvBuildArch')) { cdvBuildArch = null } // Plugin gradle extensions can append to this to have code run at the end. cdvPluginPostBuildExtras = [] }
接下来这部分代码,首先判断build-extras.gradle文件是否存在,如果存在则把它应用到构建脚本中。下面的判断语句检查build-extras.gradle是否定义了列表1-4的属性。如果有就使用build-extras.gradle中的,如果没有,则按下面语句设置:
// 列表1-5。 // Set property defaults after extension .gradle files. if (ext.cdvCompileSdkVersion == null) { ext.cdvCompileSdkVersion = privateHelpers.getProjectTarget() } if (ext.cdvBuildToolsVersion == null) { ext.cdvBuildToolsVersion = privateHelpers.findLatestInstalledBuildTools() } if (ext.cdvDebugSigningPropertiesFile == null && file('debug-signing.properties').exists()) { ext.cdvDebugSigningPropertiesFile = 'debug-signing.properties' } if (ext.cdvReleaseSigningPropertiesFile == null && file('release-signing.properties').exists()) { ext.cdvReleaseSigningPropertiesFile = 'release-signing.properties' } // Cast to appropriate types. ext.cdvBuildMultipleApks = cdvBuildMultipleApks == null ? false : cdvBuildMultipleApks.toBoolean(); ext.cdvMinSdkVersion = cdvMinSdkVersion == null ? null : Integer.parseInt('' + cdvMinSdkVersion) ext.cdvVersionCode = cdvVersionCode == null ? null : Integer.parseInt('' + cdvVersionCode)
上述代码表示如果没有设置列表1-4中的属性时设置它们,其中调用了cordova.gradle脚本中的方法。如前所述之后会说明这些变量的作用。
接下来这段代码,注释告诉我们,要让cdvBuild的任务依赖于debug/arch-specific,即平台相关,解决的是不同平台构建的问题。首先分成debug和release版,然后根据前面遇到过的cdvBuildMultipleApks和cdvBuildArch的变量判断是否是跨平台构建,如果是则返回一个根据架构名生成的结果。注意到这个cdvBuildArch变量,在上面列表1-4的注释中标明它由build.js设置。这个脚本位于项目目录的cordova/lib/文件夹中,没有纳入到构建中,是cordova cli构建脚本,在这里先不做探讨。
def computeBuildTargetName(debugBuild) { def ret = 'assemble' if (cdvBuildMultipleApks && cdvBuildArch) { def arch = cdvBuildArch == 'arm' ? 'armv7' : cdvBuildArch ret += '' + arch.toUpperCase().charAt(0) + arch.substring(1); } return ret + (debugBuild ? 'Debug' : 'Release') } // Make cdvBuild a task that depends on the debug/arch-sepecific task. task cdvBuildDebug cdvBuildDebug.dependsOn { return computeBuildTargetName(true) } task cdvBuildRelease cdvBuildRelease.dependsOn { return computeBuildTargetName(false) }
接下来定义了一个任务,显然作用是输出列表1-4以及后面定义过的属性值。
task cdvPrintProps << { println('cdvCompileSdkVersion=' + cdvCompileSdkVersion) println('cdvBuildToolsVersion=' + cdvBuildToolsVersion) println('cdvVersionCode=' + cdvVersionCode) println('cdvMinSdkVersion=' + cdvMinSdkVersion) println('cdvBuildMultipleApks=' + cdvBuildMultipleApks) println('cdvReleaseSigningPropertiesFile=' + cdvReleaseSigningPropertiesFile) println('cdvDebugSigningPropertiesFile=' + cdvDebugSigningPropertiesFile) println('cdvBuildArch=' + cdvBuildArch) println('computedVersionCode=' + android.defaultConfig.versionCode) android.productFlavors.each { flavor -> println('computed' + flavor.name.capitalize() + 'VersionCode=' + flavor.versionCode) } }
接下来就是构建的核心部分,每个使用android插件构建都会有的android方法。里面对android的构建定义了一些配置。这些配置的做用可以参考相关文章,这里就不赘述了。之后是在任务准备就绪后,对需要验证的任务进行验证。再下来定义的addSigningProps函数被android方法调用,用来在需要验证时,为属性文件添加验证配置。最后配置了构建后要执行的方法,并且添加了在自定义配置文件build-extras.gradle中配置的构建后执行方法的入口。
总结
综上,在Cordova项目中的android文件夹中的默认两个模块的内容作为容器和插件的源代码,有其相对于普通Gradle Android特殊的目录结构。android模块把CordovaLib和web内容结合在一起生成Crodova应用,android模块和CordovaLib通过使用共同的配置变量,保证了包括编译SDK版本在内的一致性,并提供了额外配置的入口。但万变不离其宗,这一切都是建立在了解Gradle构建工具的基础上的。