A
第 3 章JAVA30 分钟

项目结构解析

逐层剖析 Android 项目结构:app 模块、Manifest、res 目录、Gradle 脚本、签名配置与 ProGuard 规则

学习目标

  • 理解 app 模块与库模块的差异,能在多模块项目中定位 app 级 build.gradle
  • 逐行读懂 AndroidManifest.xml 中的 application、activity、intent-filter 与权限声明
  • 区分 res/layout、values、drawable、mipmap 的职责与多限定符(如 values-night、mipmap-hdpi)
  • 看懂项目级与模块级 build.gradle、settings.gradle、gradle.properties 的协作关系
  • 了解签名配置、proguard-rules.pro 与 multiDex 的基本用途

学习目标

第 2 章我们用模板跑出了 Hello World,但模板“帮我们填了什么”还没说清。本章把项目结构拆开看:从根目录的 settings.gradle 一路到 proguard-rules.pro,把“配置即文档”读透。目标:

  • 理解 app 模块的本质(application 类型)与 library 模块(com.android.library)的差异;
  • 读懂 AndroidManifest.xml 中 application 与 activity 节点及 intent-filter 的语义;
  • 能在 res/ 下正确定位并新增 layout/values/drawable/mipmap 资源,理解多限定符目录;
  • 看懂项目级与模块级 build.gradle、settings.gradle、gradle.properties 三者协作;
  • 了解 release 构建时用到的签名配置、proguard-rules.pro 与 multidex 的基本用途。

app 模块结构

1. 模块类型

Android Studio 的“项目”由若干 模块(Module) 组成。常见的两类:

类型 plugin 产物 用途
Application com.android.application APK / AAB 可独立安装的应用
Library com.android.library AAR 被其他模块依赖的代码库

本教程的单模块项目只有 app 一个 application 模块。多模块项目通常会拆出 core、feature-home、feature-detail 等多个 library 模块,由 settings.gradle 聚合:

// settings.gradle
include ':app', ':core', ':feature-home', ':feature-detail'

app 模块依赖其它 library 模块:

// app/build.gradle
dependencies {
    implementation project(':core')
    implementation project(':feature-home')
}

2. app 模块目录

app/
├── build.gradle               # 模块级构建脚本
├── proguard-rules.pro         # 混淆规则
├── consumer-rules.pro         # AAR 消费者规则(仅 library 有)
└── src/
    ├── main/
    │   ├── AndroidManifest.xml
    │   ├── java/com/example/helloapp/
    │   │   └── MainActivity.java
    │   └── res/...
    ├── androidTest/java/...   # 设备端集成测试
    └── test/java/...          # JVM 单元测试

src/main/ 是发版的源集,src/debug/ 与 src/release/ 可分别存放不同构建变体的代码与资源,AGP 会自动合并。

3. 命名空间与 applicationId 的区别

  • namespace(build.gradle 中的 android { namespace '...' }):决定生成 R 类、BuildConfig 类的包名,是源码层的包结构。
  • applicationId(defaultConfig 中):决定 APK 在系统中的唯一标识,等同 AndroidManifest 旧版的 package 属性。

模板中两者常常相同,但生产环境可不同:例如发布免费版 com.example.app.free、付费版 com.example.app.pro,但 namespace 都用 com.example.app。

AndroidManifest.xml

1. 完整模板

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- 权限 -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <!-- 应用级硬件要求 -->
    <uses-feature android:name="android.hardware.camera" android:required="false" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.HelloApp"
        android:name=".App"> <!-- 继承 Application 的入口类 -->

        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="standard"
            android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity
            android:name=".DetailActivity"
            android:exported="false"
            android:parentActivityName=".MainActivity" />

    </application>
</manifest>

2. 节点说明

节点 作用
<uses-permission> 申请系统权限,运行时危险权限还需在代码中请求
<uses-feature> 声明硬件特性,影响 Google Play 可见性
<application> 全局配置,name 指定自定义 Application 子类
<activity> 注册 Activity;exported=true 才能被其它应用启动
<intent-filter> 声明该 Activity 响应的 Intent,MAIN+LAUNCHER 组合即“桌面图标入口”

