Android 项目结构解析——跨平台开发者必须了解的项目骨架
Android 项目用一套固定的目录骨架组织代码、资源、原生库与构建配置; 跨平台项目里所有原生能力(插件、SDK、.so)最终都落进这套结构,看懂它才能精准定位、快速排错。
一句话概括
Android 项目的目录结构不是随意摆的,而是一套系统约定:源码、资源、原生库、构建脚本各有固定位置,Gradle 按”source set(源集)”规则把它们编进最终 APK。
对 Flutter / RN 开发者,你的原生代码、插件引入的 .so、要改的 AndroidManifest、要加的混淆规则,全在这套骨架里。理解它,集成原生 SDK、换图标、排”资源冲突/NDK 报错/图标不更新”时,你就能直接知道文件该放哪、错在哪,而不是对着目录发懵。
一句话结论:目录即契约,Gradle 按位置找文件;放错地方,它当没看见。
核心知识点
1. 跨平台项目里的 android/ 原生层
无论 Flutter 还是 RN,原生 Android 部分都在 android/ 目录下,骨架一致:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
android/
├── app/
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin/ # Kotlin 源码(旧项目用 java/)
│ │ ├── res/ # 资源
│ │ ├── assets/ # 原始资源
│ │ └── jniLibs/ # 原生 .so 库
│ ├── debug/ # debug 变体专属
│ └── release/ # release 变体专属
├── gradle/wrapper/gradle-wrapper.properties
├── build.gradle.kts # 项目级
├── settings.gradle.kts
├── gradle.properties
└── local.properties # 本机环境,不进版本库
2. Source Set(源集):Gradle 怎么”挑”文件
src/main 是公共代码;src/debug、src/release、src/<flavor> 是变体专属。编译时按优先级合并:
1
变体专属(如 src/dev)> 构建类型(src/debug)> 主源集(src/main)
同名资源/代码,高优先级覆盖低优先级——这正是多渠道换图标、换配置、换 API 地址的原理:在 src/dev/ 下放同名 config.xml,就只在该 flavor 生效,不用改 main。
3. 源码目录:src/main/kotlin 与 MainActivity
1
2
3
4
5
6
app/src/main/kotlin/com/example/myapp/
├── MainActivity.kt # 引擎宿主(Flutter/RN 的页面容器)
├── MyApplication.kt # 自定义 Application(SDK 统一初始化)
├── ui/
├── service/
└── utils/
1
2
3
4
5
6
7
// Flutter 的 MainActivity 只是个"壳",引擎自己管生命周期
class MainActivity : FlutterActivity()
// RN 的 MainActivity 继承 ReactActivity,指定 JS 入口组件
class MainActivity : ReactActivity() {
override fun getMainComponentName(): String = "MyRNApp"
}
要点:现代项目 Kotlin 源码放 src/main/kotlin/(老项目用 java/);AGP 默认两者都是源码目录,但团队应统一约定。自己写的原生模块、MethodChannel 桥接,都加在这里——插件源码则由 Gradle 自动引入,不用手动搬。
4. 资源目录 res/:按类型与配置分桶
1
2
3
4
5
6
7
8
9
10
res/
├── drawable/ # 位图、XML 矢量图、shape、selector
├── drawable-night/ # 夜间资源(按配置限定符)
├── mipmap-xxhdpi/ # 应用图标(按密度)
├── layout/ # 界面 XML(原生 Activity/Fragment 用)
├── values/ # strings / colors / themes / dimens
├── values-zh/ # 中文多语言覆盖
├── xml/ # network_security_config / file_paths / backup_rules
├── raw/ # 原始文件,经 R.raw.xxx 访问
└── font/ # 自定义字体(API 26+)
- 密度桶:
mdpi(1x) / hdpi(1.5x) / xhdpi(2x) / xxhdpi(3x) / xxxhdpi(4x)。图标一定放mipmap/(启动器会选最高密度再缩放),其它图放drawable/。 values与多语言:values-zh/strings.xml会被中文环境自动选用。xml/的常客:network_security_config.xml(放行明文域名)、file_paths.xml(FileProvider 共享路径)。这些在AndroidManifest里用@xml/xxx引用。
5. assets vs res/raw:两种”原始文件”
| 维度 | assets/ | res/raw/ |
|---|---|---|
| 访问方式 | AssetManager.open("a/b.json"),路径任意嵌套 | resources.openRawResource(R.raw.xxx) |
| 目录结构 | 自由嵌套 | 扁平、不能建子目录 |
| 典型用途 | HTML/JS、ML 模型、证书、字体 | 提示音、固定 JSON |
1
2
3
4
5
6
// assets:常用于 WebView 加载本地页面、放预训练模型
webView.loadUrl("file:///android_asset/www/index.html")
val json = assets.open("config/data.json").bufferedReader().readText()
// raw:适合按 ID 引用的媒体
val sound = resources.openRawResource(R.raw.notification_sound)
Flutter 的 pubspec.yaml 里声明的 assets 会被打进 Android 的 assets/;想在原生层读这些文件,路径要按 assets/flutter_assets/... 规则。
6. 原生库 jniLibs/ 与 ABI 瘦身
1
2
3
4
5
jniLibs/
├── arm64-v8a/ # 主流 64 位 ARM(覆盖绝大多数新设备)
├── armeabi-v7a/ # 旧 32 位 ARM
├── x86/ # 模拟器
└── x86_64/ # 64 位模拟器
.so 通过 JNI 与 Kotlin/Java 交互。默认把所有 ABI 打进包会让 APK 暴涨,优化:
1
2
3
4
5
6
7
8
android {
defaultConfig {
ndk {
// 只保留 64 位 ARM,体积立省一大截
abiFilters += listOf("arm64-v8a")
}
}
}
⚠️ 代价:x86 模拟器跑不了(用 ARM 翻译或真机调试)。所有引入的 .so 必须提供相同 ABI 集合,否则构建失败。Flutter 引擎、RN 桥接、FFmpeg/OpenCV/TFLite 插件本质都是 .so。
7. Gradle 相关文件:构建的”说明书”
| 文件 | 作用 | 注意 |
|---|---|---|
build.gradle.kts | 模块级构建配置 | 最常改 |
settings.gradle.kts | 仓库源 + 包含的 module | AGP 7+ 仓库放这 |
gradle.properties | 全局 Gradle 参数 | org.gradle.jvmargs、android.useAndroidX=true |
gradle-wrapper.properties | 固定 Gradle 版本 | distributionUrl 最关键 |
local.properties | 本机 SDK/NDK 路径 | 含机器路径,必须 gitignore |
.gradle/ | 构建缓存 | 不进版本库,清缓存大法 .gradlew clean |
1
2
# gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
1
2
3
4
# gradle.properties(加速构建)
org.gradle.jvmargs=-Xmx4096m
org.gradle.caching=true
org.gradle.parallel=true
8. 多模块(Multi-module):从”一个 app”到”组件化”
现代项目常拆成多个 Gradle module,靠 settings.gradle.kts 的 include 组织:
1
2
// settings.gradle.kts
include(":app", ":core", ":feature-home")
1
2
3
4
// :core 是库模块,用 library 插件而非 application
plugins { id("com.android.library") }
// :app 依赖它
dependencies { implementation(project(":core")) }
好处:编译并行、按需构建、代码解耦。跨平台项目里,原生能力也值得抽到独立 :native 模块,避免堆在 :app。
其实你每天都在用
- 换应用图标:手动替换
res/mipmap-*/ic_launcher.png,或用flutter_launcher_icons一键生成各密度自适应图标。 - 接原生 SDK:在
src/main/kotlin/加 Kotlin 类、改MainActivity、必要时注册AndroidManifest组件。 - WebView 加载本地页面:把 HTML/JS/CSS 丢进
assets/,loadUrl("file:///android_asset/www/index.html")。 - 用 FFmpeg/OpenCV/TFLite:把对应
.so放进jniLibs/<abi>/,并在 gradle 用abiFilters减负。 - release 崩、debug 正常:
proguard-rules.pro里给反射/序列化类加-keep。 - 多环境切 API 地址:用
productFlavors+src/<flavor>/下发不同config.xml,不碰main。 - 构建卡慢:调
gradle.properties的jvmargs/caching,或./gradlew clean清缓存。 - 加原生模块:抽到独立
:module,include进settings.gradle.kts,implementation(project(...))引用。
常见误解(FAQ)
❌ 误区一:”assets/ 和 res/raw/ 是一回事,随便放”
访问方式和约束完全不同。assets/ 用 AssetManager 按文件路径读、可任意嵌套、不进 R 类,适合 HTML/模型/证书;res/raw/ 用 R.raw.xxx ID 访问、不能建子目录,适合媒体/固定资源。搞混会导致 FileNotFoundException 或找不到 ID。
❌ 误区二:”所有 ABI 都打进包最稳,省得漏设备”
APK 体积会暴涨 2~3 倍,而且绝大多数新设备只用 arm64-v8a。正确做法是 abiFilters += listOf("arm64-v8a") 瘦身;唯一代价是 x86 模拟器跑不了,用真机或 ARM 翻译即可。前提是每个引入的 .so 都提供该 ABI,否则构建失败。
❌ 误区三:”local.properties 提交进 Git 方便协作”
它存的是你本机的 SDK/NDK 绝对路径(如 /Users/xxx/Library/Android/sdk),别人机器上路径不同,提交后只会互相踩。它本就在 .gitignore 里,靠 Android Studio / flutter doctor 自动生成,CI 里单独注入。
❌ 误区四:”改了代码 Gradle 不生效,只能重装 Android Studio”
先试廉价方案:./gradlew clean 清当前构建,或删 ~/.gradle/caches/(全局缓存,慎用)。Gradle 靠输入哈希判断是否重编,增量失败时清缓存是标准动作,远比重装来得快。
❌ 误区五:”Kotlin 文件放 java/ 目录能编过,没问题”
AGP 默认 java/ 和 kotlin/ 都算源码目录,所以能编。但团队不加约定就会乱:Kotlin 放 java/ 下、Java 放 kotlin/ 下,后续混用 Source Set 覆盖、IDE 索引、代码评审都会踩坑。约定俗成:Kotlin 统一放 src/main/kotlin/。
一句话总结
Android 项目结构是一套”位置即语义”的契约:源码进 kotlin/、资源进 res/、原生库进 jniLibs/、构建规则进 gradle 文件;跨平台开发者不必背下每个文件夹,但要明白 Gradle 按源集找文件——放对地方,事倍功半,放错地方,系统当你不存在。