使用Android Archive进行打包

本文译自「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 的内部类耦合,例如 SessionFramePlaneConfigTrackable 等等。这会带来一些实际问题:

升级 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 的清单合并工具 会自动处理大多数情况,但冲突需要使用合并规则手动解决。

参考文献

  1. 创建Android库------Android Studio | Android 开发者
  2. 上传你的库------Android Studio | Android 开发者
  3. 准备发布库------Android Studio | Android 开发人员
  4. 使用 Fused Library 将多个 Android 库合二为一发布 --- Android 开发人员
  5. 在你的 Android NDK 应用程序中启用 ARCore --- ARCore | Google 开发人员
  6. ARCore Android SDK 示例 --- GitHub
  7. Flutter 平台通道文档
  8. 使用 Gradle 管理清单 - Android 开发人员

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

相关推荐
千里马学框架1 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work1 天前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
ai2work1 天前
ch21 签名、校验与发版
kotlin
AFinalStone1 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui