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 的符号,就会报「未定义引用」的错误。

我曾经踩过的坑: 有一次我写了一个网络库 libnet,它依赖了加密库 libcrypto。我把顺序写反了,结果编译报错。查了半天才发现是链接顺序的问题。从那以后,我每次写 LOCAL_STATIC_LIBRARIES 都会把被依赖的库放在右边。

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 优化体积。
重要提醒: LOCAL_CFLAGS 是全局生效的,会影响模块里所有源文件。如果你只想对单个文件设置特殊选项,可以用 LOCAL_SRC_FILES 配合模块级编译规则,但那属于进阶用法了。

我在项目中遇到过一个问题:某个第三方库的代码里用了很多 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 提供的系统库
  • 或者链接宿主机上的特定库(比如某些测试工具)
注意: LOCAL_LDLIBS 和 LOCAL_SHARED_LIBRARIES 不要混用。前者链接的是系统路径下的库,后者链接的是 AOSP 源码树里编译出来的库。搞混了会导致链接失败或者运行时找不到库。

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 的依赖和编译体系有个直观认识,我画了一张图:

Android.mk 编译依赖体系 Android.mk 模块 LOCAL_STATIC_LIBRARIES LOCAL_SHARED_LIBRARIES LOCAL_LDLIBS LOCAL_CFLAGS LOCAL_CFLAGS LOCAL_CFLAGS .a 静态库 .so 动态库 可执行文件

这张图把今天讲的内容串起来了。你看,一个模块可以同时依赖静态库、动态库和系统库,每个依赖都可以配合 LOCAL_CFLAGS 做编译选项调校。最终产出的可能是 .a.so 或者可执行文件。

好了,Android.mk 的核心变量就这些。你只要把 LOCAL_STATIC_LIBRARIES、LOCAL_SHARED_LIBRARIES、LOCAL_CFLAGS、LOCAL_LDLIBS 这四个变量搞明白,再配合多模块编译的技巧,大部分场景都能应付。


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