3. 自定义 Application 类

模板默认不生成 Application 子类,全局初始化需要时手动添加:

package com.example.helloapp;

import android.app.Application;
import android.util.Log;

public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d("HelloApp", "App onCreate");
    }
}

然后在 manifest 中 application 节点加 android:name=".App"。Application#onCreate 在主进程启动时被调用,且 只在进程创建时一次,适合放全局 SDK 初始化、日志库初始化等。

不要在 Application#onCreate 同步做重活(如读取大文件、网络请求),会导致冷启动变慢。复杂初始化用 App Startup 库做按需懒加载。

4. 自定义权限与 exported 行为

Android 12(API 31)起,所有带 intent-filter 的组件必须显式声明 android:exported,否则安装失败。仅本 App 内调用的 Activity 设 exported="false";要被其它 App 通过 Intent 唤起的 Activity 设 true 并加权限保护:

<activity
    android:name=".ShareActivity"
    android:exported="true"
    android:permission="com.example.helloapp.permission.SHARE">
    <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="text/plain" />
    </intent-filter>
</activity>

对应自定义权限要在 manifest 中声明:

<permission
    android:name="com.example.helloapp.permission.SHARE"
    android:protectionLevel="signature" />

res 目录详解

1. 资源分类总览

res/
├── drawable/                 # 矢量图、PNG、shape XML
├── layout/                   # 布局 XML
├── values/                   # 字符串、颜色、尺寸、样式
│   ├── strings.xml
│   ├── colors.xml
│   ├── dimens.xml
│   ├── themes.xml
│   └── styles.xml
├── mipmap-anydpi-v26/        # API 26+ 自适应图标
├── mipmap-hdpi/              # 屏幕密度限定符
├── mipmap-mdpi/
├── mipmap-xhdpi/
├── mipmap-xxhdpi/
├── mipmap-xxxhdpi/
├── xml/                      # 备份规则、路径配置等
├── font/                     # 字体
└── raw/                      # 原始资源(mp3、json 等)

AGP 在编译时把所有资源编译进 APK 的 resources.arsc,并为每个资源生成唯一 ID,由 R 类暴露:

int layoutId = R.layout.activity_main;       // 0x7f030000
int stringId = R.string.app_name;            // 0x7f0e0000
int colorId  = R.color.purple_500;           // 0x7f050000

2. layout 资源

res/layout/activity_main.xml 是布局,运行时通过 setContentView(R.layout.activity_main) 加载。AGP 会解析 XML 并实例化 View 树。同一布局可按屏幕尺寸限定符拆分:

res/
├── layout/main.xml             # 默认
├── layout-land/main.xml        # 横屏
├── layout-w600dp/main.xml      # 平板宽度 ≥ 600dp
└── layout-v29/main.xml         # API 29+ 走这个版本

匹配规则:限定符越多越优先,且按“版本 > 屏幕尺寸 > 屏幕方向”等顺序逐层筛选。

3. values 资源

strings.xml:

<resources>
    <string name="app_name">HelloApp</string>
    <string name="greeting">你好,%1$s!</string>
</resources>

代码中带格式化参数的用法:

String text = getString(R.string.greeting, "Android");

colors.xml:

<resources>
    <color name="purple_200">#FFBB86FC</color>
    <color name="purple_500">#FF6200EE</color>
</resources>

dimens.xml:

<resources>
    <dimen name="margin_normal">16dp</dimen>
</resources>

themes.xml 与 styles.xml:

<resources xmlns:tools="http://schemas.android.com/tools">
    <style name="Theme.HelloApp" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        <item name="colorPrimary">@color/purple_500</item>
        <item name="colorPrimaryVariant">@color/purple_200</item>
        <item name="colorOnPrimary">@color/white</item>
    </style>
</resources>

