5、Android.mk详解(下):LOCAL_STATIC_LIBRARIES、LOCAL_SHARED_LIBRARIES、LOCAL_CFLAGS、LOCAL_LDLIBS、多模块编译
好,咱们接着聊 Android.mk 的下半部分。上一节我们把模块声明、源文件、头文件路径这些基础配置讲透了。这一节,我重点说说链接库、编译选项,还有多模块怎么组织。
说白了,一个模块不可能活在真空里。你写的代码总要依赖别人写好的东西——要么是静态库,要么是动态库。怎么把这些依赖关系理清楚,是 Android.mk 里最考验功力的地方。
5.1 LOCAL_STATIC_LIBRARIES:静态链接
先看静态库。这个变量用来声明你的模块需要链接哪些 .a 文件。
LOCAL_STATIC_LIBRARIES := \
liblog \
libutils \
libtinyxml2
静态库的链接方式,说白了就是把库的代码直接复制到你的最终产物里。这样做的好处是:你的模块独立性强,拿到哪都能跑。坏处呢?产物体积会变大,而且如果多个模块都链接了同一个静态库,那内存里就有多份拷贝。
这里有个坑要注意:静态库的链接顺序是有讲究的。GCC 链接器是从右向左解析符号的。如果你写成:
LOCAL_STATIC_LIBRARIES := libA libB libC
链接器会先处理 libC,再 libB,最后 libA。如果 libA 里引用了 libB 的符号,那没问题。但如果 libB 引用了 libA 的符号,就会报「未定义引用」的错误。
5.2 LOCAL_SHARED_LIBRARIES:动态链接
动态库就灵活多了。它只在运行时加载,编译时只做符号检查。
LOCAL_SHARED_LIBRARIES := \
libbinder \
libcutils \
libhardware
动态库的好处很明显:节省空间,多个模块共享一份代码。但代价是:你的模块对环境有依赖——目标系统上必须存在这些 .so 文件。
嗯,这里要特别说一下:LOCAL_SHARED_LIBRARIES 和 LOCAL_STATIC_LIBRARIES 可以混用。比如你的模块既链接一个静态的日志库,又链接一个动态的 Binder 库,完全没问题。
| 特性 | 静态库 (.a) | 动态库 (.so) |
|---|---|---|
| 链接时机 | 编译时 | 运行时 |
| 产物体积 | 变大 | 不变 |
| 部署依赖 | 无 | 需要 .so 文件 |
| 多模块共享 | 多份拷贝 | 单份拷贝 |
5.3 LOCAL_CFLAGS:编译选项的调校
这个变量用来传递额外的 C/C++ 编译参数。你想想看,有时候我们需要打开某些宏定义,或者关闭某些警告,就得靠它。
LOCAL_CFLAGS := \
-DDEBUG_ENABLED=1 \
-Wno-unused-variable \
-O2
常用的几个场景:
- -D 定义宏:比如
-DLOG_TAG=\"MyModule\",这样代码里就能直接用LOG_TAG这个宏。 - -W 控制警告:
-Werror把警告当错误,-Wno-unused忽略未使用变量的警告。 - -O 优化级别:
-O0不优化(调试用),-O2标准优化,-Os优化体积。
我在项目中遇到过一个问题:某个第三方库的代码里用了很多 C++11 的特性,但默认编译标准是 C++98。加上 -std=c++11 就解决了。这种问题排查起来其实挺费时间的,因为编译错误信息往往不直接告诉你「标准不对」。
5.4 LOCAL_LDLIBS:链接系统库
这个变量用来链接宿主系统(比如 Linux)上的库,而不是 AOSP 源码树里的库。
LOCAL_LDLIBS := \
-lz \
-ldl \
-llog
注意看,这里用的是 -l 前缀,跟 gcc 命令行一模一样。-lz 链接 libz.so,-ldl 链接 libdl.so。
什么时候用 LOCAL_LDLIBS?
- 你的模块需要链接 NDK 提供的系统库
- 或者链接宿主机上的特定库(比如某些测试工具)
5.5 多模块编译:一个 Android.mk 搞定多个模块
一个 Android.mk 文件里可以定义多个模块。怎么做呢?很简单,每个模块用 include $(BUILD_*) 结束就行。
# 模块1:静态库
include $(CLEAR_VARS)
LOCAL_MODULE := libmystatic
LOCAL_SRC_FILES := static_impl.cpp
include $(BUILD_STATIC_LIBRARY)
# 模块2:动态库
include $(CLEAR_VARS)
LOCAL_MODULE := libmyshared
LOCAL_SRC_FILES := shared_impl.cpp
LOCAL_SHARED_LIBRARIES := libbinder
include $(BUILD_SHARED_LIBRARY)
# 模块3:可执行文件
include $(CLEAR_VARS)
LOCAL_MODULE := myapp
LOCAL_SRC_FILES := main.cpp
LOCAL_STATIC_LIBRARIES := libmystatic
LOCAL_SHARED_LIBRARIES := libmyshared
include $(BUILD_EXECUTABLE)
你看,三个模块写在一个文件里,清晰明了。每个模块之间用 include $(CLEAR_VARS) 隔开,互不影响。
我个人习惯把功能相关的模块放在同一个 Android.mk 里。比如一个 HAL 实现,它可能包含一个动态库(给上层调用)、一个测试可执行文件、一个配置文件生成工具。放在一起,维护起来方便。
LOCAL_MODULE_TAGS 来控制哪些模块参与编译。比如 LOCAL_MODULE_TAGS := optional 表示可选模块,只有被依赖时才会编译。
5.6 知识体系总览
为了让你对整个 Android.mk 的依赖和编译体系有个直观认识,我画了一张图:
这张图把今天讲的内容串起来了。你看,一个模块可以同时依赖静态库、动态库和系统库,每个依赖都可以配合 LOCAL_CFLAGS 做编译选项调校。最终产出的可能是 .a、.so 或者可执行文件。
好了,Android.mk 的核心变量就这些。你只要把 LOCAL_STATIC_LIBRARIES、LOCAL_SHARED_LIBRARIES、LOCAL_CFLAGS、LOCAL_LDLIBS 这四个变量搞明白,再配合多模块编译的技巧,大部分场景都能应付。