资讯动态

Android.mk编译动态库:核心语法、依赖管理与常见报错实战

发布时间:2026/10/4 4:41:27 来源:尧图企业网站定制
Android 10 的编译系统里Android.mk 可能是绕不开也必须学会的东西。我写这个系列到第十篇前几篇分别讲了根文件系统怎么裁剪、内核怎么编译、SELinux 怎么配置今天终于讲到一个偏实操、偏编译细节的主题——用 Android.mk 编译动态库。为什么要单独把这个话题拎出来因为动态库在 Android 系统里的地位太特殊了。你做的任何一个 Native 模块要么本身就是一个.so要么最终要链接一堆.so。系统里跑着的 servicemanager、surfaceflinger、各种 HAL 实现本质上全是动态库再由 init 进程按需加载。学懂了 Android.mk 编译动态库你就等于拿到了进系统开发这扇门的钥匙。这篇文章先从 Android 10 编译系统的现状说起再讲 Android.mk 的核心语法接着用一个完整的示例走一遍编译流程最后把依赖管理、常见报错的排查思路一并整理出来。适合正在做 ROM 定制、HAL 开发、系统 App 集成 Native 库的工程师参考。1. Android 10 的编译系统现状Android.mk 还有没有用很多新入行的朋友会问我Android 10 都已经用 Soong/Blueprint 了Android.bp 才是官方推荐的写法为什么还要花时间学 Android.mk这是个好问题但实际情况比想象中复杂。1.1 新旧构建系统的过渡期Android 7.0 引入了 Soong用 Android.bp 替代 Android.mk到 Android 10 时大部分核心模块都已经迁移到.bp文件了。但是注意迁移不是一蹴而就的。AOSP 源码树里仍然保留了大量 Android.mk尤其是厂商私有模块、老牌硬件抽象层、第三方预编译库很多还在用 Make 语法。还有一个更关键的原因如果你做的是 BSP 适配、芯片厂商 SDK 集成拿到的往往是全套 Android.mk 的老代码。高通、MTK 早期释放的很多模块到现在都还是.mk后缀。你不能说系统换代了就把这些代码全部重写一遍不现实。所以 Android 10 编译系统专门搞了一个兼容层Make 时代的 Android.mk 会通过 Kati 转换成 Ninja 文件再参与最终构建。这意味着 Android.mk 在 Android 10 里依旧能跑而且和 Android.bp 可以共存。1.2 动态库在编译产物中的位置动态库的编译结果最终会放进系统镜像的不同分区分区32 位路径64 位路径典型用途system/system/lib/system/lib64系统核心库、应用依赖库vendor/vendor/lib/vendor/lib64HAL 实现、厂商私库product/product/lib/product/lib64产品定制库recovery/system/lib—恢复模式用库Android.mk 里通过LOCAL_MODULE_PATH可以指定最终安装路径默认情况下会根据LOCAL_MODULE_CLASS自动放到system/lib下。这个下面会细讲。1.3 为什么还要理解 Make 的变量机制说得直白一点Android.bp 是一套声明式语法告诉你有什么、依赖啥Android.mk 则是一套命令式语法里面可以写循环、条件、函数调用灵活性比 bp 高得多。早期 Android 系统就是用纯 Make 搭建起来的很多精巧的模板函数比如my-dir、all-subdir-makefiles都是 Make 时代的遗产。理解了这层背景你就明白为什么现在还要学 Android.mk。不是因为它新、因为它好而是因为它存量巨大、兼容性稳、你早晚会遇到。2. Android.mk 核心语法变量与函数是灵魂Android.mk 本质上就是 GNU Make 的扩展语法Google 在它上面封装了一整套模块描述规则。你编写的不是普通 Makefile而是给 Android 编译系统提供这个模块怎么构建的描述文件。2.1 最基础的骨架结构任何一个 Android.mk 文件开头基本长这样LOCAL_PATH : $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE : libexample LOCAL_SRC_FILES : example.c include $(BUILD_SHARED_LIBRARY)这四行大概是 Android.mk 的最小可运行版本了。拆开来看LOCAL_PATH定位当前模块所在目录my-dir是编译系统提供的函数返回当前 Android.mk 所在路径。CLEAR_VARS清空所有LOCAL_开头的变量。这个很重要因为整个 Android 编译过程是全局共享一套变量的不在每个模块开始前清空你会拿到上一个模块的残留配置。LOCAL_MODULE模块名会直接映射到生成的库文件名。比如这里定义libexample最终产物就是libexample.so。BUILD_SHARED_LIBRARY告诉编译系统按照动态库的规则来处理这个模块。2.2 必须掌握的关键变量除了上面提到的几个下面这张表里的变量是编译动态库时最高频会碰到的变量作用补充说明LOCAL_SRC_FILES源文件列表支持 .c、.cpp、.S路径相对 LOCAL_PATHLOCAL_C_INCLUDES头文件搜索路径编译时用 -I 传入但不会自动导出给依赖者LOCAL_EXPORT_C_INCLUDES导出的头文件路径别人链接你的库时自动加到头文件搜索路径LOCAL_SHARED_LIBRARIES链接的动态库依赖编译时会自动添加链接参数和头文件路径LOCAL_STATIC_LIBRARIES链接的静态库依赖静态库会被打包进当前模块LOCAL_LDLIBS额外的链接参数常用于 -llog 这类系统库也可传 -L 路径LOCAL_CFLAGSC 编译选项比如 -DXXX1、-O2、-WallLOCAL_CPPFLAGSC 编译选项只在编译 .cpp 时生效LOCAL_MODULE_CLASS模块类型SHARED_LIBRARIES、STATIC_LIBRARIES、EXECUTABLES、ETC 等LOCAL_MODULE_PATH指定输出路径覆盖默认放置位置LOCAL_MULTILIB32/64 位策略both 表示同时编译两种架构LOCAL_STRIP_MODULE是否 strip 符号默认会 strip调试期可以设为 false2.3 动态库和静态库编译的差异很多人一开始搞不清BUILD_SHARED_LIBRARY和BUILD_STATIC_LIBRARY的区别。一句话总结动态库的.so是在运行期被加载的静态库的.a是编译期被合并进可执行文件或动态库的。对应到系统表现动态库单独生成一个.so文件有独立的文件路径。静态库不会出现在最终镜像里它的代码被复制到了用到它的模块里。所以在 Android.mk 里静态库通常不需要指定LOCAL_MODULE_PATH也不会被安装到系统分区。如果你写了一个纯内部实现的中间层库不想暴露为外部接口那更适合编成静态库减少系统镜像体积。3. 实战用 Android.mk 编译一个动态库讲完语法直接上一段完整的实操。我以一个简单的数学工具库为例走一遍从源码到.so产物的全过程。3.1 准备源码目录假设我做的模块叫libsimplemath放置在vendor/xxx/simplemath/下面。目录结构如下vendor/xxx/simplemath/ ├── Android.mk ├── simplemath.h └── simplemath.csimplemath.h 内容#ifndef SIMPLEMATH_H #define SIMPLEMATH_H int add(int a, int b); int multiply(int a, int b); #endifsimplemath.c 内容#include simplemath.h int add(int a, int b) { return a b; } int multiply(int a, int b) { return a * b; }3.2 编写 Android.mkLOCAL_PATH : $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE : libsimplemath LOCAL_MODULE_CLASS : SHARED_LIBRARIES LOCAL_SRC_FILES : simplemath.c LOCAL_C_INCLUDES : $(LOCAL_PATH) LOCAL_EXPORT_C_INCLUDES : $(LOCAL_PATH) LOCAL_CFLAGS : -Wall -Werror include $(BUILD_SHARED_LIBRARY)这里我额外做了几件事LOCAL_MODULE_CLASS : SHARED_LIBRARIES显式声明这是一个动态库模块。其实 BUILD_SHARED_LIBRARY 已经隐含了这个设定但显式写出来可以让别人阅读代码时更直观。LOCAL_EXPORT_C_INCLUDES把当前目录导出给依赖者这样其他模块只要在LOCAL_SHARED_LIBRARIES里加上libsimplemath就能自动找到simplemath.h。-Wall -Werror把警告当错误处理。这属于个人强迫症在系统代码里比较常见可以减少低级失误。3.3 编译命令与产物确认在 Android 10 源码根目录执行source build/envsetup.sh lunch aosp_arm64-eng mmm vendor/xxx/simplemath/mmm指定模块目录编译只构建该目录下的模块相对make全量编译快得多。如果只是想在当前目录里直接编可以用mm。编译完成后检查产物ls -l out/target/product/generic/system/lib64/libsimplemath.so ls -l out/target/product/generic/system/lib/libsimplemath.so64 位产物在lib6432 位在lib。因为我没有设置LOCAL_MULTILIB默认会跟随TARGET_ARCH生成对应位数的库我 lunch 的是 arm64所以主要看lib64下的结果。提示mmm编译时日志里会有两段关键输出make ... libsimplemath表示进入模块构建Install: out/target/product/generic/system/lib64/libsimplemath.so表示安装到系统镜像目录完成。3.4 在另一个模块里链接这个库动态库编译出来是要给别人用的。假设同目录下有个可执行程序math_test要调用它对应的 Android.mk 是这样LOCAL_PATH : $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE : math_test LOCAL_SRC_FILES : main.c LOCAL_SHARED_LIBRARIES : libsimplemath LOCAL_C_INCLUDES : $(LOCAL_PATH) include $(BUILD_EXECUTABLE)关键就在LOCAL_SHARED_LIBRARIES : libsimplemath这一行。编译系统会自动确保 libsimplemath 先被编译。给当前模块添加-L路径和-lsimplemath链接参数。如果 libsimplemath 声明了LOCAL_EXPORT_C_INCLUDES还会自动把对应头文件路径加到编译参数里。这样你在 main.c 里直接写#include simplemath.h就能编译通过。4. 依赖管理与链接细节别把环境变量当玩具这个部分最容易被忽略也是实际报错的重灾区。搞懂了依赖怎么传递、链接参数怎么生效你就能解决 90% 的编译问题。4.1 LOCAL_SHARED_LIBRARIES 和 LOCAL_LDLIBS 到底选哪个这俩是最容易混淆的一对。我见过不少新手在 Android.mk 里用LOCAL_LDLIBS : -lfoo去链接一个系统里的库结果编出来的东西在目标机器上运行时报cannot find -lfoo原因就是对依赖管理理解不到位。LOCAL_SHARED_LIBRARIES是给编译系统看的它不只是加了一个链接参数还会构建依赖关系当前模块依赖的库会被优先编译并且相关头文件路径、导出符号都会被正确传递。你写的是模块依赖而不是链接一个库。LOCAL_LDLIBS则是纯粹的链接参数透传等价于直接往ld命令后面追加参数。它不会触发任何依赖构建逻辑。一般情况下优先使用 LOCAL_SHARED_LIBRARIES。只有链接系统库比如 libc、libm、libdl、liblog这种必然存在的库时才考虑直接-l传参。一个例外情况如果你要链接的是 NDK 里提供的预编译库且目标库已经存在于系统镜像中用LOCAL_LDLIBS是可以的。但你在 AOSP 源码树内编译自己的模块更规范的做法仍然是把预编译库声明为BUILD_SHARED_LIBRARY或BUILD_PREBUILT模块再通过LOCAL_SHARED_LIBRARIES引用。4.2 符号可见性为什么自己的函数没被导出去动态库默认会导出所有非 static 函数的符号但从 Android 6.0 开始系统默认加上了-fvisibilityhidden的编译选项目的是减少动态符号表、提升加载性能。这就导致一个常见现象你写的全局函数在 .c 文件里明明是外部可见的编成 .so 之后却调不到。解决办法有三种在函数声明上显式加可见性属性__attribute__((visibility(default))) int my_api_function(int arg);在 Android.mk 里取消隐藏LOCAL_CFLAGS : -fvisibilitydefault使用LOCAL_EXPORT_CFLAGS给依赖者传递编译选项而不是直接改自己的更推荐。LOCAL_EXPORT_CFLAGS : -fvisibilitydefault检查符号是否导出的命令readelf --dyn-syms out/target/product/generic/system/lib64/libsimplemath.so4.3 依赖链的传递问题A 模块依赖 B 模块B 模块依赖 C 模块。当你把 A 链接到 B 时A 并不会自动获得 C 的符号。这是一个经典的传递依赖陷阱。看一个实际场景libmath_test依赖了libutils而libutils依赖了liblog。如果你在math_test的代码里直接调用了__android_log_print这是 liblog 提供的 API但没有在自己的 Android.mk 里声明LOCAL_SHARED_LIBRARIES : liblog那么编译可以过因为链接时你可能加了-Wl,--no-undefined没开但运行到那行代码会直接抛undefined symbol错误。我的建议是每个模块都显式声明自己直接调用的库不要依赖间接传递。虽然系统库在运行期可能会把符号解析到全局但这种能跑就行的习惯早晚会被某个特殊模块坑到。5. 常见报错与排查技巧实录这部分整理的是我这些年编译动态库时踩过的坑。每一条都对应一个真实的报错现象和定位思路。5.1 报错 No rule to make target ... needed by ...这个多半是LOCAL_SRC_FILES里写的文件路径对不上。常见原因源文件不在LOCAL_PATH下但路径没写完整。文件名大小写写错了Linux 对大小写敏感。源文件还没被 git 纳入跟踪在某些编译环境下文件在磁盘上存在但构建系统没感知到。排查思路先确认文件存在再检查 Android.mk 里写的路径是否以LOCAL_PATH为基准。5.2 报错 undefined reference to xxx动态库编译时最常见的就是这个。分两类第一类你自己的代码里引用了一个函数但没实现也没链接对应的库。解决办法是先确认函数在哪个库里grep -r 函数名 out/soong/.intermediates/ --include*.so -l更直接的办法是往代码里加日志或注释确认到底是哪一行引入了这个符号。第二类链接顺序问题。GNU ld 在解析符号时是从左到右处理的如果一个库 A 引用了库 B 的符号那么链接命令里 A 必须在 B 前面。Android 编译系统会自动处理LOCAL_SHARED_LIBRARIES里的顺序但如果你在LOCAL_LDLIBS里手动加了-l参数就得自己保证顺序正确。5.3 报错 multiple definition of xxx同一个函数在多处定义。常见场景是A 模块和 B 模块都有同一个全局函数同时被当前模块链接了。解决思路不要写全局函数改成 static 函数。用LOCAL_CFLAGS : -Dxxx加宏隔离。检查依赖方是否重复链接了同一个静态库。5.4 报错 cannot find -lxxx链接器找不到你指定的库。思路这个库是否编译出来了去out/target/product/product/obj/lib或obj_arm/lib下面确认。是否把LOCAL_LDLIBS和LOCAL_SHARED_LIBRARIES搞混了前者不会触发依赖库的构建。库的位数对不对64 位模块链接 32 位库一定会报这个错。5.5 报错 module xxx already defined模块名冲突了。整个系统镜像里模块名必须是唯一的。排查方式grep -r LOCAL_MODULE : libsimplemath --includeAndroid.mk .看看是不是已经有一个模块占用了这个名字。如果有改掉其中一个的名字。提示Android 10 的编译系统会把 Android.mk 和 Android.bp 里的模块统一管理所以也要搜一下同名模块是否在 .bp 里定义了。5.6 编译时找不到头文件fatal error: xxx.h: No such file or directory的处理思路头文件是否在LOCAL_C_INCLUDES指定的路径里头文件是否在依赖库的LOCAL_EXPORT_C_INCLUDES里导出如果只是自己用可以临时用绝对路径定位但要根治最好把头文件放到约定俗成的 include 目录里。我常用的排查命令kati -f build/core/main.mk --regen out/build_aosp_arm64.ninja然后去看生成的ninja文件里当前模块编译命令的-I参数是否包含了目标头文件路径。Ninja 文件是文本格式直接搜模块名就能定位。6. Android.mk 与 Android.bp 的转换迟早要面对既然 Android.bp 是大趋势我这里简单说一下两者之间的关系避免你换了项目就懵。6.1 Soong 提供的转换工具Android 10 源码树里有一个工具叫androidmk可以将基础语法的 Android.mk 转换成 Android.bp。使用方法source build/envsetup.sh androidmk Android.mk Android.bp不过实际转换出来的代码经常需要手工调整。它擅长转换简单的模块声明遇到条件判断、循环调用这种 Make 高级语法就无能为力了。6.2 动态库模块的 bp 写法对应上面 libsimplemath 的 Android.bpcc_library { name: libsimplemath, srcs: [simplemath.c], cflags: [-Wall, -Werror], export_include_dirs: [.], }看到没有结构和 Android.mk 基本一一对应cc_library对应BUILD_SHARED_LIBRARYname对应LOCAL_MODULEsrcs对应LOCAL_SRC_FILEScflags对应LOCAL_CFLAGSexport_include_dirs对应LOCAL_EXPORT_C_INCLUDES理解 Android.mk 后转过去也就一两天的事。6.3 两者混用时的注意事项一个系统镜像里可以同时存在 Android.mk 和 Android.bp 模块但有几个雷区模块名不能重复。哪怕一个是 .mk 一个是 .bp重名就是冲突。依赖关系可以跨类型。Android.mk 的模块可以依赖 Android.bp 的模块反过来也行编译系统会统一解析。优先用 bp 写新模块。存量 .mk 能不改就不改迁移属于锦上添花不是必须。7. 关于动态库编译的一些个人体会文章写到这核心内容基本讲完了。最后说几点我自己的实操感受。Android.mk 编译动态库这件事单独看似乎不难几行配置就行。但真正难的是搞清楚它背后那套依赖关系、变量传递机制和构建顺序逻辑。很多时候报错并不是语法写错了而是对模块之间的关系理解不到位。一个好的习惯是每个动态库模块在一开始就明确三件事——它对外提供哪些头文件、它依赖哪些别的模块、它的输出路径应该在哪。想清楚再动手写 Android.mk 就是填空题了。另外就是别小看LOCAL_EXPORT_C_INCLUDES和LOCAL_SHARED_LIBRARIES这两个变量的组合使用。它们能让你的模块依赖关系变得非常清晰新同事拿到代码看一眼就能明白这个库能不能直接用、该怎么用。这比写一长串注释有用得多。如果你正在做 Android 10 的系统开发我的建议是把这篇文章里的示例完整敲一遍然后看一下out/soong/.intermediates/vendor/xxx/simplemath/下生成的ninja文件你会发现编译系统其实把每个步骤都记录得特别清楚。理解了这一层Android.mk 对你来说就不再是黑盒了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