第二十三章:安装与打包——从本地构建到远程交付

说实话,很多做 NDK 开发的朋友,写代码是一把好手,但一到「怎么把我的 .so 交给别人用」就卡壳了。我自己早年也踩过这个坑——编译好的库往同事那一扔,结果人家连头文件路径都找不到。后来我才意识到,安装与打包这件事,其实是你代码工程的「最后一公里」。

这一章,我们就来聊聊这「最后一公里」怎么走。从 CMake 的 install() 命令开始,到 CPack 打包,再到生成 AAR 发布到 Maven 仓库。嗯,一条龙讲清楚。

23.1 install() 命令:让构建产物各归其位

先问个问题:你编译出来的 .so、头文件、可执行文件,它们应该放在哪?

如果你只是在自己机器上调试,那无所谓。但如果你要交付给别人,或者集成到 Android Studio 项目里,那就必须有一套规范的「摆放规则」。CMake 的 install() 命令就是干这个的。

23.1.1 基本用法

语法其实很简单:

install(TARGETS target_name
        RUNTIME DESTINATION bin
        LIBRARY DESTINATION lib
        ARCHIVE DESTINATION lib/static)

install(FILES header.h
        DESTINATION include)

这里我解释一下:

  • TARGETS:指定你要安装的目标(库或可执行文件)
  • RUNTIME:可执行文件(Windows 下 .exe,Linux 下无后缀)
  • LIBRARY:共享库(.so / .dylib)
  • ARCHIVE:静态库(.a / .lib)
  • FILES:头文件或其他资源

我在项目中遇到过一种情况:同事把 .so 和 .a 都扔到同一个目录,结果链接器总是优先找到静态库,导致运行时崩溃。后来我强制用 ARCHIVE DESTINATION 分开存放,问题就解决了。

23.1.2 在 Android NDK 中的特殊处理

Android 的 ABI 架构很多(arm64-v8a、armeabi-v7a、x86_64...),你不能一股脑全装到同一个目录。我个人的习惯是:

install(TARGETS my_native_lib
        LIBRARY DESTINATION ${CMAKE_ANDROID_ARCH_ABI})

这样每个 ABI 的 .so 就会自动放到对应的文件夹里。你想想看,如果手动去分,那得多累。

小技巧:在 Android 工程里,install() 的目标路径通常是相对于 CMAKE_INSTALL_PREFIX 的。你可以通过 cmake --install . --prefix /some/path 来指定安装根目录。

23.2 CPack:一键打包成安装包

install() 只是把文件放好,但你要交付给用户,总不能让人家自己一个个拷贝吧?这时候 CPack 就登场了。

CPack 是 CMake 自带的打包工具。它能把你安装好的文件打包成各种格式:

  • Linux:DEB、RPM、TGZ
  • macOS:DMG、Bundle
  • Windows:NSIS、WiX

配置起来也很简单,在 CMakeLists.txt 里加上:

include(CPack)

set(CPACK_PACKAGE_NAME "MyNativeLib")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A native library for Android")
set(CPACK_GENERATOR "TGZ")  # 先打个 tar.gz 试试

然后执行:

cmake --build . --target install
cpack

你就会得到一个 MyNativeLib-1.0.0-Linux.tar.gz。打开看看,里面的目录结构跟你 install 时定义的一模一样。

注意:CPack 在 Android NDK 交叉编译环境下,有些生成器可能不支持。我建议你优先用 TGZ 或 ZIP 格式,通用性最强。我曾经试过在 NDK 下生成 DEB 包,结果依赖解析全乱了,折腾了一下午。

23.3 生成 AAR 包:Android 原生库的标准交付物

如果你做的是 Android 库,那 AAR 才是正道。AAR 是 Android Archive 的缩写,它把 .so、Java/Kotlin 代码、资源文件、AndroidManifest 都打包在一起。

生成 AAR 有两种方式:

23.3.1 方式一:用 Android Gradle Plugin 的 native 模块

build.gradle 里配置:

android {
    defaultConfig {
        ndk {
            abiFilters 'arm64-v8a', 'armeabi-v7a'
        }
    }
    externalNativeBuild {
        cmake {
            path 'src/main/cpp/CMakeLists.txt'
        }
    }
}

然后执行 ./gradlew assembleRelease,就会在 build/outputs/aar/ 下生成 library-release.aar

打开这个 AAR(其实就是个 zip),你会看到:

  • jni/arm64-v8a/libnative.so
  • jni/armeabi-v7a/libnative.so
  • classes.jar(如果有 Java 代码)
  • AndroidManifest.xml

