Android 项目结构解析——跨平台开发者必须了解的项目骨架
深入解析 Android 项目的目录结构与文件作用,涵盖源码目录、资源目录、Gradle 配置、NDK 集成等,帮助跨平台开发者理解项目每一层的含义。
一句话概括
Android 项目结构定义了代码、资源、构建配置和原生库的组织方式,跨平台开发者理解它就能在集成插件和解决构建问题时精准定位、快速处理。
背景与意义
从一个常见的困惑开始
很多 Flutter 或 React Native 开发者第一次打开 Android 项目目录时,会有种”信息过载”的感觉。一个简单的 flutter create 命令生成的 Android 目录下,包含了大量的文件和文件夹:src、res、assets、jniLibs、gradle 目录,以及各种 .gradle、.properties、.pro 文件。
当你需要做以下操作时,理解项目结构就不是”锦上添花”,而是必要条件:
- 替换应用图标和启动画面
- 集成第三方原生 SDK(推送、支付、地图)
- 引入 C/C++ 本地库(如 OpenCV、FFmpeg)
- 排查构建失败的原因
- 配置代码混淆规则
Android 项目 vs 跨平台项目的目录关系
在 Flutter 项目中,Android 原生部分位于 android/ 目录下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
flutter_project/
├── android/
│ ├── app/
│ │ ├── src/
│ │ ├── build.gradle.kts
│ │ └── proguard-rules.pro
│ ├── gradle/
│ ├── build.gradle.kts
│ ├── settings.gradle.kts
│ ├── gradle.properties
│ ├── local.properties
│ └── gradlew (与 gradlew.bat)
├── ios/
├── lib/
└── pubspec.yaml
在 React Native 项目中,Android 目录结构类似,位置在 android/ 下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
rn_project/
├── android/
│ ├── app/
│ │ ├── src/
│ │ │ ├── main/
│ │ │ │ ├── java/
│ │ │ │ ├── res/
│ │ │ │ ├── assets/
│ │ │ │ └── AndroidManifest.xml
│ │ │ ├── debug/
│ │ │ └── release/
│ │ ├── build.gradle.kts
│ │ └── proguard-rules.pro
│ ├── gradle/
│ ├── build.gradle.kts
│ └── settings.gradle.kts
...
无论使用哪种跨平台框架,Android 原生部分的基本骨架是一致的。理解了这个骨架,你就能在任何跨平台项目的原生目录中自如操作。
Android 项目发展简史
Android 项目结构经历了几个重要阶段:
- Eclipse ADT 时代(2013 年前):使用
eclipseIDE,项目结构松散 - Android Studio + Gradle 时代(2013-至今):标准化了基于 Gradle 的构建体系
- Android Gradle Plugin 3.0+(2017+):引入
implementation/api依赖机制 - AGP 7.0+(2021+):要求 Gradle 7.0+,Java 11+
- AGP 8.0+(2023+):要求 Java 17,默认使用 Kotlin DSL
当前,AGP 8.x 是主流,构建脚本的 Kotlin DSL(.kts)已经成为新项目的默认选择。
核心知识点拆解
app/src/main/java——源码目录
这是 Android 应用的核心 Java/Kotlin 源码存放位置。
1
2
3
4
5
6
7
8
9
10
app/src/main/java/com/example/myapp/
├── MainActivity.java # 或 MainActivity.kt
├── MyApplication.java # 自定义 Application 类
├── ui/
│ ├── MainScreen.java
│ └── SettingsScreen.java
├── service/
│ └── BackgroundService.java
└── utils/
└── PermissionHelper.java
包名目录结构
java/ 目录下的子目录格式遵循 Java 包名约定。包名 com.example.myapp 对应目录路径 com/example/myapp/。
跨平台开发者须知:
- Flutter 项目的 Java 源码通常只有一个
MainActivity.java(或 Kotlin 版本) - RN 项目的 Java 源码包含
MainActivity.java和MainApplication.java - 当你集成原生插件时,插件的源码会在编译时自动引入,不需要手动放到这个目录下
- 在
src/main/java/下添加自定义的 Java/Kotlin 类是完全允许的,并且是在 Flutter/RN 中编写原生模块的标准做法
MainActivity.java 的角色
对于 Flutter 项目,MainActivity 是 Flutter 引擎的宿主容器:
1
2
3
4
5
6
7
8
package com.example.myapp;
import io.flutter.embedding.android.FlutterActivity;
public class MainActivity extends FlutterActivity {
// Flutter 会自动处理完整的生命周期和渲染
// 通常不需要修改这个文件
}
对于 RN 项目,MainActivity 是 React Native 的容器:
1
2
3
4
5
6
7
8
9
package com.example.myapp;
import com.facebook.react.ReactActivity;
public class MainActivity extends ReactActivity {
@Override
protected String getMainComponentName() {
return "MyRNApp";
}
}
app/src/main/res——资源目录
res/(resources 的缩写)存放所有非代码类的资源文件。Android 系统会根据不同的设备配置(屏幕密度、语言、系统版本等)使用不同版本的资源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
app/src/main/res/
├── drawable/ # 位图或 XML 矢量图
│ ├── ic_launcher_background.xml
│ ├── ic_logo.png
│ └── shape_button_rounded.xml
├── drawable-v24/ # API 24+ 才使用的资源
│ └── ic_animated.xml
├── layout/ # 界面布局 XML
│ ├── activity_main.xml
│ └── notification_layout.xml
├── mipmap-hdpi/ # 高密度图标(48×48px)
│ └── ic_launcher.png
├── mipmap-mdpi/ # 中密度图标(32×32px)
├── mipmap-xhdpi/ # 超高密度(64×64px)
├── mipmap-xxhdpi/ # 超超高密度(96×96px)
├── mipmap-xxxhdpi/ # 极超高密度(144×144px)
├── values/ # 常量值定义
│ ├── strings.xml # 字符串
│ ├── colors.xml # 颜色值
│ ├── themes.xml # 主题
│ └── dimens.xml # 尺寸值
├── values-zh/ # 中文语言资源
│ └── strings.xml
├── xml/ # XML 配置文件
│ ├── network_security_config.xml # 网络安全配置
│ └── backup_rules.xml # 备份规则
├── raw/ # 原始文件(不经压缩)
│ ├── notification_sound.ogg
│ └── app_config.json
└── font/ # 自定义字体(API 26+)
└── custom_font.ttf
drawable 目录
drawable/ 存放所有可绘制的资源,包括:
- 位图文件(
.png、.jpg、.gif、.webp) - 九宫格图片(
.9.png),用于自适应拉伸的背景图 - XML 矢量图形(
<vector>标签定义的 SVG 图形) - Shape Drawable(
<shape>标签定义的图形,如圆角矩形) - Selector Drawable(
<selector>标签定义的状态列表,如按钮按下、抬起状态)
不同屏幕密度的位图放在对应的后缀目录中:
mdpi:~160dpi(基线密度,1×)hdpi:~240dpi(1.5×)xhdpi:~320dpi(2×)xxhdpi:~480dpi(3×)xxxhdpi:~640dpi(4×)
跨平台技巧:Flutter 项目建议使用”自适应图标”(Adaptive Icon),它会自动适配不同设备和系统版本。使用 flutter_launcher_icons 工具可以自动生成所有密度版本。
mipmap 目录
mipmap/ 专门存放应用图标。它与 drawable/ 的区别在于:
- Android 启动器(Launcher)会使用最高密度的
mipmap资源来显示图标,然后在运行时按比例缩放 - 如果图标放在
drawable/中,某些启动器在缩放时可能产生伪影 - 因此,应用图标(通常是
ic_launcher.png)一定放在mipmap/,其他图片资源放在drawable/
values 目录
定义各种常量值,通常包含多个 XML 文件:
strings.xml:字符串资源,支持多语言本地化colors.xml:颜色值定义,命名规范如colorPrimary、colorAccentthemes.xml:主题定义,控制应用的全局样式dimens.xml:尺寸值,便于统一调整间距和大小styles.xml:样式定义,可以组合多个属性
多语言支持:在 values-zh、values-ja、values-fr 等目录下放置对应语言的 strings.xml,Android 系统会自动选择。
colors.xml 示例:
1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#6200EE</color>
<color name="colorPrimaryDark">#3700B3</color>
<color name="colorAccent">#03DAC5</color>
<color name="white">#FFFFFF</color>
<color name="black">#000000</color>
</resources>
layout 目录
layout/ 存放界面布局的 XML 文件,定义 UI 组件的结构和排列。每个 Activity 或 Fragment 通常对应一个布局文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/titleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World" />
<Button
android:id="@+id/actionButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="点击" />
</LinearLayout>
跨平台焦点:Flutter 和 RN 使用自己的声明式 UI 框架,不直接使用 Android 的 XML 布局。但以下场景需要理解 layout/:
- 集成某些推送 SDK 时,需要添加自定义布局作为透明 Activity
- 配置闪屏(Splash Screen)时需要修改主题指向的布局文件
- 有些 Native 插件在调用原生 Activity 时会加载布局文件
xml 目录
xml/ 存放任意 XML 配置文件,Android 系统在运行时会读取这些文件。常见用途:
network_security_config.xml:网络安全策略配置backup_rules.xml:指定哪些数据可以在 Google Drive 上备份shortcuts.xml:桌面快捷方式配置file_paths.xml:FileProvider 的文件路径配置
网络配置示例(调试时需要允许明文流量):
1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">localhost</domain>
</domain-config>
</network-security-config>
这个文件在 AndroidManifest.xml 中引用:
1
2
3
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
app/src/main/assets 与 raw 区别
这两个目录都用于存放原始文件,但它们有本质的不同。
assets 目录
- 资源通过
AssetManager以文件流形式访问 - 子目录结构自由,可以任意嵌套
- 资源引用不使用 R 文件 ID
- 适合存放较大的二进制文件或结构化数据
使用方式(Java/Kotlin):
1
2
AssetManager am = getAssets();
InputStream is = am.open("config/data.json");
跨平台常用场景:
- Flutter 插件将预训练的机器学习模型放在 assets 中
- RN 的
react-native-fs可以读取 assets 目录中的文件 - 自签名的 SSL 证书(
assets/certificate.pem) - WebView 加载的本地 HTML/JS 文件
raw 目录
- 资源通过
R.raw.xxxID 访问 - 子目录结构固定,不能嵌套
- Android 编译时会对文件做 URI 索引
- 适合存放多媒体文件和需要 ID 引用的资源
使用方式(Java/Kotlin):
1
InputStream is = getResources().openRawResource(R.raw.notification_sound);
关键区别总结
| 特性 | assets | raw |
|---|---|---|
| 访问方式 | AssetManager | R.raw.xx ID |
| 子目录 | 任意嵌套 | 不支持 |
| 索引方式 | 文件路径 | 资源 ID |
| 压缩 | 按文件类型 | 部分类型压缩 |
| 典型用途 | HTML/JS/模型/证书 | 音频/视频/JSON |
跨平台执行建议:在 Flutter 项目中,如果通过 flutter: assets: 声明资源文件,Flutter 会自动将其打包到 Android 的 assets 目录中。如果你想直接在原生层访问这些资源(比如在 Java 代码中读取 assets/flutter_assets/ 下的文件),需要理解 assets 的路径规则。
app/src/main/jniLibs——原生库目录
jniLibs/ 存放使用 C/C++ 编写的原生库(Native Library),即 .so 文件。这些库通过 JNI(Java Native Interface)与 Java/Kotlin 代码交互。
1
2
3
4
5
6
7
8
9
app/src/main/jniLibs/
├── arm64-v8a/
│ └── libnative_lib.so # 64 位 ARM 设备
├── armeabi-v7a/
│ └── libnative_lib.so # 32 位 ARM 设备
├── x86/
│ └── libnative_lib.so # x86 模拟器
├── x86_64/
│ └── libnative_lib.so # x86_64 模拟器
ABI(Application Binary Interface)
Android 支持多种 CPU 架构,每种架构对应一种 ABI:
- arm64-v8a:当前主流设备(覆盖超过 95% 的新设备),64 位 ARM 架构
- armeabi-v7a:较旧设备的 32 位 ARM 架构
- x86/x86_64:模拟器和少数 Intel 设备
打包优化
默认情况下,Gradle 会将所有 ABI 版本的 .so 文件打包到 APK 中,导致 APK 体积剧增。优化方案:
1
2
3
4
5
6
7
8
android {
defaultConfig {
ndk {
// 只保留 arm64-v8a,舍弃其他 ABI
abiFilters += listOf("arm64-v8a")
}
}
}
这样 APK 体积可以减少 60% 以上,但代价是 x86 模拟器上无法运行(需要使用 ARM 翻译或选择其他调试方式)。
跨平台场景
- Flutter: Flutter 引擎本身就是原生库,编译后的
.so文件放在jniLibs中 - RN: React Native 的 Native 桥接也是通过
.so实现的 - 插件: 使用 FFmpeg、OpenCV、TFLite 等 C/C++ 库的插件需要提供对应 ABI 的
.so文件
注意事项:如果你的应用使用 native 库,且所有引入的库都提供相同 ABI 版本,Gradle 才能正确构建。一个库只提供了 armeabi-v7a 版本而你的编译目标只包括 arm64-v8a,就会导致构建失败。
Gradle Wrapper(gradle-wrapper.properties)
Gradle Wrapper 是 Android 项目的标配,它确保所有开发者使用相同版本的 Gradle,避免”在我机器上能编译”的问题。
文件位置:gradle/wrapper/gradle-wrapper.properties
1
2
3
4
5
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
关键参数
distributionUrl:指定要下载的 Gradle 版本。这是最重要的参数。- Gradle 版本与 AGP 版本必须匹配,否则构建会失败。
跨平台维护:
- Flutter 项目由 Flutter SDK 自动管理
gradle-wrapper.properties中的版本号 - 贸然修改 Gradle 版本可能导致与 Flutter 的兼容性问题
- 需要升级 Gradle 时,建议先检查 Flutter 官方支持的版本范围
手动更新 Gradle Wrapper:
1
./gradlew wrapper --gradle-version 8.3
这会自动下载新版本并更新 gradle-wrapper.properties。
local.properties——本地环境配置
local.properties 定义了 Android 构建所需的本地环境路径,通常包含 SDK 和 NDK 的路径。
1
2
sdk.dir=/Users/username/Library/Android/sdk
ndk.dir=/Users/username/Library/Android/sdk/ndk/26.1.10909125
配置要点
sdk.dir:Android SDK 的安装路径。Android Studio 安装时会自动创建。ndk.dir:Android NDK 的路径。仅在使用原生 C/C++ 代码时需要。- 该文件由 Android Studio 自动生成,不应被版本控制(已在
.gitignore中) - 不同开发者的
local.properties内容不同,取决于 SDK 安装位置
跨平台常见问题:
- Flutter 的
flutter doctor命令实际上就是检测local.properties中配置的 SDK 路径是否有效 - 在某些 CI/CD 环境中,需要确保
local.properties中存在合法的 SDK 路径 - 在搭载 M 系列芯片的 Mac 上,需要确保 NDK 版本 >= 23 以支持 ARM64 架构
proguard-rules.pro——混淆规则
proguard-rules.pro 定义了代码混淆和压缩的规则。混淆可以减小 APK 体积、提升性能,并增加逆向难度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# ---------- 基本规则 ----------
# 保留所有使用了 @Keep 注解的类和成员
-keep @androidx.annotation.Keep class * { *; }
# 保留数据模型类(用于 JSON 序列化/反序列化)
-keep class com.example.myapp.models.** { *; }
# ---------- Flutter 特定规则 ----------
# Flutter 引擎使用了很多反射,需要保留
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
# ---------- React Native 特定规则 ----------
-keep class com.facebook.react.** { *; }
-keep class com.facebook.hermes.** { *; }
# ---------- 第三方 SDK 规则 ----------
# Firebase
-keep class com.google.firebase.** { *; }
# ---------- 日志 ----------
# Release 版本中自动移除 Log 调用
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int d(...);
public static int i(...);
}
混淆的工作原理
混淆分为三个层面:
- 压缩(Shrinking):移除未被使用的代码和资源
- 优化(Optimization):内联方法、简化表达式等
- 混淆(Obfuscation):重命名类、方法和变量名为短名称
跨平台混淆问题
最大的问题是反射。Flutter 和 RN 的插件经常使用 Java 反射来动态调用方法,如果这些方法被混淆器重命名,就会运行时崩溃。
典型表现为:
- Release 版本崩溃,Debug 版本正常
- 报错信息包含
ClassNotFoundException或NoSuchMethodException - 崩溃发生在调用某个第三方 SDK 的时候
解决方案:
- 在
proguard-rules.pro中添加 Keep 规则,保留相关的类和成员 - 使用
-keepattributes保留注解、签名等信息 - 在 CI 中构建 Release APK 后立刻做基础的功能验证
.gradle/ 缓存目录
.gradle/ 是 Gradle 的缓存目录,在项目根目录可见。它包含:
- 下载的 Gradle 版本
- 依赖缓存(第三方库的 jar、aar 文件)
- 构建缓存(编译过的中间产物)
- 项目特定的缓存数据
缓存清除
当遇到诡异的构建问题时,”清缓存”是一种常见(且有效😅)的解决方案:
1
2
3
4
5
6
7
8
9
10
# 清空当前项目的构建缓存
./gradlew clean
# 清空 Gradle 全局缓存(慎用)
rm -rf ~/.gradle/caches/
# Flutter 项目中更彻底的清理
flutter clean
cd android
./gradlew clean
但是:
- 过度清缓存会浪费大量重编译时间
- 建议先尝试
./gradlew clean,不行再考虑全局缓存 .gradle/目录不应提交到版本控制(已在.gitignore中)
构建缓存原理
Gradle 的构建缓存通过对比输入文件的哈希值来判断是否需要重新编译。对于增量构建来说:
- 如果源文件未变,直接使用缓存的编译结果
- 如果依赖库版本未变,不会重新下载
- 如果 Gradle 守护进程(Daemon)仍在运行,第二次构建会快得多
实战案例
案例一:Flutter 项目更换应用图标
场景:为 Flutter 应用设计新的启动图标。
手动方式:
- 准备好各密度的 PNG 图片
- 替换
android/app/src/main/res/mipmap-*/中的ic_launcher.png - 同时替换
ic_launcher_round.png(圆形图标版本)
自动化方式(推荐): 在 pubspec.yaml 中添加:
1
2
3
4
5
6
7
8
9
dev_dependencies:
flutter_launcher_icons: "^0.13.1"
flutter_launcher_icons:
android: true
ios: true
image_path: "assets/icons/app-icon.png"
adaptive_icon_background: "#FFFFFF"
adaptive_icon_foreground: "assets/icons/app-icon-foreground.png"
运行:
1
2
flutter pub get
flutter pub run flutter_launcher_icons
这会自动生成所有密度的图标文件并放到正确的 mipmap 目录中。
案例二:RN 项目接入原生日志库
场景:在 RN 项目中通过 jniLibs 引入自定义的 C/C++ 原生库。
- 将编译好的
.so文件放入android/app/src/main/jniLibs/arm64-v8a/等目录 - 编写 JNI 桥接 Java 类
- 在 JS 端通过 NativeModules 调用
1
2
3
4
5
6
7
8
9
10
// NativeLogBridge.java
package com.example.myapp;
public class NativeLogBridge {
static {
System.loadLibrary("native_log");
}
public static native void writeLog(String message, int level);
}
案例三:修改 WebView 加载的本地资源
场景:在跨平台应用中使用 WebView 加载本地 HTML 文件,并加载 JavaScript 资源。
- 将 HTML/JS/CSS 文件放入
android/app/src/main/assets/目录 - 在原生代码中加载:
1
webView.loadUrl("file:///android_asset/www/index.html");
常见问题
Q1: Merge 资源冲突
症状:编译报错 Error: Duplicate resources 或 Merging of manifest files failed。
原因:不同的依赖库包含了同名的资源文件(如 R.drawable.ic_launcher)。
解决:
- 在
build.gradle.kts中配置资源合并策略:1 2 3 4 5 6 7
android { packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } }
- 使用
tools:replace或tools:remove
Q2: NDK 相关报错
症状:No rule to make target 或 Unsupported ABI。
解决:
- 确认所有
.so文件提供了相同的 ABI 集合 - 检查
local.properties中的 NDK 路径是否有效 - 在
build.gradle.kts中明确abiFilters
Q3: 应用图标未更新
症状:更换图标后,桌面图标仍然显示旧图标。
原因:系统缓存了启动器图标,或替换的图标密度不完整。
解决:
- 完全卸载应用后重新安装
- 确保所有 mipmap 目录都更新了图标
- 清除启动器缓存(在系统设置中找到”存储”→”其他应用”→”启动器”→”清除缓存”)
Q4: assets 资源读取失败
症状:运行时 AssetManager.open() 抛出 FileNotFoundException。
可能原因:
- 文件名路径大小写错误(Android 区分大小写)
- 文件放在了
res/raw/而不是assets/ - 文件未在构建时正确打包(检查 APK 中的 assets 目录)
总结
Android 项目结构是一个有机的整体,每一层都有其明确的目的:
src/main/java/是 Java/Kotlin 代码的家,也是跨平台应用原生模块的编写位置src/main/res/管理所有非代码资源,从图标到字符串到主题样式的统一管理src/main/assets/和raw/存放原始文件,但访问方式和用途截然不同src/main/jniLibs/是 C/C++ 原生库的集结地,决定了哪些设备可以运行你的应用- Gradle 相关文件(
build.gradle.kts、settings.gradle.kts、gradle-wrapper.properties)定义了构建的输入和规则 local.properties是本地环境的身份证明proguard-rules.pro是应用保险箱的密码本build.gradle.kts是整个构建中心的指挥官
当你面对 Android 项目,不再因为”这文件是干什么的”而困惑时,你就已经从”这个目录黑箱”的心态中走出来了。无论是集成新的 Flutter 插件、配置 RN 的原生模块,还是排查构建错误,理解项目结构都是最基础也是最关键的第一步。