本文译自「Packaging with Android Archive」,原文链接medium.com/gitconnecte...,由Chirani Rajapaksha发布于2026年6月20日。

当 Android 开发者构建库模块时,输出的并非标准的 JAR 文件,而是 AAR 文件。Android 引入 AAR 格式的初衷是,JAR 文件基于 JVM 构建,并不包含 Android 特有的组件。JAR 文件可以包含编译后的字节码,但无法包含清单文件、资源文件、布局文件或原生 .so 库。
AAR 是一种 ZIP 压缩的归档文件,它将 Android 库所需的所有内容打包到一个可分发的文件中。根据Android开发者文档,AAR文件可以包含:
-
AndroidManifest.xml--- 在构建时合并到使用该库的应用的清单文件中 -
classes.jar--- 已编译的Kotlin/Java字节码 -
res/--- Android资源,例如布局、可绘制对象和字符串 -
assets/--- 原始资源文件 -
jni/--- 已编译的本地.so库 -
libs/--- 其他JAR文件 -
Consumer ProGuard规则
-
资源引用的R.txt符号
这与JAR文件有着本质区别,JAR文件仅包含已编译的类文件和元数据。当你的库需要声明权限、注册组件或发布本地代码时,这种区别就显得尤为重要,而这些都是封装硬件API(例如ARCore)的SDK的常见需求。
AAR 与 JAR:选择合适的格式
Android Studio 项目文档明确区分了这两种格式:
Android 库生成 AAR 文件。Kotlin 或 Java 库生成 JAR 文件。
实际规则很简单:
场景 格式 纯业务逻辑,不包含 Android API JAR 共享工具、数据模型 JAR Android UI 组件 AAR 相机或传感器访问 AAR AR/位置/地图功能 AAR 原生 C/C++ 库 AAR SDK 分发给客户端应用 AAR
ARCore 完全属于 AAR 范畴。它需要在 AndroidManifest.xml 中声明相机权限,需要为 Google Play 服务添加特定的 meta-data 条目,并且内部包含原生 .so 库。这些都不需要 JAR 文件。
ARCore 的使用案例:隔离的重要性
考虑一个使用 Flutter 开发的应用,其中包含一个用 Kotlin 编写的原生 Android 层。该应用程序使用 Google ARCore 进行平面检测和表面识别,例如识别摄像头在现实世界中看到的水平或垂直平面。
在典型的初始版本中,开发者会在整个 Android 代码库中直接导入 ARCore:
vbnet
flutter_app/
└── android/
└── app/
├── MainActivity.kt ← ARCore imports here
├── ArSessionManager.kt ← ARCore imports here
└── PlaneDetectionHelper.kt ← ARCore imports here
每个涉及 AR 功能的文件都会与 ARCore 的内部类耦合,例如 Session、Frame、Plane、Config、Trackable 等等。这会带来一些实际问题:
升级 ARCore 存在风险。 ARCore API 的任何重大变更都意味着所有导入它的文件都需要修改,而且没有统一的边界来控制影响范围。
客户端会获取过多信息。 如果你将此 Android 模块交付给客户,他们可以查看和修改所有 ARCore 集成代码。这可能会暴露你希望保护的实现细节。
测试难度加大。 由于调用应用程序模块了解 ARCore 类,因此无法像使用桩代码那样轻松地将 AR 后端替换为单元测试用的桩代码。
解决方案是划定一个明确的边界。ARCore 成为一个实现细节,隐藏在一个接口之后,应用程序的其他部分无需了解底层 SDK 即可使用该接口。
模块结构设计
针对此场景,一个简洁的架构使用了三个模块:
kotlin
android/
├── app/ ← Flutter host, platform channels, UI
├── ar-facade/ ← Public interfaces and data classes only
└── ar-core-impl/ ← ARCore SDK + implementation → compiled as AAR
模块:ar-facade
此模块仅包含契约。此处未声明任何 ARCore 依赖项。
kotlin
// ar-facade/src/main/kotlin/com/example/ar/ArSurfaceDetector.kt
interface ArSurfaceDetector {
fun startSession()
fun stopSession()
fun getDetectedPlanes(): List<DetectedPlane>
}
data class DetectedPlane(
val id: String,
val type: PlaneType,
val centerX: Float,
val centerY: Float
)
enum class PlaneType { HORIZONTAL_UP, HORIZONTAL_DOWN, VERTICAL }
app 模块依赖于 ar-facade。它调用 detector.startSession() 并接收 List<DetectedPlane>。它不会访问 com.google.ar.core.Plane 或任何其他 ARCore 类。
模块:ar-core-impl
这是生成 AAR 文件的库模块。所有 ARCore 导入都仅存在于此模块中。
kotlin
// ar-core-impl/src/main/kotlin/com/example/ar/impl/ArCoreDetector.kt
import com.google.ar.core.ArCoreApk
import com.google.ar.core.Config
import com.google.ar.core.Frame
import com.google.ar.core.Plane
import com.google.ar.core.Sessionclass
ArCoreDetector(private val context: Context) : ArSurfaceDetector {
private var session: Session? = null
override fun startSession() {
session = Session(context).also { s ->
s.configure(Config(s))
s.resume()
}
}
override fun stopSession() {
session?.pause()
session?.close()
session = null
}
override fun getDetectedPlanes(): List<DetectedPlane> {
val frame = session?.update() ?: return emptyList()
return frame.getUpdatedTrackables(Plane::class.java).map { plane ->
DetectedPlane(
id = plane.hashCode().toString(),
type = plane.type.toDetectedPlaneType(),
centerX = plane.centerPose.tx(),
centerY = plane.centerPose.tz()
)
}
}
private fun Plane.Type.toDetectedPlaneType(): PlaneType = when (this) {
Plane.Type.HORIZONTAL_UPWARD_FACING -> PlaneType.HORIZONTAL_UP
Plane.Type.HORIZONTAL_DOWNWARD_FACING -> PlaneType.HORIZONTAL_DOWN
Plane.Type.VERTICAL -> PlaneType.VERTICAL
}
}
ar-core-impl 模块之外的任何文件都不会访问这些 ARCore 类型。
构建 AAR:分步指南
步骤 1 --- 将模块声明为 Android 库
在 ar-core-impl/build.gradle.kts 文件中,应用库插件而不是应用程序插件:
bash
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
ini
android {
namespace = "com.example.ar.impl"
compileSdk = 35 defaultConfig {
minSdk = 24
consumerProguardFiles("consumer-rules.pro")
}
}
com.android.library 插件使 Gradle 生成 AAR 文件而不是 APK 文件。正如 Android Studio 文档 所述,应用此插件后,构建过程会创建 AAR 文件,而不是 APK 文件。
步骤 2 --- 添加 ARCore 依赖项
scss
dependencies {
implementation("com.google.ar:core:1.45.0")
implementation(project(":ar-facade"))
}
步骤 3 --- 添加 ARCore 所需的清单条目
AAR 的 AndroidManifest.xml 文件会自动包含其所需的条目。当使用 AAR 的应用包含该 AAR 时,这些清单条目会被合并到最终的应用清单中:
xml
<!-- ar-core-impl/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
ini
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature
android:name="android.hardware.camera.ar"
android:required="true" /> <application>
<meta-data
android:name="com.google.ar.core"
android:value="required" />
</application></manifest>
根据 ARCore NDK 快速入门文档,uses-feature 标签会将应用在 Google Play 商店中的可见性限制在支持 ARCore 的设备上,而 meta-data 标签会将应用标记为 AR 必需,这意味着必须安装 Google Play 服务 AR 版。
由于这些条目存在于 AAR 的清单中,因此使用 AAR 的 app 模块无需声明任何 ARCore 特有的清单配置。它会继承库中的所有配置。
步骤 4 --- 添加消费者 ProGuard 规则
arduino
# consumer-rules.pro
-keep class com.example.ar.impl.** { *; }
库的 Gradle 配置中的 consumerProguardFiles 设置可确保在使用库的应用运行 R8 或 ProGuard 时自动应用这些规则。Android 文档 指出,消费者 ProGuard 规则会随 AAR 文件一起打包,并应用于消费者的构建;SDK 作者控制哪些规则会被保留,而无需消费者手动管理这些规则。
步骤 5 --- 构建 AAR 文件
ruby
./gradlew :ar-core-impl:assembleRelease
输出文件位于:
rust
ar-core-impl/build/outputs/aar/ar-core-impl-release.aar
在应用模块中使用 AAR 文件
选项 A:本地文件依赖
将 AAR 文件放在 app/libs/ 目录下,并在 app/build.gradle.kts 文件中声明它:
less
dependencies {
implementation(files("libs/ar-core-impl-release.aar"))
implementation(project(":ar-facade"))
}
选项 B:本地 Maven 仓库(推荐用于客户端交付)
Android 开发者文档 解释说,直接分发原始 AAR 文件会缺少重要的元数据------使用者无法获得版本、标识或传递依赖项等信息。通过 Maven 仓库发布可以解决这个问题:
ruby
./gradlew :ar-core-impl:publishToMavenLocal
然后,使用者声明一个标准依赖项:
scss
repositories {
mavenLocal()
}
scss
dependencies {
implementation("com.example.ar:ar-core-impl:1.0.0")
}
这实现了语义化版本控制、自动传递依赖项解析以及清晰的升级路径------使用者只需更新版本字符串,而无需替换文件。
隔离后的应用程序模块是什么样子
边界到位后,"app"模块永远不会引用 ARCore。 Flutter 平台通道处理程序可能如下所示:
ruby
// app/src/main/kotlin/com/example/app/ArPlatformChannel.kt
kotlin
import com.example.ar.ArSurfaceDetector // facade only
import com.example.ar.DetectedPlane // facade onlyclass
ArPlatformChannel(private val detector: ArSurfaceDetector) :
MethodChannel.MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"startSession" -> { detector.startSession(); result.success(null) }
"stopSession" -> { detector.stopSession(); result.success(null) }
"getDetectedPlanes" -> result.success(detector.getDetectedPlanes().map { it.toMap() })
else -> result.notImplemented()
}
}
}
"app/"中的任何位置都不存在"import com.google.ar.core.*"行。
向客户端演示隔离
当将 AAR 连同"app"模块一起交给客户时,有具体的方法来证明边界成立。
在源代码级别进行验证
在应用程序模块中运行 ARCore 导入搜索:
bash
grep -r "com.google.ar" app/src
空结果证明"app"模块没有直接的 ARCore 依赖项。客户可以在收货前自行审核。
显示公共合约
展示"ar-facade"模块接口。这些是客户代码将与之交互的唯一类型:"ArSurfaceDetector"、"DetectedPlane"和"PlaneType"。 ARCore SDK 不在此列表中。
展示可替换性
解释架构结果:如果 ARCore 的 API 在未来版本中发生变化,或者如果首选替代 AR 引擎,则可以完全替换"ar-core-impl"模块。 app 模块和 Flutter 层需要零更改,因为它们只知道外观。 AAR 被交换;边界以上的一切都未受影响。
运行应用程序
在支持的设备上启动应用程序。相机激活、飞机被检测、Flutter UI 更新------所有这些都不需要应用程序模块了解检测的内部工作原理。
值得了解的局限性
可以进行逆向工程。 AAR 是编译代码,而不是加密代码。 JADX 等工具可以反编译部分实现。为了提供更强大的 IP 保护,R8 混淆与激进的缩小和重命名提供了有意义的抵抗,尽管它并不能阻止坚定的逆向工程师。 C/C++ 中的本机实现更难反编译,并且可能适合特别敏感的逻辑。
原始 AAR 没有依赖项元数据。 正如 Android 文档说明,原始 AAR 文件不会声明其身份、版本或传递依赖项。对于超出简单本地集成的任何内容,建议通过 Maven 发布。
清单合并需要注意。 当 AAR 的清单合并到使用应用程序时,可能会出现冲突 - 例如,如果应用程序已经声明了具有不同设置的"uses-feature"。 Android 的清单合并工具 会自动处理大多数情况,但冲突需要使用合并规则手动解决。
参考文献
- 创建Android库------Android Studio | Android 开发者
- 上传你的库------Android Studio | Android 开发者
- 准备发布库------Android Studio | Android 开发人员
- 使用 Fused Library 将多个 Android 库合二为一发布 --- Android 开发人员
- 在你的 Android NDK 应用程序中启用 ARCore --- ARCore | Google 开发人员
- ARCore Android SDK 示例 --- GitHub
- Flutter 平台通道文档
- 使用 Gradle 管理清单 - Android 开发人员
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
保护原创,请勿转载!