11. 文件与文件集合:File与FileCollection、文件树、复制与归档任务、文件过滤与转换、临时文件管理
说实话,Gradle 里的文件操作,是我早期踩坑最多的地方。你想想看,一个构建脚本里,少则几十个文件路径,多则成百上千个资源文件要处理。路径写死了?换台机器就崩。过滤条件写错了?打出来的包多了几个不该有的文件。嗯,今天我们就来彻底搞定它。
11.1 File 与 FileCollection:最基础的文件操作
Gradle 的 File 对象,本质上就是 Java 的 java.io.File。但 Gradle 给它加了一层“糖”——Project.file() 方法。我个人习惯用这个方法来创建文件引用,因为它能自动处理相对路径和绝对路径的转换。
// 推荐写法
def myFile = file('src/main/res/values/strings.xml')
def absoluteFile = file('/home/user/config.properties')
// 不推荐直接 new File()
// def badFile = new File('src/main/res/values/strings.xml') // 路径可能不对
为什么会这样?因为 Project.file() 会基于当前项目的根目录解析路径。而 new File() 是基于 JVM 的工作目录,在 Gradle 多项目构建中,工作目录可能不是你想象的那个。
FileCollection 就是一组文件的集合。你可以把它理解成一个“懒加载”的文件列表。它不会立即把所有文件都加载到内存里,而是等你真正需要遍历或操作时才会去解析。
// 创建一个 FileCollection
def configFiles = files('config/development.properties', 'config/production.properties')
// 遍历它
configFiles.each { file ->
println "找到配置文件: ${file.name}"
}
fileTree() 比 files() 更省事。后面会讲到。
11.2 文件树:目录结构的天然映射
文件树(FileTree)是 FileCollection 的子接口。它代表一个目录树结构。说白了,就是你指定一个根目录,它自动把里面所有文件(包括子目录里的)都给你列出来。
// 创建一个文件树,从 src 目录开始
def sourceTree = fileTree('src').include('**/*.kt').exclude('**/test/**')
// 遍历所有 Kotlin 源文件(排除测试目录)
sourceTree.each { file ->
println "源文件: ${file.path}"
}
我在项目中遇到过一个问题:用 fileTree() 时忘了加 exclude,结果把 build 目录下的临时文件也打包进去了。嗯,那次的教训就是——过滤条件一定要写严谨。
| 方法 | 说明 | 常用场景 |
|---|---|---|
fileTree(dir) |
基于目录创建文件树 | 扫描源码目录 |
fileTree(dir, include) |
带包含模式的文件树 | 只扫描特定类型文件 |
fileTree(dir, include, exclude) |
带包含和排除模式 | 精确控制文件集合 |
** 表示任意多层目录,* 表示任意文件名(不含路径分隔符)。写错了匹配不到文件,Gradle 也不会报错,只会默默返回一个空集合。
11.3 复制与归档任务:Copy 和 Zip 的实战用法
复制文件,听起来简单吧?但 Gradle 的 Copy 任务,远比你想象的强大。它支持过滤、重命名、替换令牌……基本上你能想到的文件操作,它都能做。
task copyAssets(type: Copy) {
from 'src/main/assets'
into 'build/intermediates/assets'
include '**/*.png'
exclude '**/raw/**'
// 重命名文件
rename { fileName ->
fileName.replace('.png', '.webp')
}
}
归档任务就更常用了。打包 APK、AAR、JAR,本质上都是归档操作。Gradle 提供了 Zip、Tar、Jar 等现成的任务类型。
task packageDistribution(type: Zip) {
archiveFileName = 'my-app-${version}.zip'
destinationDirectory = file('build/dist')
from 'build/outputs'
from 'config'
// 可以添加额外的文件
into('docs') {
from 'README.md'
}
}
核心要点: Copy 和 Zip 任务都继承自 AbstractCopyTask。它们共享相同的 API:from()、into()、include()、exclude()、rename()、filter()。学会一个,另一个就通了。
11.4 文件过滤与转换:filter、eachFile 和 transform
文件过滤,说白了就是“挑出你想要的”。但 Gradle 的过滤不仅仅是 include/exclude 那么简单。你还可以在复制过程中修改文件内容。
task processConfig(type: Copy) {
from 'config/template.properties'
into 'build/config'
// 使用 Ant 风格的过滤器替换令牌
filter(ReplaceTokens, tokens: [
version: project.version.toString(),
buildTime: new Date().format('yyyy-MM-dd HH:mm:ss')
])
// 也可以使用闭包自定义过滤
filter { line ->
line.startsWith('#') ? null : line.toUpperCase()
}
}
我曾经在做一个多渠道打包插件时,需要动态替换 AndroidManifest.xml 里的占位符。用 filter 配合 ReplaceTokens,一行代码就搞定了。如果自己写 IO 流去替换,至少得几十行。
对于更复杂的转换需求,可以用 eachFile 钩子。它会在每个文件被复制前回调你。
task transformResources(type: Copy) {
from 'src/main/res'
into 'build/transformed/res'
eachFile { fileCopyDetails ->
// 跳过大于 1MB 的文件
if (fileCopyDetails.size > 1024 * 1024) {
fileCopyDetails.exclude()
}
// 修改文件路径
if (fileCopyDetails.name.endsWith('.9.png')) {
fileCopyDetails.path = fileCopyDetails.path.replace('.9.png', '.png')
}
}
}
11.5 临时文件管理:tmpDir、delete 和 clean 任务
临时文件,用完了就得删。不然日积月累,你的 build 目录会膨胀到几个 G。Gradle 提供了几种管理临时文件的方式。
最直接的方式是用 Project.mkdir() 和 Project.delete():
task createTempDir {
doLast {
def tmpDir = file("${buildDir}/tmp/myTask")
mkdir(tmpDir)
// 做一些操作...
// 用完删除
delete(tmpDir)
}
}
但更好的做法是利用 Gradle 的 clean 任务。你只需要把你的临时目录注册到 clean 的依赖里:
clean {
delete file('build/tmp')
delete file('build/generated')
}
// 或者更优雅的方式
tasks.register('cleanTemp') {
doLast {
delete fileTree('build/tmp').include('**/*.tmp')
}
}
clean.dependsOn cleanTemp
doLast 里直接调用 delete() 删除临时文件,结果因为 Gradle 的输入输出快照机制,导致任务永远被认为是“过期的”。每次构建都要重新执行。正确的做法是:把临时文件放在 build 目录下,让 clean 任务统一管理。
11.6 知识体系总览
下面这张图,是我梳理的本章知识结构。你可以把它当作一个“文件操作速查地图”。
嗯,看到这张图你应该能感受到,Gradle 的文件操作其实是一个环环相扣的体系。从最底层的 File 对象,到上层的 Copy 任务,再到跨任务的临时文件管理,每一步都有它的设计意图。
我个人建议,在实际项目中,尽量用 fileTree() 代替手写路径列表,用 Copy 任务代替自己写 IO 流。Gradle 团队已经帮你处理好了增量构建、缓存、并发等问题。你只需要关注“要复制什么”和“要过滤什么”就行了。
最后,记住一句话:文件操作无小事。一个路径写错,可能导致整个构建失败。一个过滤条件漏了,可能把敏感文件打包进 APK。多测试,多验证,总没错。
公众号:蓝海资料掘金营,微信deep3321