文章

Android 项目结构解析——跨平台开发者必须了解的项目骨架

Android 项目用一套固定的目录骨架组织代码、资源、原生库与构建配置; 跨平台项目里所有原生能力(插件、SDK、.so)最终都落进这套结构,看懂它才能精准定位、快速排错。

Android 项目结构解析——跨平台开发者必须了解的项目骨架

一句话概括

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仓库源 + 包含的 moduleAGP 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 按源集找文件——放对地方,事倍功半,放错地方,系统当你不存在。

本文由作者按照 CC BY 4.0 进行授权