稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

使用Android Archive进行打包

本文译自「Packaging with Android Archive」,原文链接https://medium.com/gitconnected/packaging-with-android-archive-f5a33c48a25e,由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:选择合适的格式

考虑一个使用 Flutter 开发的应用,其中包含一个用 Kotlin 编写的原生 Android 层。该应用程序使用 Google ARCore 进行平面检测和表面识别,例如识别摄像头在现实世界中看到的水平或垂直平面。

在典型的初始版本中,开发者会在整个 Android 代码库中直接导入 ARCore:

1
2
3
4
5
6
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 即可使用该接口。

模块结构设计

此模块仅包含契约。此处未声明任何 ARCore 依赖项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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

步骤 1 — 将模块声明为 Android 库

ar-core-impl/build.gradle.kts 文件中,应用库插件而不是应用程序插件:

1
2
3
4
plugins {
    id("com.android.library")
    id("org.jetbrains.kotlin.android")
}
1
2
3
4
5
6
7
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 依赖项

1
2
3
4
dependencies {
    implementation("com.google.ar:core:1.45.0")
    implementation(project(":ar-facade"))
}

步骤 3 — 添加 ARCore 所需的清单条目

AAR 的 AndroidManifest.xml 文件会自动包含其所需的条目。当使用 AAR 的应用包含该 AAR 时,这些清单条目会被合并到最终的应用清单中:

1
2
<!-- ar-core-impl/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
1
2
3
4
5
6
7
8
<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 规则

1
2
# consumer-rules.pro
-keep class com.example.ar.impl.** { *; }

库的 Gradle 配置中的 consumerProguardFiles 设置可确保在使用库的应用运行 R8 或 ProGuard 时自动应用这些规则。Android 文档 指出,消费者 ProGuard 规则会随 AAR 文件一起打包,并应用于消费者的构建;SDK 作者控制哪些规则会被保留,而无需消费者手动管理这些规则。

步骤 5 — 构建 AAR 文件

1
./gradlew :ar-core-impl:assembleRelease

输出文件位于:

1
ar-core-impl/build/outputs/aar/ar-core-impl-release.aar

在应用模块中使用 AAR 文件

边界到位后,“app”模块永远不会引用 ARCore。 Flutter 平台通道处理程序可能如下所示:

1
// app/src/main/kotlin/com/example/app/ArPlatformChannel.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 是编译代码,而不是加密代码。 JADX 等工具可以反编译部分实现。为了提供更强大的 IP 保护,R8 混淆与激进的缩小和重命名提供了有意义的抵抗,尽管它并不能阻止坚定的逆向工程师。 C/C++ 中的本机实现更难反编译,并且可能适合特别敏感的逻辑。

原始 AAR 没有依赖项元数据。 正如 Android 文档说明,原始 AAR 文件不会声明其身份、版本或传递依赖项。对于超出简单本地集成的任何内容,建议通过 Maven 发布。

清单合并需要注意。 当 AAR 的清单合并到使用应用程序时,可能会出现冲突 - 例如,如果应用程序已经声明了具有不同设置的“uses-feature”。 Android 的清单合并工具 会自动处理大多数情况,但冲突需要使用合并规则手动解决。

参考文献

Comments