4. 多限定符示例:夜间模式与多语言

夜间模式只需新建 values-night/:

res/
├── values/colors.xml
├── values-night/colors.xml       # 系统切到深色模式时被选中
├── values/strings.xml
├── values-en/strings.xml         # 系统语言为英语时
└── values-zh-rTW/strings.xml     # 繁体中文(台湾)

values-night/colors.xml:

<resources>
    <color name="background">#FF000000</color>
</resources>

values/colors.xml:

<resources>
    <color name="background">#FFFFFFFF</color>
</resources>

引用方式不变:@color/background,框架会根据当前 Configuration 选对应目录。

5. drawable 与 mipmap 区别

  • drawable/:放普通图片、矢量图、shape XML、selector;
  • mipmap/:仅用于启动器图标,系统可在不同密度 Launcher 中选择最优资源,不参与运行时 density 限定符匹配。

启动器图标自 API 26 起推荐自适应图标(adaptive icon),由前景、背景两层组成:

<!-- res/mipmap-anydpi-v26/ic_launcher.xml -->
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@color/ic_launcher_background" />
    <foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

build.gradle(app 级)

完整模板(注释版):

plugins {
    id 'com.android.application'
}

android {
    namespace 'com.example.helloapp'
    compileSdk 34              // 编译用 SDK 版本
    buildToolsVersion "34.0.0" // 可省,默认取最新

    defaultConfig {
        applicationId "com.example.helloapp"
        minSdk 24
        targetSdk 34
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables.useSupportLibrary = true
    }

    buildTypes {
        debug {
            // 默认 debug,可覆盖 applicationId 后缀
            applicationIdSuffix ".debug"
            debuggable true
            minifyEnabled false
        }
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
    }

    flavorDimensions += "channel"
    productFlavors {
        google { dimension "channel" }
        huawei { dimension "channel" }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
    }

    buildFeatures {
        viewBinding true      // 启用 ViewBinding
        buildConfig true      // 启用 BuildConfig 类生成
    }

    packagingOptions {
        resources {
            excludes += ['META-INF/*.kotlin_module']
        }
    }
}

dependencies {
    // API:依赖会传递给上游(库被其它模块用时)
    // implementation:依赖不传递,仅当前模块可见(推荐)
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.11.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'

    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

关键点:

  • compileSdk 34 决定编译器能引用哪些 API;
  • targetSdk 34 决定系统是否对老 API 行为做兼容;
  • minSdk 24 决定能装在哪些设备上;
  • Build Variants = productFlavors × buildTypes,本例生成 googleDebug、googleRelease、huaweiDebug、huaweiRelease 四个变体。

settings.gradle

pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}
rootProject.name = "HelloApp"
include ':app'
  • pluginManagement 决定 Gradle 从哪下插件;
  • dependencyResolutionManagement 决定依赖仓库,FAIL_ON_PROJECT_REPOS 表示禁止模块级 repositories {},统一收敛;
  • include 列出所有模块。

gradle.properties

# JVM 内存
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8

# 并行构建
org.gradle.parallel=true

# 缓存
org.gradle.caching=true

# 配置按需
org.gradle.configureondemand=false

# AndroidX
android.useAndroidX=true
android.nonTransitiveRClass=true

# Kotlin
kotlin.code.style=official
  • android.useAndroidX=true 强制用 AndroidX,否则会出现 support 库冲突;
  • android.nonTransitiveRClass=true 自 AGP 8 起默认,每个模块只引用自己的 R,减少资源冲突。

签名配置与 proguard-rules.pro

1. 签名配置

