文章

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

深入解析 Android 项目的目录结构与文件作用,涵盖源码目录、资源目录、Gradle 配置、NDK 集成等,帮助跨平台开发者理解项目每一层的含义。

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

一句话概括

Android 项目结构定义了代码、资源、构建配置和原生库的组织方式,跨平台开发者理解它就能在集成插件和解决构建问题时精准定位、快速处理。

背景与意义

从一个常见的困惑开始

很多 Flutter 或 React Native 开发者第一次打开 Android 项目目录时,会有种”信息过载”的感觉。一个简单的 flutter create 命令生成的 Android 目录下,包含了大量的文件和文件夹:srcresassetsjniLibsgradle 目录,以及各种 .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 年前):使用 eclipse IDE,项目结构松散
  • 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.javaMainApplication.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:颜色值定义,命名规范如 colorPrimarycolorAccent
  • themes.xml:主题定义,控制应用的全局样式
  • dimens.xml:尺寸值,便于统一调整间距和大小
  • styles.xml:样式定义,可以组合多个属性

多语言支持:在 values-zhvalues-javalues-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.xxx ID 访问
  • 子目录结构固定,不能嵌套
  • Android 编译时会对文件做 URI 索引
  • 适合存放多媒体文件和需要 ID 引用的资源

使用方式(Java/Kotlin):

1
InputStream is = getResources().openRawResource(R.raw.notification_sound);

关键区别总结

特性assetsraw
访问方式AssetManagerR.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(...);
}

混淆的工作原理

混淆分为三个层面:

  1. 压缩(Shrinking):移除未被使用的代码和资源
  2. 优化(Optimization):内联方法、简化表达式等
  3. 混淆(Obfuscation):重命名类、方法和变量名为短名称

跨平台混淆问题

最大的问题是反射。Flutter 和 RN 的插件经常使用 Java 反射来动态调用方法,如果这些方法被混淆器重命名,就会运行时崩溃。

典型表现为:

  • Release 版本崩溃,Debug 版本正常
  • 报错信息包含 ClassNotFoundExceptionNoSuchMethodException
  • 崩溃发生在调用某个第三方 SDK 的时候

解决方案

  1. proguard-rules.pro 中添加 Keep 规则,保留相关的类和成员
  2. 使用 -keepattributes 保留注解、签名等信息
  3. 在 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 应用设计新的启动图标。

手动方式

  1. 准备好各密度的 PNG 图片
  2. 替换 android/app/src/main/res/mipmap-*/ 中的 ic_launcher.png
  3. 同时替换 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++ 原生库。

  1. 将编译好的 .so 文件放入 android/app/src/main/jniLibs/arm64-v8a/ 等目录
  2. 编写 JNI 桥接 Java 类
  3. 在 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 资源。

  1. 将 HTML/JS/CSS 文件放入 android/app/src/main/assets/ 目录
  2. 在原生代码中加载:
    1
    
    webView.loadUrl("file:///android_asset/www/index.html");
    

常见问题

Q1: Merge 资源冲突

症状:编译报错 Error: Duplicate resourcesMerging of manifest files failed

原因:不同的依赖库包含了同名的资源文件(如 R.drawable.ic_launcher)。

解决

  1. build.gradle.kts 中配置资源合并策略:
    1
    2
    3
    4
    5
    6
    7
    
    android {
     packaging {
         resources {
             excludes += "/META-INF/{AL2.0,LGPL2.1}"
         }
     }
    }
    
  2. 使用 tools:replacetools:remove

Q2: NDK 相关报错

症状No rule to make targetUnsupported ABI

解决

  1. 确认所有 .so 文件提供了相同的 ABI 集合
  2. 检查 local.properties 中的 NDK 路径是否有效
  3. build.gradle.kts 中明确 abiFilters

Q3: 应用图标未更新

症状:更换图标后,桌面图标仍然显示旧图标。

原因:系统缓存了启动器图标,或替换的图标密度不完整。

解决

  1. 完全卸载应用后重新安装
  2. 确保所有 mipmap 目录都更新了图标
  3. 清除启动器缓存(在系统设置中找到”存储”→”其他应用”→”启动器”→”清除缓存”)

Q4: assets 资源读取失败

症状:运行时 AssetManager.open() 抛出 FileNotFoundException

可能原因

  1. 文件名路径大小写错误(Android 区分大小写)
  2. 文件放在了 res/raw/ 而不是 assets/
  3. 文件未在构建时正确打包(检查 APK 中的 assets 目录)

总结

Android 项目结构是一个有机的整体,每一层都有其明确的目的:

  • src/main/java/ 是 Java/Kotlin 代码的家,也是跨平台应用原生模块的编写位置
  • src/main/res/ 管理所有非代码资源,从图标到字符串到主题样式的统一管理
  • src/main/assets/raw/ 存放原始文件,但访问方式和用途截然不同
  • src/main/jniLibs/ 是 C/C++ 原生库的集结地,决定了哪些设备可以运行你的应用
  • Gradle 相关文件build.gradle.ktssettings.gradle.ktsgradle-wrapper.properties)定义了构建的输入和规则
  • local.properties 是本地环境的身份证明
  • proguard-rules.pro 是应用保险箱的密码本
  • build.gradle.kts 是整个构建中心的指挥官

当你面对 Android 项目,不再因为”这文件是干什么的”而困惑时,你就已经从”这个目录黑箱”的心态中走出来了。无论是集成新的 Flutter 插件、配置 RN 的原生模块,还是排查构建错误,理解项目结构都是最基础也是最关键的第一步。

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