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) 带包含和排除模式 精确控制文件集合
注意: 文件树的匹配模式用的是 Ant 风格的通配符。** 表示任意多层目录,* 表示任意文件名(不含路径分隔符)。写错了匹配不到文件,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 提供了 ZipTarJar 等现成的任务类型。

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 / FileCollection Project.file() 创建引用 files() 懒加载集合 FileTree(文件树) fileTree(dir).include().exclude() Ant 风格通配符匹配 Copy / Zip 任务 from() / into() / include() filter() / rename() / eachFile() 文件过滤与转换 临时文件管理 ReplaceTokens / 闭包过滤 mkdir() / delete() / clean eachFile 钩子修改路径/内容 注册到 clean 任务统一清理

嗯,看到这张图你应该能感受到,Gradle 的文件操作其实是一个环环相扣的体系。从最底层的 File 对象,到上层的 Copy 任务,再到跨任务的临时文件管理,每一步都有它的设计意图。

我个人建议,在实际项目中,尽量用 fileTree() 代替手写路径列表,用 Copy 任务代替自己写 IO 流。Gradle 团队已经帮你处理好了增量构建、缓存、并发等问题。你只需要关注“要复制什么”和“要过滤什么”就行了。

最后,记住一句话:文件操作无小事。一个路径写错,可能导致整个构建失败。一个过滤条件漏了,可能把敏感文件打包进 APK。多测试,多验证,总没错。


公众号:蓝海资料掘金营,微信deep3321