android {
    signingConfigs {
        release {
            // 从 ~/.gradle/gradle.properties 读取,避免硬编码
            storeFile file(RELEASE_STORE_FILE)
            storePassword RELEASE_STORE_PASSWORD
            keyAlias RELEASE_KEY_ALIAS
            keyPassword RELEASE_KEY_PASSWORD
            enableV1Signing true
            enableV2Signing true
            enableV3Signing false
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
}

gradle.properties 中以明文保存密码仍不安全,生产环境推荐 sdkmanager 的 signingReport 任务、或把密码放 CI 加密变量。V2/V3 签名是 APK Signature Scheme v2/v3,比 V1(JAR 签名)更安全且更快。

2. proguard-rules.pro

开启 minifyEnabled true 后,AGP 调用 R8/ProGuard 对字节码做混淆、压缩、优化。默认规则来自 getDefaultProguardFile('proguard-android-optimize.txt'),自身规则写在模块 proguard-rules.pro,常见保留:

# 保留反射调用的类
-keep class com.example.helloapp.model.** { *; }

# 保留 Gson 序列化的字段
-keepclassmembers class com.example.helloapp.data.** {
    <fields>;
}

# 保留 enum 的 name() 与 valueOf()
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# 保留 Native 方法
-keepclasseswithmembernames class * {
    native <methods>;
}

# 保留 Parcelable
-keepclassmembers class * implements android.os.Parcelable {
    public static final android.os.Parcelable$Creator CREATOR;
}

3. multidex 概览

minSdk 21+ 自动开启 multidex,无需配置。如果 minSdk < 21,需要:

android {
    defaultConfig {
        multiDexEnabled true
    }
}
dependencies {
    implementation 'androidx.multidex:multidex:2.0.1'
}

并在 manifest 加 android:name="androidx.multidex.MultiDexApplication" 或在自定义 Application 中调用 MultiDex.install(this)。原因是单 dex 的方法数上限是 65535,超了就要拆。

常见坑与最佳实践

  1. compileSdk 与 targetSdk 混淆:compileSdk 是编译期可用的 API 集合;targetSdk 是运行期兼容开关,targetSdk=34 时系统会按 Android 14 行为约束你(如前台服务类型限制)。
  2. implementation vs api:api 会让依赖传递到上游,可能引发依赖冲突与构建变慢;默认用 implementation,只有当 SDK 需要对外暴露类型时才用 api。
  3. 资源名重复导致合并失败:不同模块都定义 R.string.app_name 时会冲突,给资源加模块前缀(如 home_app_name)能避免。
  4. Manifest 节点合并顺序:主 manifest 在 src/main/,library 模块或 src/debug/ 的同名节点会合并;冲突时用 tools:replace="android:label" 显式覆盖。
  5. proguard-rules.pro 漏 keep 导致 release 崩:反射、@Parcelize、Gson POJO、JNI 都要 keep,否则类被改名导致 ClassNotFoundException。可用 @Keep 注解替代手写规则。
  6. gradle.properties 改了不生效:必须 File → Sync Project with Gradle Files 才能生效;某些项需重启 Android Studio。
  7. buildToolsVersion 写死成旧版:AGP 8 不强制要求,写错反而报“version too low”。删掉这行让 AGP 自选。
  8. 多渠道打包 applicationId 冲突:用 applicationIdSuffix ".debug" 区分变体,方便同一设备同时装 debug 和 release。

章节小结

  • app 模块是 com.android.application,产物 APK;library 模块产物 AAR;
  • AndroidManifest.xml 是“应用契约”,组件、权限、入口都靠它声明;自 Android 12 起 exported 必须显式;
  • res/ 分 layout/values/drawable/mipmap 等,多限定符(-land、-night、-v29、-w600dp)让一套代码适配多端;
  • build.gradle(模块级)控制 SDK 版本、签名、混淆、构建变体;settings.gradle 收敛模块与仓库;gradle.properties 控 Gradle 与 AndroidX 全局开关;
  • release 构建需要 .jks + signingConfigs + proguard-rules.pro,minSdk < 21 时还要 multidex。

下一章预告

第 4 章我们将进入 Java 速成:从变量、运算符、流程控制,到类与对象、继承多态、接口、内部类、泛型、Lambda、集合、异常与 IO,并结合 Android 开发场景说明 Java 在 Android 中的特殊用法,为零基础同学铺好通往后续章节的路。