23.3.2 方式二:手动组装 AAR(适用于纯 C++ 库)

有时候你不想引入 Gradle 那一套,只想用 CMake 搞定一切。那你可以手动构建 AAR 结构:

# 在 CMakeLists.txt 中
install(TARGETS my_native_lib
        LIBRARY DESTINATION jni/${CMAKE_ANDROID_ARCH_ABI})

# 然后写一个脚本,把 jni 目录、AndroidManifest.xml、proguard.txt 打包成 zip,改后缀为 .aar

我个人更推荐方式一,因为 Gradle 会自动处理 ABI 筛选、混淆配置等细节。但如果你在做一个纯 C++ 的 SDK,方式二反而更灵活。

核心要点:AAR 里的 .so 必须按 ABI 分目录存放。Android 系统在加载时,会根据设备的 CPU 架构选择对应的目录。如果你把所有 ABI 的 .so 都扔到同一个目录,那安装包会变得巨大,而且可能加载失败。

23.4 发布到 Maven 仓库:让全世界都能用你的库

AAR 生成好了,怎么给别人用?最规范的方式就是发布到 Maven 仓库。

Maven 仓库分两种:

  • 本地仓库~/.m2/repository,仅供自己或局域网使用
  • 远程仓库:如 Maven Central、JitPack、私有 Nexus

23.4.1 发布到本地 Maven 仓库

build.gradle 里添加:

apply plugin: 'maven-publish'

publishing {
    publications {
        release(MavenPublication) {
            groupId = 'com.example'
            artifactId = 'my-native-lib'
            version = '1.0.0'

            artifact "${buildDir}/outputs/aar/library-release.aar"
        }
    }
    repositories {
        mavenLocal()
    }
}

执行 ./gradlew publishReleasePublicationToMavenLocal,你的 AAR 就发布到本地了。其他项目引用时,只需要在 build.gradle 里加:

implementation 'com.example:my-native-lib:1.0.0'

23.4.2 发布到远程 Maven 仓库

发布到远程仓库稍微复杂一点,需要配置认证信息。以 Maven Central 为例:

publishing {
    repositories {
        maven {
            url = 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/'
            credentials {
                username = project.findProperty('ossrhUsername')
                password = project.findProperty('ossrhPassword')
            }
        }
    }
}

然后在 gradle.properties 里设置用户名和密码(千万别提交到 Git!)。

避坑指南:我曾经把密码直接写在 build.gradle 里,结果不小心 push 到了公开仓库。虽然马上删除了,但 Git 历史里还有记录。后来我改用环境变量 ORG_GRADLE_PROJECT_ossrhPassword,安全多了。

23.5 知识体系总览

这一章的内容比较多,我画了一张图帮你理清思路:

安装与打包知识体系 CMake 构建产物 install() 命令 按 ABI 分目录、安装头文件 CPack 打包 生成 TGZ / DEB / DMG 生成 AAR 包 Gradle / 手动组装 发布到 Maven 本地 / 远程仓库 安装包分发 Android 集成 依赖管理 最终目标:让用户通过一行依赖就能使用你的原生库

从图上你可以看到,整个流程是线性的:构建 → 安装 → 打包 → 发布。每一步都有对应的工具和最佳实践。

23.6 一些实战建议

最后,分享几个我这些年总结出来的经验:

  1. 版本号管理:我习惯把版本号定义在 CMakeLists.txtproject() 命令里,然后通过 -D 参数传递给 Gradle。这样版本号只有一个源头,不会出现两边不一致的情况。
  2. ABI 选择:不要一股脑把所有 ABI 都打进去。如果你的库只用在手机端,arm64-v8a 就够了。armeabi-v7a 可以保留,但 x86 和 x86_64 除非你确定需要,否则建议去掉。包体积能小不少。
  3. 签名与校验:发布到 Maven Central 的 AAR 必须签名。我建议在 CI 流程里自动完成签名,避免手动操作漏掉。
  4. 文档与示例:AAR 包里可以附带一个 docs/ 目录,放 API 文档。虽然 Maven 仓库不强制,但用户会很感激。
我的习惯:每次发布前,我都会在本地跑一遍 ./gradlew publishToMavenLocal,然后在另一个干净项目里引用这个本地版本,验证一切正常后,再发布到远程仓库。这一步虽然简单,但能避免很多尴尬的「发布即翻车」事故。

好了,这一章的内容就到这里。安装与打包看似是体力活,但做好了,能让你和你的用户都省心不少。记住:好的工程习惯,往往体现在这些「最后一公里」的细节里


公众号:蓝海资料掘金营,微信deep3321