14、SDK 与 NDK 编译:SDK 编译流程、NDK 模块编译、预编译工具链

说到 Android 构建,SDK 和 NDK 是绕不开的两座大山。很多人刚开始接触时容易混淆——SDK 是给 Java/Kotlin 用的,NDK 是给 C/C++ 用的。但实际项目里,它们经常要协同工作。我见过不少团队,Java 层跑得飞起,一到 Native 层就各种踩坑。

这一节,咱们就把 SDK 编译、NDK 模块编译、还有预编译工具链这几个核心概念彻底讲透。你想想看,搞懂了这些,以后遇到编译问题至少能知道从哪下手。

14.1 SDK 编译流程

SDK 编译,说白了就是把你的 Java/Kotlin 源码变成 .dex 文件,再打包成 APK 或 AAR。这个过程在 Android Studio 里点一下「Build」就完成了,但背后其实是一套流水线作业。

我个人习惯把 SDK 编译拆成几个关键阶段:

  1. 资源编译:aapt2 把资源文件(布局、图片、字符串等)编译成二进制格式,生成 R.java
  2. 源码编译:javac 或 kotlinc 把 Java/Kotlin 源码编译成 .class 文件
  3. DEX 转换:d8 或 R8 把 .class 文件转成 .dex 文件,同时做混淆和优化
  4. APK 打包:把所有 .dex、资源、签名信息打包成 APK

核心要点:SDK 编译的最终产物是 .dex 文件,它运行在 ART 或 Dalvik 虚拟机上。所有 Java/Kotlin 代码最终都会被转换成这个格式。

我在项目中遇到过一个问题:某个第三方库的 .class 文件版本太高,导致 d8 转换时报错。后来发现是编译 JDK 版本不一致造成的。嗯,这里要注意——Android 编译用的 JDK 版本和普通 Java 项目不一样,建议统一用 Android Studio 自带的 JDK。

14.2 NDK 模块编译

NDK 编译就完全是另一套玩法了。它用的是交叉编译——在 x86 的电脑上编译出 ARM 架构的机器码。说白了,就是让 C/C++ 代码能在手机上跑起来。

NDK 编译的核心是 Android.mkCMakeLists.txt。我个人更推荐 CMake,因为语法更现代,跨平台支持也更好。不过老项目里 Android.mk 依然很常见。

来看一个典型的 CMake 配置:

cmake_minimum_required(VERSION 3.18.1)
project("my_native_lib")

add_library(
    my_native_lib
    SHARED
    native-lib.cpp
    utils.cpp
)

find_library(
    log-lib
    log
)

target_link_libraries(
    my_native_lib
    ${log-lib}
)

这段代码干了三件事:

  • 声明一个叫 my_native_lib 的共享库
  • native-lib.cpputils.cpp 编译进去
  • 链接 Android 的 log 库,方便打印日志

小技巧:如果你在 NDK 编译时遇到 undefined reference 错误,八成是链接库没写对。我曾经花了一整天排查,最后发现是少写了一个 target_link_libraries

NDK 编译还有一个重要概念——ABI 分屏。Android 支持多种 CPU 架构:

ABI 名称 对应架构 常见设备
armeabi-v7a ARM 32位 老款手机、低端设备
arm64-v8a ARM 64位 主流中高端手机
x86 Intel 32位 模拟器、部分平板
x86_64 Intel 64位 模拟器、Chromebook

你想想看,如果只编译 arm64-v8a,那在 armeabi-v7a 的老手机上就直接崩溃了。反过来,如果只编译 armeabi-v7a,在 64 位设备上也能跑,但性能会打折扣。

注意:Google Play 从 2019 年 8 月开始要求新应用必须支持 64 位架构。所以 arm64-v8a 现在是必选项,armeabi-v7a 建议保留做兼容。

14.3 预编译工具链

预编译工具链,听起来很高大上,其实就是「提前编译好的二进制文件」。在 Android 构建系统里,预编译模块非常常见——比如你引入了一个第三方的 .so 库,或者用到了预编译的静态库。

预编译模块的配置方式很简单。在 Android.mk 里这样写:

include $(CLEAR_VARS)
LOCAL_MODULE := my_prebuilt_lib
LOCAL_SRC_FILES := libs/$(TARGET_ARCH_ABI)/libprebuilt.so
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)

这段代码的意思是:告诉构建系统,这个模块不需要重新编译,直接用我提供的 .so 文件。注意 $(TARGET_ARCH_ABI) 这个变量——它会自动匹配当前编译的 ABI 架构。

我遇到过最坑的情况是:预编译的 .so 文件是用旧版 NDK 编译的,和新版 NDK 的 libc++ 不兼容。运行的时候直接 crash,连日志都来不及打印。后来我养成了一个习惯——预编译库的 NDK 版本一定要和项目保持一致

预编译工具链还有一个重要用途——提供系统级接口。比如你想调用 Android 的硬件编解码器,但不想自己写 JNI,就可以用预编译的 libstagefright.so。当然,这种做法现在已经不推荐了,因为 Android 版本迭代太快,直接依赖系统库很容易出问题。

14.4 知识体系总览

说了这么多,咱们用一张图把 SDK 和 NDK 编译的核心逻辑串起来:

Android 构建系统:SDK 与 NDK 编译流程 SDK 编译流程 Java/Kotlin 源码 + 资源文件 aapt2 编译资源 → R.java javac/kotlinc → .class 文件 d8/R8 → .dex 文件 NDK 编译流程 C/C++ 源码 + CMake/Android.mk 交叉编译工具链 (clang/gcc) 编译为 .o 目标文件 链接 → .so 共享库 最终产物:APK(包含 .dex + .so + 资源文件)

这张图把 SDK 和 NDK 的编译流程做了对比。左边是 Java 层的编译链路,右边是 Native 层的编译链路。两条线最终在 APK 打包时汇合——.dex 文件和 .so 文件一起被打进 APK 里。

我个人觉得,理解这张图比背一堆命令重要得多。你想想看,只要搞清楚了「源码→中间产物→最终产物」这条线,遇到编译问题就能快速定位——是 Java 层报错还是 Native 层报错?是资源问题还是链接问题?

总结一下:SDK 编译产出 .dex,NDK 编译产出 .so,预编译工具链就是直接拿现成的 .so 来用。这三者构成了 Android 构建系统的核心骨架。

好了,这一节的内容就到这里。记住我强调的几个关键点:JDK 版本一致性、ABI 分屏策略、预编译库的 NDK 版本匹配。这些都是实战中容易踩坑的地方,提前注意能省不少时间。


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