
前阵子帮同事调一个 vendor 提供的动态库同事盯着 Android.mk 看了半天第一句话是Android 10 了还用得着看这个不是都改成 Android.bp 了吗。这个问题我几乎每次带新人都会遇到。先说结论Android 10 推荐新模块用 Android.bp但整个编译系统并没有把 Android.mk 扫进垃圾堆尤其你一旦要动 BSP、vendor 库、老产品分支或者想把一份 C 源码编成系统动态库塞进 system 分区Android.mk 依然是绕不开的东西。这篇把这个系列里“编译产物”这一环补上专门讲清楚 Android.mk 编译动态库这件事从最小配置到底层逻辑再到安装路径和排错一条线走完。1. Soong时代为什么还有这么多Android.mk——先搞明白你面对的是哪套编译体系1.1 双轨制不是过渡期而是一种长期状态Android 10 的编译系统确实已经是 Soong 的天下新增模块官方推荐写 Android.bp系统自带的 framework、服务、应用也大量完成了 bp 化。但你打开一个真实的商用工程在 device/ 和 vendor/ 目录下翻一翻Android.mk 的数量绝对能吓你一跳。原因在于Android 10 的编译流程里Soong 负责解析 Android.bp但有一块独立的逻辑仍然通过 kati 继续解析所有 Android.mk两者最终都会生成 Ninja 文件统一交给 Ninja 去执行。也就是说mk 不是“被支持的老古董”而是整个 Android.mk 体系的正式入口。kati 不退役Android.mk 就能继续正常工作而且可以和 Android.bp 模块在同一个镜像里共存、互相依赖。我碰到过不少只写应用层的开发者以为 AOSP 全源码是纯 bp结果拿到一个 SDK 或 BSP 包发现里面全是 mk当场就懵了。实际上除了 AOSP 主线的 base 系统其余的定制层、厂商适配层、老平台代码很大一部分还活在 Android.mk 里。google 自己也清楚这个问题所以对 mk 的兼容做得相当保守不会甩手不管。1.2 哪些场景轮到你手写Android.mk如果你是做纯新模块我建议直接上 Android.bp没必要逆潮流。但下面这几种场景Android.mk 是躲不掉的你在维护一个老产品分支分支里几十个库全是 mk你新增的模块要加入它们的依赖链用 mk 写能保持风格统一也方便后续合并代码。你拿到手的 vendor BSP 里厂商只提供了带 Android.mk 的预编译库或源码库你需要在它之上加一层封装最稳的做法是把封装库也用 mk 写因为 mk 可以直接引用 mk 模块依赖关系最顺。模块里有大量 ifneq/ifeq 条件分支比如“调试版本编 A正式版本编 B”这种复杂的编译期条件逻辑用 Android.mk 的宏处理非常直接改成 bp 反而要绕一大圈。顺带说一个误解有人觉得 Android.mk 编出来的东西“老”Android.bp 编出来的东西“新”其实最终产物都是 ELF、都是 .so没有任何质量差别。mk 和 bp 只是写给构建系统看的接口不同编译器、链接器、优化选项都是同一套。所以我在这个系列前面聊根文件系统时提到过系统里的一堆 /system/lib64/*.so不会因为你是用 mk 编的还是用 bp 编的就有什么三六九等。2. 一个能跑的Android.mk动态库最小配置逐行拆到骨头里2.1 完整示例一个带JNI和日志输出的动态库先给一个最能说明问题的例子编一个 JNI 动态库上层 Java 可以加载它调用 native 方法。我故意选了 JNI 场景一方面是因为它几乎每个 AOSP 定制项目都会碰到另一方面它能顺带讲清楚符号导出、extern C 这些让新人摔跤的点。假设你的模块目录结构是packages/apps/MyApp/jni/ ├── Android.mk ├── native-lib.cpp └── include/ └── mynative.hAndroid.mk 最开始可以长这样LOCAL_PATH : $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE : libmynative LOCAL_MODULE_CLASS : SHARED_LIBRARIES LOCAL_SRC_FILES : native-lib.cpp LOCAL_SHARED_LIBRARIES : liblog LOCAL_C_INCLUDES : $(LOCAL_PATH)/include LOCAL_CFLAGS : -Wall -Wextra LOCAL_CPPFLAGS : -stdc17 include $(BUILD_SHARED_LIBRARY)native-lib.cpp 里写一个最简单的 JNI 接口#include jni.h #include android/log.h #include mynative.h #define LOG_TAG MyNative #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) extern C JNIEXPORT jstring JNICALL Java_com_example_mynative_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) { LOGI(call from java layer); return env-NewStringUTF(Hello from Android.mk dynamic lib); }2.2 每个变量的作用和坑这段代码精简到只剩必需的变量但每个都值得展开说。LOCAL_PATH : $(call my-dir)必须在 mk 文件靠前的位置出现它的作用是记录当前 mk 所在目录。整个 AOSP 构建系统是递归解析的编译器并不知道你的源码在哪全靠LOCAL_PATH定位。include $(CLEAR_VARS)则是清理上一轮模块残留的变量避免 A 模块的配置污染 B 模块。这两行几乎是所有 Android.mk 的标准开头顺序不要反。LOCAL_MODULE是模块名也是构建系统里用来引用这个模块的 ID。这里有个容易误会的点LOCAL_MODULE : mynative和LOCAL_MODULE : libmynative最终产物都是libmynative.so因为 BUILD_SHARED_LIBRARY 会自动给不带 lib 前缀的名字补上 lib但如果你已经写了 lib 开头它不会再叠加一次。我见过有人为了保险写成LOCAL_MODULE : liblibmynative编出来的 so 叫liblibmynative.so依赖它的模块写着libmynative链接时直接找不到。LOCAL_MODULE_CLASS : SHARED_LIBRARIES指明这是个共享库。这个变量直接影响产物的输出目录和安装属性SHARED_LIBRARIES 类别的产物最终落在镜像里的 /system/lib 或 /system/lib64。很多简化教程不写这一行也能编过因为构建系统会根据 BUILD_SHARED_LIBRARY 自动推断但我建议显式写出来后面你想用LOCAL_MODULE_RELATIVE_PATH控制安装子目录时会更清晰。LOCAL_SRC_FILES填写参与编译的源文件支持 .c、.cpp、.S 汇编文件。多个文件用空格或换行分隔都可以注意路径是相对LOCAL_PATH的。如果你有一个目录不需要编译千万别写进去构建系统不会自动过滤。LOCAL_C_INCLUDES是头文件搜索路径等价于 gcc 里的 -I。我这里的写法是$(LOCAL_PATH)/include意思是把模块目录下的 include 目录加进去。有人喜欢写绝对路径比如/work/aosp/packages/apps/MyApp/jni/include这种写法一旦你换了 AOSP 根目录就废了永远用$(LOCAL_PATH)开头才是最稳的。LOCAL_CFLAGS是对 C 和 C 都生效的编译选项LOCAL_CPPFLAGS只对 C 生效。我加了-stdc17注意 Android 10 默认的 C 标准可能比你本地环境低如果你的源码用了 C17 语法但没这行编译会报一堆奇奇怪怪的错误比如std::optionalnot found。最后include $(BUILD_SHARED_LIBRARY)是整个 Android.mk 的触发器。前面所有LOCAL_*变量都只是“数据”这行 include 才真正让构建系统把这些数据组装成一条完整的编译规则。这就像你准备了一堆食材这行是“开火”。2.3 单编命令与产物验证在 AOSP 源码根目录先执行source build/envsetup.sh lunch 你的产品名-userdebug然后两种方式都能编这个模块m libmynative # 或者在模块目录下执行 mmmmm 目录在老教程里常见它可以指定任意目录编译Android 10 里依然可用。我个人更习惯用m libmynative因为它只看模块名不关心你现在在哪个目录。编完以后产物在out/target/product/product/system/lib64/libmynative.so如果你的产品是 32 位路径就变成out/target/product/product/system/lib/libmynative.so。同时你会在out/target/product/product/symbols/system/lib64/下找到一个带符号表但体积更大的同名文件那是调试专用副本。拿到产物后建议立刻做三件事验证file看看 ELF 是 32 位还是 64 位readelf -d看它的 DT_NEEDED 依赖了哪些库nm -D看导出符号里有没有Java_com_example_...。这一步能帮你提前发现位数不对、依赖缺失、符号没导出这三种最常见的病不用等到跑起来再炸。3. 别让符号和依赖毁掉一个能编译的产物链接细节与头文件导出3.1 三种“把库拉进来”的写法看起来都一样差别很远新手最容易在这块栽跟头。Android.mk 里有三种看起来都能“让模块用上别的库”的写法但机制完全不同。写法作用关键区别LOCAL_SHARED_LIBRARIES : liblog声明依赖另一个动态库模块参与依赖图解析能传递头文件、自动排序构建系统会管理它LOCAL_STATIC_LIBRARIES : libtinyxml把静态库内容直接链进当前 so产物形态发生变化静态库代码被合并进你的 so不产生运行时依赖LOCAL_LDLIBS : -lz -ldl直接给链接器传 -l 参数不参与模块依赖解析构建系统不知道你依赖了谁也不做自动排序用LOCAL_SHARED_LIBRARIES的时候你填的是模块名不是文件名。比如你要链 liblog就写liblog不要写-llog也不要写路径。构建系统会自动在依赖图里找到 liblog 模块链接时生成正确的-l参数和搜索路径。更重要的是如果你依赖的模块本身又依赖别的模块构建系统会做拓扑排序保证链接顺序不出问题编出来的 so 的 DT_NEEDED 也会带上完整传递依赖。LOCAL_LDLIBS则是完全绕过模块系统。它适合的场景是链接那些不在 AOSP 源码树里、直接由交叉编译工具链提供的库。但代价是构建系统不会为它建立依赖关系不会自动重编也不会帮你把头文件路径带过来。如果你用LOCAL_LDLIBS : -lz能编过并不代表模块系统里存在 zlib 模块只是链接器在 sysroot 里找到了 libz.so。这里有个非常坑人的点改成LOCAL_SHARED_LIBRARIES : libz的前提是 AOSP 里确实定义了 libz 这个模块而LOCAL_LDLIBS : -lz不关心这回事。能用模块名就用模块名少碰 LOCAL_LDLIBS。Android 的 bionic C 库和 Ubuntu 的 glibc 还有一个明显差异在 Ubuntu 上写 Makefile 经常要-lpthread -lrt在 Android.mk 里基本不需要pthread 相关的符号和大部分实时函数都在 libc.so 里。如果你从宿主机习惯里把这两个参数搬过来链接器会在 sysroot 里找 libpthread.so运气好能找到兼容壳运气不好直接报 cannot find -lpthread。3.2 头文件的传递与导出头文件路径默认是不会在模块之间自动传递的。A 模块的LOCAL_C_INCLUDES只对 A 模块自己生效B 模块依赖 A 模块B 的编译器命令里并不会自动出现 A 的头文件目录。那怎么让 B 模块能用上 A 模块的公开头文件答案是LOCAL_EXPORT_C_INCLUDE_DIRS。比如你编了一个 libfoo.so它的对外接口头文件放在模块目录的 include 下你希望任何依赖 libfoo 的模块都能直接#include foo.h你的 Android.mk 里应该这样写LOCAL_C_INCLUDES : $(LOCAL_PATH)/include LOCAL_EXPORT_C_INCLUDE_DIRS : $(LOCAL_PATH)/include前一行保证编译 libfoo 自己时能找到头文件后一行把这个路径“导出”给所有依赖 libfoo 的模块。我见过不少人只写了LOCAL_C_INCLUDES自己模块编得飞起下游模块一编译就报头文件找不到原因就在这。3.3 C/C符号问题为什么JNI必须包extern CC 编译器会对函数名做 name mangling也就是把函数名和参数类型揉成一个带_ZN之类的复杂符号。Java 层的 native 方法是通过Java_com_example_...这个固定名字去 dlsym 查找的它根本不知道 name mangling 这回事。所以 JNI 函数的定义必须用extern C包起来强制编译器按 C 规则导出原名。如果你忘了这句话编译依然会通过但nm -D看导出符号时会看到一大串_ZN...开头的东西运行时 dlopen 能成功JNI_OnLoad也能执行可一旦 system.loadLibrary 之后调用 native 方法就会报UnsatisfiedLinkError: No implementation found for ...。这个错很经典排查方向也固定先nm -D看符号再查 extern C。还有一些库作者会把公开 C 接口的头文件写成这种形式#ifdef __cplusplus extern C { #endif void my_native_api(void); #ifdef __cplusplus } #endif这是一种防御性写法让同一个头文件既能被 C 文件 include也能被 C 文件 include 并保持 C 链接规则。你在自己的动态库里对外提供 C 接口时建议照着这个模式写。3.4 控制动态库的符号可见性动态库默认情况下会导出所有非 static 的符号。带来的问题也很直接如果你的 so 里实现了一个和系统库同名但行为不同的函数某些情况下可能被别的模块优先匹配造成诡异的行为错乱。如果想收一下符号可以在LOCAL_CFLAGS里加-fvisibilityhidden这样默认所有符号都不导出再在需要导出的函数上显式标记可见性。JNI 导出函数由于宏JNIEXPORT已经展开了__attribute__((visibility(default)))和 JNICALL所以不需要额外处理普通 C 接口函数需要自己加__attribute__((visibility(default))) void my_export_api(void);还有一种更细的控制方式是链接脚本比如把导出符号表写进 export.map然后LOCAL_LDFLAGS : -Wl,--version-script$(LOCAL_PATH)/export.map这种方式适合对导出符号有严格要求的 SDK 型动态库。改动符号可见性之后务必用nm -D复查一遍导出列表别出现该导出的没导出、不该导出的漏出去一大片。4. so编完去哪里安装路径、PRODUCT_PACKAGES和vendor分区规则4.1 从out目录到system镜像so的旅程前面提到单编完的 so 会出现在out/target/product/product/system/lib64/下。这地方对应到最终运行设备上就是根文件系统里的/system/lib64。在 Android 10 里/system分区是挂载成根的/system/lib64就是系统 64 位动态库的集合地。新手经常有一个误解我 mmm 编出 so 了是不是它已经进 system.img 了不是。单编只是把产物丢到 out 目录对应的镜像目录结构里并没有触发打包。要让它真正进入 system.img 或 vendor.img你得把它写进PRODUCT_PACKAGES。打开你的设备产品配置文件一般在device/厂商/产品/device.mk里加上一行PRODUCT_PACKAGES libmynative这样整机打包时构建系统会从模块列表里找到 libmynative把它的输出产物路径纳入镜像文件清单。这也是整个编系统镜像是“由依赖关系驱动”的体现没有出现在 PRODUCT_PACKAGES 里的模块即使编出来了也进不了固件。4.2 指定安装位置RELATIVE_PATH与MODULE_PATH默认情况下SHARED_LIBRARIES 类别装到/system/lib64或/system/lib由构建系统根据模块位数决定。如果你想让 so 装到子目录比如 HAL 模块通常要进/system/lib64/hw/用LOCAL_MODULE_RELATIVE_PATHLOCAL_MODULE_RELATIVE_PATH : hw这行的意思是在默认安装目录下追加一个 hw 子目录。注意它是“追加”而不是重写绝对路径。所以搭配LOCAL_MODULE_CLASS : SHARED_LIBRARIES时最终路径就是/system/lib64/hw/。还有一个老写法叫LOCAL_MODULE_PATH可以直接写死绝对路径LOCAL_MODULE_PATH : $(TARGET_OUT_SHARED_LIBRARIES)/hwTARGET_OUT_SHARED_LIBRARIES是构建系统提供的变量指/system/lib64或 lib。这种写法在 Android 10 里仍然有效但可读性和可移植性不如LOCAL_MODULE_RELATIVE_PATH我建议新代码尽量用 relative path让构建系统自己推算出该去 system 还是 vendor。4.3 让so进入vendor分区和VNDK的约束如果你的库是给硬件相关的别的模块用的通常不能放在 system 分区而是要进 vendor 分区。Android 10 里最常见的两种写法LOCAL_PROPRIETARY_MODULE : true或者更语义化的LOCAL_VENDOR_MODULE : true这两种写法最终都会把 so 安装到/vendor/lib64或/vendor/lib。它们存在的意义不只是路径问题更重要的是构建系统会据此对你“能依赖谁”做检查。如果设备启用了 VNDK 机制vendor 模块不能随意链接 system 分区的私有库只能链接 LL-NDK 公开库和 VNDK 列表内的库。你编一个 vendor 动态库却去依赖一个只在 system 分区存在的私有库构建时会直接报错报错信息会带有 VNDK 字样并列出违规依赖。这时候别想着偷偷关掉检查老老实实把依赖换成公共库或者在 VNDK 扩展列表里加入你的私有库但后者涉及面更大通常需要系统负责人审批。这个约束是 Android 10 强化的设计逻辑目的是让 vendor 部分和 system 部分解耦未来系统升级不至于因为私有库变化导致设备驱动崩溃。4.4 32位/64位同编的关键参数Android 10 的产品配置里一般通过TARGET_SUPPORTS_32_BIT_APPS和TARGET_SUPPORTS_64_BIT_APPS控制支持位数。默认情况下一个动态库会跟随产品配置如果你整个产品只开 64 位so 就只编 64 位。但有些库必须同时给出 32 位和 64 位版本例如会被 32 位和 64 位进程同时加载的库。这时可以用LOCAL_MULTILIB : both强制编译两种位数的产物。如果你明确只要 64 位可以写LOCAL_MULTILIB : 64这能省掉一半的编译时间也避免 32 位代码编不过的尴尬。这里有个隐藏的坑如果你的源文件里有内联汇编或者引用了 32 位下不存在的外部库强制 both 会直接编不过。我建议新模块先跟随默认配置编一遍确认没问题后再考虑要不要补 32 位。另外验证产物时用file命令分别看 lib 和 lib64 目录下的 so确认是不是真的同版本同时间戳这能避免拿着 64 位的 so 放到 32 位进程里跑然后莫名其妙 ”dlopen failed“。5. 编译动态库时我踩过且经常在别人代码里看到的五个坑报错信息反推根因5.1 undefined reference头文件在并不代表链接库在现象永远是undefined reference to __android_log_print但头文件明明能找到你在源码里也#include android/log.h了。这个坑的迷惑性在于头文件提供了函数声明编译器很开心地编译完每一个 .cpp到了链接阶段却告诉你找不到实现。根因很简单你只给了编译器“函数长什么样”没给链接器“函数在哪里实现”。解决方式是在 Android.mk 里加上LOCAL_SHARED_LIBRARIES : liblog如果报了undefined reference to xxxxx但找不到任何明显头文件对应我的排查顺序是看报错符号是 C 风格还是 C mangled 风格确认是不是 extern C 问题。在源码树里搜索这个符号在哪个库中实现。确认该库有没有在LOCAL_SHARED_LIBRARIES或LOCAL_LDLIBS里。确认该库和你当前模块的位数一致。用nm -D查看该库的导出符号确认符号确实被导出。最后一步经常会查出“源码里明明实现了但没导出”的情况比如自定义函数加了 static、或者被 -fvisibilityhidden 藏起来了。5.2 编过push进去却dlopen failed传递依赖没有带走这个坑发生在“单编单个 so然后手动 adb push 到设备验证”的开发流程里。A.so 链接了 libB.solibB.so 又链接了 libC.so。你只 push 了 A.so 和 libB.so跑起来直接报dlopen failed: library libC.so not found。因为动态库的运行期依赖是传递的A 通过 DT_NEEDED 找到 BB 又通过自己的 DT_NEEDED 需要 C。你只带走了 A 和 BC 不在系统装载器自然崩溃。排查方法是用readelf -d A.so看软链接表的 DT_NEEDED 列表然后对每个依赖递归看下一级。AOSP 的交叉环境里没有完整的 ldd我用得最顺手的就是 readelfreadelf -d out/target/product/product/system/lib64/libmynative.so | grep NEEDED看到liblog.so、libcutils.so之类的名字后再逐个确认目标镜像里是否存在。手动 push 验证时最好的办法是把整个 out/target/product/ /system/lib64 同步到设备虽然笨但保证依赖不会漏。这个坑在 AOSP 里比在 Ubuntu 上更容易出现因为系统库版本很多你本地 system/lib64 里没有别人机器的库很正常。5.3 cannot find -lxxx把宿主机习惯带进交叉编译的恶果这种报错通常是你在LOCAL_LDLIBS里写了-lglut、-lGL、-lX11这类宿主机图形库或者-lpthread -lrt。链接器报cannot find -lglut原因是 AOSP 的交叉编译工具链 sysroot 里根本没有这些库。你在 Ubuntu 上能编过是因为宿主机/usr/lib/x86_64-linux-gnu里有但 Android 的目标环境是另一套库集合。这和我在 Ubuntu 下编 Qt 5.15.2 的 MySQL 驱动时犯的错一模一样宿主机有 libmysqlclient交叉编译环境里没有凭宿主机印象去写依赖必然翻车。遇到 cannot find -lxxx第一时间确认两个方向你写的库是不是真的存在于 Android 目标环境如果不存在得用别的实现替代。如果存在但不在默认搜索路径要用LOCAL_SHARED_LIBRARIES去引用 AOSP 内模块而不是自己加 -L。别把宿主机 Makefile 的习惯直接复制到 Android.mk 里这两个世界的 sysroot 差异比你想象的大得多。5.4 multiple definition链接器嫌符号重复multiple definition of xxx这个报错出现时链接器已经在收尾阶段了。常见场景是你同时依赖了两个静态库而这两个静态库内部包含了同名符号的 .o 文件或者同一个静态库被加了两次。和动态库不同静态库的代码最终是要被“复制”进你的 so 的。两个静态库里都有同一个函数实现复制两次就冲突了。更隐蔽的是LOCAL_WHOLE_STATIC_LIBRARIES它会把静态库里的所有 .o 强制整个拉入如果你同时又用LOCAL_STATIC_LIBRARIES引了同一个小库重复概率更高。排查时可以用 nm 在多个 .a 文件里找同一个符号nm -A out/target/product/product/obj/STATIC_LIBRARIES/*_intermediates/*.a | grep T xxx找到重复来源后删掉冗余依赖。这里特别提醒依赖库之间也存在传递你可能没有直接写重复库但甲库依赖乙库你又直接依赖了乙库间接加直接就重复了。构建系统对动态库的重复依赖有合并机制对静态库的重复依赖合并没有你想的那么智能。5.5 VNDK限制vendor库不是想链谁就链谁最后一个坑来自 Android 10 的 VNDK 机制。你编一个 vendor 分区动态库很正常地链了libhidlbase或者某个只在 system 分区存在的私有库编译日志里突然出现红色报错error: VNDK restriction: module ... should not link to ...第一反应通常是“为什么我之前编过没这问题”大概率是你之前编的 product 没开严格 VNDK 检查现在换了一个开启了BOARD_VNDK_VERSION : current的产品。Android 10 下vendor 模块能链接的 system 库范围被收紧。想快速验证是不是 VNDK 限制导致的报错开头一般会带VNDK字样。临时解决办法是去掉违规依赖或者把依赖库也改成 vendor 模块。长期设计上vendor 和 system 之间的动态库边界一定要在架构阶段就定好尤其是做 HAL、sensor、camera 相关模块时别等到整机编译在最后一步才爆出来。这个错虽然叫“编译期报错”但其实是架构层面的问题。6. 从Android.mk到Android.bp迁移不是重写是等价翻译6.1 变量对照表与一个可用的bp例子很多模块负责人会问要不要把所有 mk 都改成 bp我的答案很直接新模块直接写 bp存量模块只要还在正常编不改。但如果你已经决定要迁借用 androidmk 工具或者手工重写之前先记住下面这个等价表。Android.mk 写法Android.bp 等价写法LOCAL_MODULE : libfoocc_library_shared { name: libfoo }LOCAL_SRC_FILES : a.cpp b.cppsrcs: [a.cpp, b.cpp]LOCAL_SHARED_LIBRARIES : liblogshared_libs: [liblog]LOCAL_STATIC_LIBRARIES : libtinyxmlstatic_libs: [libtinyxml]LOCAL_C_INCLUDES : $(LOCAL_PATH)/includecflags: [-Ipath] 或用 export_include_dirsLOCAL_MODULE_RELATIVE_PATH : hwrelative_install_path: hwLOCAL_PROPRIETARY_MODULE : trueproprietary: trueLOCAL_VENDOR_MODULE : truevendor: trueLOCAL_MULTILIB : 64multilib: { lib64: { enabled: true } }对应前面那个 JNI 例子Android.bp 版本是这样cc_library_shared { name: libmynative, srcs: [native-lib.cpp], shared_libs: [liblog], export_include_dirs: [include], cflags: [-Wall, -Wextra], cppflags: [-stdc17], }注意 bp 里的export_include_dirs是模块自己的 include 目录相对于模块目录的路径它同时起到 mk 里LOCAL_C_INCLUDES和LOCAL_EXPORT_C_INCLUDE_DIRS两个作用。如果你只想要内部头文件不导出可以只在 cflags 里加 -I但要公开给别的模块就用export_include_dirs。6.2 androidmk自动转换的边界AOSP 里自带一个工具叫 androidmk在source build/envsetup.sh之后可以直接用androidmk Android.mk Android.bp它的输出只能当作草稿。原因是它做的是“语法等价翻译”但很多 mk 里的惯用法它处理不好。比如 mk 里经常出现ifeq ($(TARGET_ARCH), arm64)这种条件分支androidmk 会把它转成 bp 里的arch:变体吗不一定。再比如自定义函数$(call)这个工具基本无能为力。我的建议是简单模块也就是没有复杂条件分支、没有自定义宏、没有多个 include 的模块可以自动转但要人工 review 一遍生成结果复杂模块别转手写 bp 反而更快更清晰。转换完以后一定要用m 模块名重新全量编一次确认产物和原来一致尤其是 so 的导出符号列表用 nm 对比。6.3 什么情况下我劝你暂时别迁移Android.mk 和 Android.bp 在 Android 10 里是能共存的mk 模块可以被 bp 模块用 shared_libs 依赖bp 模块也可以被 mk 模块依赖因为它们最终都汇入同一个 Ninja 图链接器看的是产物文件路径不关心模块声明语法。所以“迁移”不是一种生存压力更多是维护体验的选择。如果以下条件满足我劝你把迁移排期往后放模块已经被十几个其他模块依赖每个依赖都要跟着验证回归。模块核心逻辑里塞了大量ifeq、TARGET_*条件这些内容迁移成 bp 的变体语法后可读性不一定更好。团队里只有你一个熟 Android.mk而产品马上要发版改动风险远大于收益。我个人经历过一次把二十多个 mk 库统一迁 bp 的“大工程”最后发现真正有价值的其实只有其中一个被反复修改的模块其他大多是白迁。从那以后我定了个规矩哪个模块未来三个月内有明确的功能改动计划才考虑迁移只是躺着不动的库别碰。在实际项目里编译系统没有绝对的“新旧”只有“合不合适”。Android.mk 编译动态库这件事核心是先弄懂变量语义再弄清产物去向最后掌握报错排查手段。你把这些想透了遇到 Android.bp 也只是换套语法底层还是同一套编译、链接、打包逻辑。