Android SDK 基础:项目结构、Gradle 构建与依赖管理

做 Android SDK 适配,首先得摸透 Android 项目的骨架。很多刚转过来的 iOS 同学问我:“Android 项目怎么这么‘重’?” 没错,它确实比 iOS 项目多了一层构建脚本和资源分类。我把最核心的三个点拆开讲:项目结构长啥样、Gradle 怎么配、依赖该用 implementation 还是 api。

Android 项目结构:你得知道的几个文件夹

一个标准的 Android 项目,从根目录往下看,大概长这样:

MySDKProject/
├── app/                          # 主模块(可独立运行)
│   ├── build/                    # 编译产物,别手改
│   ├── libs/                     # 放置 .jar / .aar 文件
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/             # 源代码(Kotlin/Java)
│   │   │   ├── res/              # 资源文件
│   │   │   ├── AndroidManifest.xml
│   │   │   └── jniLibs/          # 存放 .so 库
│   │   └── test/                 # 单元测试
│   └── build.gradle.kts          # 模块级构建脚本
├── library/                      # SDK 模块(可作为库发布)
├── build.gradle.kts              # 项目级构建脚本
├── settings.gradle.kts           # 项目模块声明
├── gradle.properties             # 全局属性
└── local.properties              # SDK 路径(本地环境,别提交)

我个人习惯在 SDK 适配阶段,把 jniLibs 文件夹拎出来重点检查。如果你的 SDK 包含了 C/C++ 代码,.so 文件必须按 CPU 架构放在对应子目录(armeabi-v7a、arm64-v8a、x86_64)。少一个架构,用户跑在高通骁龙 8 Gen 3 上可能直接崩。我曾经碰到过某第三方 SDK 只给了 armeabi 的 so,在 Pixel 6 上黑屏半天。

Gradle 构建配置:你绕不开的 DSL

Gradle 是 Android 的构建系统,现在主流是用 Kotlin DSL(.kts 结尾)。以前项目大多用 Groovy,我建议新项目直接上 Kotlin DSL,类型安全,IDE 补全也更友好。

一个典型的模块级 build.gradle.kts 结构:

plugins {
    id("com.android.library")  // 如果是 SDK 模块就用 library
    id("org.jetbrains.kotlin.android")
}

android {
    namespace = "com.sdk.core"
    compileSdk = 34

    defaultConfig {
        minSdk = 21
        targetSdk = 34
        consumerProguardFiles("consumer-rules.pro")
    }

    buildTypes {
        release {
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
}

dependencies {
    implementation("androidx.core:core-ktx:1.12.0")
    implementation("com.squareup.okhttp3:okhttp:4.12.0")

    // 注意这里!
    api("com.google.guava:guava:33.0.0-android")
}

这里有几个字段你需要留意:

  • compileSdk:编译用的 API 版本,最好用最新的稳定版(目前 34)。高了可能会有弃用警告,低了有些新 API 不能用。
  • minSdk:最低支持版本。做 SDK 适配时这参数很关键——定得太高(比如 minSdk=26)直接丢掉 5% 的用户;定得太低(14)你又要兼容一堆旧特性。我个人建议 SDK 最少支持到 21 或 23。
  • consumerProguardFiles:SDK 专属的 ProGuard 规则。你的 SDK 打包成 aar 后,这个文件会自动合并到宿主应用里。要是忘了配,混淆后反射调用直接炸。

依赖管理:implementation vs api,别再混用了

这个困扰过无数新人的问题,我讲个故事你就懂了。

假设你的 SDK 依赖了 Guava,然后你写了一个工具类 SDKUtils,它的方法签名里用到了 Guava 的 Optional 类型。SDK 内部用了 implementation("com.google.guava:guava:33.0.0-android")。宿主编译时调了 SDKUtils.someMethod(),编译直接报错:

error: cannot access com.google.common.base.Optional

为什么?因为 implementation 的意思是“只在模块内部使用,对外隐藏”。宿主的编译路径上根本拿不到 Guava 的类。

改成 api 就解决了。但注意:滥用 api 会导致依赖传递膨胀。宿主要是也引了个不同版本的 Guava,就可能冲突。

关键字是否可传递典型场景
implementation否(不暴露给依赖方)内部实现细节、第三方工具(如 OkHttp 内部使用)
api是(暴露给依赖方)SDK 对外 API 使用的依赖(如接口参数或返回类型用到的库)
compileOnly否(仅编译期)只为了编译检查,不打包进去(如注解处理器)
runtimeOnly否(仅运行时)运行时才需要的日志实现等
核心口诀:你的 SDK 对外暴露的接口里用到了哪个库的类,那个库就用 api;否则一律用 implementation。
一个小建议:写 SDK 时尽量把对外暴露的依赖剥离出来,比如设计成 interface + 代理模式,不让宿主编译期感知你具体用了哪个框架。这样依赖管理会清爽很多。
我曾经踩的坑:一个 SDK 内部用了 Gson,外部接口用到了它的 Gson 类型参数,却用 implementation 引入。结果上线后宿主集成时编译不过,临时发补丁改成 api,又导致宿主原有的 Gson 版本冲突。后来我学乖了——优先用 org.json 这种 JDK 自带的解析方案,避免强绑定第三方库。

一张图看懂核心逻辑

Android 项目结构与依赖作用域 Android 项目核心结构 & 依赖作用域 Android 项目结构 app/ (主模块) ├─ src/main/java ├─ src/main/res ├─ src/main/jniLibs └─ AndroidManifest.xml library/ (SDK 模块) ├─ build.gradle.kts ├─ consumer-rules.pro └─ libs/*.aar / *.jar 依赖声明作用域 implementation 模块内部可见,对外隐藏。性能好。 api 暴露给调用方,可传递。谨慎使用。 compileOnly runtimeOnly 选择 api 还是 implementation?看你的公开 API 是否引用了该类

常见问题快问快答

  • Q:整个项目用 implementation,编译会更快吗?
    是的,Gradle 不需要重新编译依赖方,增量编译速度提升明显。所以默认用 implementation,非必要不用 api。
  • Q:SDK 模块里的 aar 文件怎么放?
    直接丢进 libs/ 目录,然后在 dependencies 里加一句 implementation(fileTree("libs/*.aar"))。或者单独引用:implementation(files("libs/mysdk-1.0.aar"))。
  • Q:JNI 库的 so 文件能不能不按架构分文件夹?
    不能。Android 系统根据 CPU 架构搜索对应子目录。如果你只放一个 arm64-v8a 下的 so,用户手机是 armeabi-v7a 就会闪退。要么所有架构都放,要么只放最广泛的两个。
我的一个小习惯:在 build.gradle.kts 里加一段任务打印最终依赖树,调试依赖冲突特别好用:
tasks.register("printDeps") { doLast { configurations.implementation.get().forEach { println(it.name) } } }

嗯,Android SDK 基础这块其实不复杂,但细节特别多。项目结构决定你放资源的姿势,Gradle 配置决定你编译的方式,implementation 和 api 的选择直接决定宿主集成的体验。用好这些基础,后面讲 SDK 混淆、多 module 构建、资源覆盖才不会有坑。