ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android 10 用 Android.mk 编译动态库:核心语法、常见坑与集成实践

Android 10 用 Android.mk 编译动态库:核心语法、常见坑与集成实践 做系统开发的这几年几乎每天都要跟Android.mk打交道。尤其在 Android 10 这个版本上虽然 Google 已经在推 Soong也就是Android.bp但老项目、三方库、硬件厂商的 SDK 里仍然大量沿用Android.mk体系。这篇博文是我「Android 10 根文件系统和编译系统」系列的第 10 篇单独把「用 Android.mk 编译动态库」这件事拎出来写透原因是动态库.so在整个 Android 系统里的地位太特殊了——它是系统进程、HAL 层、JNI 以及应用层 Native 代码之间的桥梁只要链接关系出了问题轻则某个功能失效重则开机直接 boot loop。文章面向两类读者一类是刚接触 Android 系统编译、想搞清楚 mk 语法规则的初级工程师另一类是在移植或者集成第三方库时被各种undefined reference、cannot find -lxxx折腾过的同学。我会从 mk 的底层逻辑讲起到实际编译一个可用动态库的完整流程最后把我在 Android 10 上踩过的坑和排查思路整理出来。1. 为什么还在用 Android.mk动态库编译的底层逻辑1.1 编译系统殊途同归mk 与 bp 的关系不少新入行的同学会问Android 10 不是早就用Android.bp了吗为什么还要学Android.mk这个问题的答案其实就是 Android 编译系统的历史包袱和现实选择。Android 从 7.0 开始引入 Soong 构建系统但在相当长的时间内两条体系并行。到了 Android 10系统源码里依然有大量Android.mk文件存在尤其是硬件厂商的 BSP、老版本移植过来的模块、部分 vendor 下的预编译库统统还在用 mk 描述。无论Android.mk还是Android.bp最终都要被转换成 Ninja 文件交给构建系统执行。Android.mk走的是 kati 转换器逻辑用 Makefile 语法表达Android.bp走的是 soong_build逻辑用类似 JSON 的 Blueprint 语法表达。本质上两者干的是同一件事描述模块怎么编译、链接什么库、输出什么东西。mk 的语法更灵活也因此更容易写出“魔法逻辑”bp 语法更严格适合大规模并行构建。读 mk 文件时要有一个观念转变它不是像 Shell 脚本那样从上到下执行就完了而是会被 kati 解析成一张依赖图。include $(CLEAR_VARS)清空的是模块描述变量不是文件系统上的东西LOCAL_MODULE给模块起名字include $(BUILD_SHARED_LIBRARY)触发把当前目录下的源码构建成动态库。理解了这个模型你在写 mk 时才会知道哪些变量该在哪里赋值哪些变量不能乱动比如TARGET_ARCH这些只读变量是由构建系统帮你设好的自己覆盖多半会出事。1.2 动态库在 Android 10 系统里的角色动态库在 Android 系统里几乎无处不在。/system/lib和/system/lib64下存放的是系统库/vendor/lib和/vendor/lib64下存放的是 HAL 实现、厂商扩展库/apex分区里的 APEX 包内部也有一堆.so应用私有目录下的lib/文件夹里则是 APK 打包进来的 JNI 库。不同位置的.so有不同的链接和可见性规则这也是编译时就要决定的事。从进程视角看动态库是代码复用和模块化最重要的手段。比如你用dlopen打开一个 vendor 库或者通过System.loadLibrary加载一个 JNI 库背后都依赖动态链接器的行为。Android 10 的动态链接器/system/bin/linker和/system/bin/linker64对命名空间做了严格限制一个.so能看见哪些库、不能看见哪些库由 linker namespace 配置决定。编译期如果没把依赖声明清楚运行期就可能报dlopen failed: cannot locate symbol或者library libxxx.so not found这类错误。标题里提到的「根文件系统」和动态库的编译结果直接相关。编译出来的.so最终要落到根文件系统的某个目录里要么是system.img要么是vendor.img要么打进 APEX。mk 里的LOCAL_MODULE_PATH、LOCAL_PROPRIETARY_MODULE这些变量决定了产物到底进哪个分区进错分区在 Android 10 上往往会触发 seccomp 或 SELinux 策略问题甚至直接无法挂载。所以编译动态库这件事从来不只是在终端里跑一条命令它牵涉到分区规划、链接可见性和运行权限。1.3 一个动态库的生命周期从源码到out目录把编译动态库的全流程拆开看大概是这样的mk 文件被 kati 解析 → 生成 Ninja 目标 → 调用clang编译每一个源文件生成.o→ 调用链接器把.o和依赖库打包成.so→ 按LOCAL_STRIP_MODULE选项决定是否去除符号表 → 拷贝到out/target/product/device/...目录。中间任何一步出错ke 构建系统都会用红色错误信息打断你大部分新手就是在这里被劝退的。我个人的经验是花一天时间看懂 mk 的核心变量比盲试一百次错误命令都管用。下面进入实战环节。2. 手写 Android.mk动态库编译的核心语法2.1 第一个可用的动态库 mk假设我们手里有一个简单的 C 源文件hello.c内容是一个导出函数hello_world#include stdio.h int hello_world(const char *name) { printf(Hello, %s!\n, name); return 0; }要在 Android 源码树里把它编译成libhello.so对应的Android.mk长这样LOCAL_PATH : $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE : libhello LOCAL_SRC_FILES : hello.c LOCAL_EXPORT_C_INCLUDE_DIRS : $(LOCAL_PATH)/include include $(BUILD_SHARED_LIBRARY)这是最简单也最典型的 mk 写法。逐行解释一下LOCAL_PATH是当前模块所在目录几乎每个 mk 文件第一行都是它call my-dir是构建系统提供的一个函数返回当前文件所在路径。include $(CLEAR_VARS)这条命令在 Makefile 语境里类似“清空画板”它会把之前模块定义的LOCAL_*变量全部重置避免模块之间互相污染。如果漏掉这条上一个模块的LOCAL_SRC_FILES可能还残留在变量里当前模块就会编译出莫名其妙的东西。LOCAL_MODULE必须唯一它同时决定了输出文件名。比如这里设为libhello最终产物就是libhello.so前缀lib和后缀.so是构建系统自动加的。如果你的库名不含lib前缀但你又希望产物叫libxxx.so可以设置LOCAL_MODULE_STEM或者直接用全名定义。一般不建议搞特殊遵守libname.so命名是 Android 的惯例。LOCAL_SRC_FILES是参与编译的源文件列表支持空格分隔多个文件也可以放.c、.cpp、.S汇编文件。include $(BUILD_SHARED_LIBRARY)是最关键的一行它告诉构建系统“现在把上面定义的模块构建成一个动态库”。与之相对的还有BUILD_STATIC_LIBRARY静态库、BUILD_EXECUTABLE可执行文件、BUILD_HOST_SHARED_LIBRARY宿主机上运行的动态库一般用于编译期工具。后面几类在实际开发中同样常用但本文聚焦动态库。2.2 链接第三方库LOCAL_SHARED_LIBRARIES 与 LOCAL_LDLIBS 的区别动态库一般不可能是孤家寡人它要链接其他的库。最常见的场景是我要写一个 JNI 库它依赖系统已有的liblog打印日志或者我要写一个 HAL 层的实现库它依赖libhardware里的接口。mk 里声明链接依赖有两种方式新手经常搞混。一种是LOCAL_SHARED_LIBRARIES它声明的是“同属 Android 源码树内的共享库模块”。构建系统会先保证这些库被编译出来然后在你当前模块的链接命令里自动加上-lname。它不只是链接层面的依赖还是一种构建顺序依赖。比如你依赖liblog写的是LOCAL_SHARED_LIBRARIES : liblog构建系统会自动把它映射到源码树里的system/core/liblog或者对应模块保证先编好 liblog 再编你的模块。这个变量还可以传递A 依赖 BB 依赖 CA 在链接时会自动带上 C 的链接参数前提是 B 用了LOCAL_EXPORT_SHARED_LIBRARIES把依赖关系导出。另一种是LOCAL_LDLIBS它声明的是“系统库”级别的链接参数直接传给链接器的-l选项。举个例子如果你想链接libzzlib 压缩库可以写LOCAL_LDLIBS : -lz注意LOCAL_LDLIBS不会触发构建顺序依赖它是假定这些库已经存在于 sysroot 中。对 Android 10 来说NDK 提供的系统库LL-NDK 列表里的那些可以直接用LOCAL_LDLIBS链接比如-llog、-lz、-lm等。但在系统源码树里编译时同样的库用LOCAL_SHARED_LIBRARIES : liblog更规范因为 Android 10 已经有针对 NDK 库的链接限制如果直接-llog可能没问题但如果链接一个非 LL-NDK 的私有库用LOCAL_LDLIBS硬链到了运行期很可能因为 linker namespace 限制而找不到符号。除了链接参数还有头文件搜索路径的问题。很多人容易忽略LOCAL_C_INCLUDES和LOCAL_EXPORT_C_INCLUDE_DIRS的区别。LOCAL_C_INCLUDES只对当前模块生效说白了就是告诉编译器“去这些目录里找头文件”LOCAL_EXPORT_C_INCLUDE_DIRS则会把这个路径导出给依赖当前模块的其他模块。假设你编译一个libhello它对外暴露hello.h那么被依赖方在链接libhello时自动能 include 到hello.h这对于设计 SDK 风格的库很有用。2.3 预编译动态库没有源码也要能集成现实里经常遇到这种情况厂商给了你一个编译好的libfoo.so但没有给源码只给了头文件和文档。你需要在你的系统项目里集成它那 mk 里就要用BUILD_SHARED_LIBRARY的“预编译变体”——准确说用include $(BUILD_PREBUILT)并把LOCAL_SRC_FILES指向那个.so文件。一个典型的预编译动态库 mk 写法include $(CLEAR_VARS) LOCAL_MODULE : libfoo LOCAL_SRC_FILES : lib/$(TARGET_ARCH_ABI)/libfoo.so LOCAL_EXPORT_C_INCLUDE_DIRS : $(LOCAL_PATH)/include include $(BUILD_PREBUILT)这里$(TARGET_ARCH_ABI)是一个由编译系统定义的变量Android 10 上常见取值是arm64-v8a、armeabi-v7a、x86_64。如果你的预编译库分了架构目录用这个变量能省很多事。关于BUILD_PREBUILT有几个细节值得注意它会把LOCAL_SRC_FILES里指定的.so拷贝到对应的out目录并且注册为一个模块其他模块可以通过LOCAL_SHARED_LIBRARIES : libfoo依赖它。如果你想放在 vendor 分区还是得配合LOCAL_PROPRIETARY_MODULE : true或显式指定LOCAL_MODULE_PATH来使用。我踩过一个坑预编译库在 Android 10 上链接后运行时报dlopen failed: library libfoo.so not found但明明out/target/product/device/vendor/lib64/libfoo.so存在。后来发现是应用或进程的 linker namespace 里没配置这个库。编译期没问题运行期不一定没问题这个认知要早日建立。3. 编译实操从 mk 到产物的完整流程3.1 准备编译环境与配置 Android 10 源码编译动态库听起来简单实际要先把整个 Android 编译环境跑通。Android 10 的源码编译对开发机的配置有一定要求建议至少 16GB 内存、500GB 可用磁盘空间、Ubuntu 18.04 或 20.04 系统。即便你只是想编一个小动态库首次构建也需要先初始化环境。流程大体是先下载 Android 10 源码我们假设你已经有一份可编译的源码树然后执行source build/envsetup.sh lunch product-userdebuglunch选择一个目标产品它会配置一系列交叉编译的工具链路径、目标架构、分区信息等环境变量。Android 10 上常见的选择是aosp_arm64-userdebug或者你手上硬件对应的产品名。userdebug 和 user 版本的区别在于 userdebug 保留 root 权限和调试工具日常开发我基本都用 userdebug。如果只是为了编译一个模块lunch之后直接执行单模块编译即可不必在首次构建时全量编译整个系统但实际中因为要生成build-time的工具链和某些依赖第一次单编也可能会触发不少隐含依赖的构建耗时取决于机器性能。3.2 单编动态库mm、mmm 与 m 的区别在envsetup.sh里定义了几个实用的编译命令m、mm、mmm、make。它们的关系属于常见的混淆点我拆开讲。m是全源码编译等价于make。在 Android 10 里会触发完整的系统构建一次可能几十分钟甚至几小时除非你清楚自己在干什么否则不要随便用m编译整个系统。mm是编译当前目录下的模块。它要求你先cd到包含Android.mk或Android.bp的目录然后执行mm构建系统只编译这个目录里的目标模块。这是我最常用的方式修改一个动态库源码后在它目录下执行mm几秒钟到一两分钟就能得到新的.so。mmm dir是编译指定目录下的模块不需要先切换目录。比如mmm vendor/xxx/libhello等价于先cd vendor/xxx/libhello再mm。还有个mma和mmma区别在于它们会额外编译依赖的模块。举个例子如果你的动态库依赖了liblog而liblog的源码发生改动还没编译直接mm可能因为链接库过旧而出错这时候用mma可以连带构建依赖。在 Android 10 上kati 的增量检查已经比较智能大部分情况mm够了。实际操作中我发现对mm的理解不到位容易导致一个问题你改了某个系统库 A 的源码然后在模块 B 目录下执行mmB 链接的 A 可能还是旧的.so因为构建系统没有检测到 A 的改动。解决办法就是mma或者先到 A 目录下mm一次再到 B 目录下mm。这属于“构建顺序”的经典坑下面讲常见问题时细说。3.3 产物位置和 out 目录结构Android 10 编译产物默认都在out/target/product/product/目录下。动态库的最终归宿取决于模块类型普通系统库out/target/product/product/system/lib64/或system/lib/vendor 库out/target/product/product/vendor/lib64/或vendor/lib/应用私有库打进 APK 后位于 APK 内部lib/目录如果你在源码树里执行mm终端结束后一般会显示Install: out/target/product/product/system/lib64/libhello.so这就是动态库拷贝到的位置。你可以用adb push把这个.so推到设备上但要注意 Android 10 的 SELinux 和只读分区机制——/system默认是只读挂载的为了验证一个库我一般会把它推到/data/local/tmp/再用LD_LIBRARY_PATH指定加载路径或者直接重打包系统镜像。具体怎么选取决于你是要“能跑”还是要“能发布”。如果编译的是 32 位库产物在system/lib/64 位在system/lib64/。Android 10 的绝大多数设备还是以 64 位为主但很多厂商 BSP 里 32 位库仍有一席之地。要注意TARGET_ARCH和TARGET_2ND_ARCH分别表示主架构和第二架构。arm64 设备的TARGET_ARCH是arm64TARGET_2ND_ARCH是arm对应的产物目录就是lib64和lib。mk 里可以用TARGET_ARCH判断当前编译架构以适配不同架构的源码差异。3.4 带版本号、符号可见性与 strip 处理动态库编译里除了“能用”还有一个工程化的问题版本管理、符号导出和 strip。Android 系统库一般不太使用 SO_VERSION 这种 GNU 风格的版本命名libhello.so就是完整文件名不像 Linux 桌面系统里还带libhello.so.1.2.3。但如果你在做一个需要对外提供稳定 ABI 的库符号可见性就要格外注意。默认情况下编译动态库会导出所有非 static 的全局符号。这会导致两个问题一是符号表庞大二是可能与其他库的符号冲突。Android 10 的链接器默认开启了-Wl,--no-undefined吗其实没有完全强制但系统源码里很多模块会加上版本脚本或者-fvisibilityhidden来控制导出。mk 中控制符号可见性有两个常用变量LOCAL_CFLAGS -fvisibilityhidden LOCAL_LDFLAGS -Wl,--version-script$(LOCAL_PATH)/exports.map-fvisibilityhidden表示默认所有符号隐藏只有显式标了__attribute__((visibility(default)))的函数才导出。--version-script用一个 map 文件精确列出导出的符号更严格。这样做还有个好处链接器可以做更好的优化把未导出的符号内联或裁剪最终.so体积会小一些。strip 是另一个影响产物大小的环节。Android 10 的构建系统默认会对动态库做 strip去除调试符号但保留一份带符号的版本在out/symbols/目录下用于 native 崩溃时的 addr2line 解析。:symbols目录不参与打包所以出问题时一定要留好对应的版本。mk 里控制 strip 行为的变量是LOCAL_STRIP_MODULE可选值有true默认编译产物被 strip、false保留符号、keep_symbols生成一个带符号的副本。日常开发如果想在设备上直接对.so做 gdb 调试可以临时设置LOCAL_STRIP_MODULE : false。4. 常见问题排查思路、方法与案例实录4.1 链接错误速查表写 mk 编译动态库时我遇到最多的问题就是链接错误。这里整理一个速查表日常排障可以直接对照。错误现象根本原因解决方法undefined reference to xxx依赖库未声明或声明顺序不对检查LOCAL_SHARED_LIBRARIES/LOCAL_STATIC_LIBRARIES确认目标库已编译静态库链接顺序要放在被依赖方后面cannot find -lxxx链接器找不到指定库确认库名称是否正确系统库用LOCAL_LDLIBS源码树内库用LOCAL_SHARED_LIBRARIESlibxxx.so is not allowed to link to libyyy.so违反 Android 10 的链接可见性规则检查库类型system/vendor/NDK确认是否在允许链接的命名空间内multiple definition of xxx符号重复定义检查是否有两个库导出了相同符号用nm -D查看导出符号再决定是否隐藏其中一个relocation R_AARCH64_ABS64 against xxx错误一般是版本脚本或符号可见性与实际定义不匹配检查组件的visibility属性必要时调整LOCAL_LDFLAGSFAILED: ... clang: error: no such file or directory: xxx.h头文件路径未声明检查LOCAL_C_INCLUDES和LOCAL_EXPORT_C_INCLUDE_DIRS是否覆盖了对应目录4.2 实战案例undefined reference 的完整排查有一次我在移植一个硬件抽象库mk 里写了LOCAL_SHARED_LIBRARIES : libhardware liblog源文件也调用了hw_get_module和ALOGD宏但编译时一直报undefined reference to hw_get_module。新手很可能直觉认为“依赖没写对”然后盲目把库换成LOCAL_LDLIBS : -lhardware结果还是报错。我当时做了两件事。第一确认libhardware模块在源码树里确实存在并且确认它的构建产物已经生成执行find out/target/product/product -name libhardware.so第二用grep查看libhardware对外导出的符号nm -D out/target/product/product/system/lib64/libhardware.so | grep hw_get_module结果发现符号确实存在。那问题出在哪儿后来我注意到LOCAL_SHARED_LIBRARIES里的库名写的是libhardware但在 Android 10 源码树中hardware/libhardware模块名可能是libhardware没错真正的原因在于编译该模块时TARGET_ARCH是arm而我查看的是lib64目录下的库。32 位库根本没编出来链接器在 32 位 sysroot 里找不到libhardware.so。解决办法很朴素先确保目标架构的库存在再执行mm或者直接mma连带构建libhardware。这个案例告诉我们排查链接错误时不能只盯着 mk 本身要看实际链接的架构、产物路径和依赖库是否已经真实编译出来。很多时候“找不到符号”实际是“找不到库”或“库架构不对”。4.3 动态库命名冲突与符号污染Android 源码树很大模块重名的概率低但动态库导出符号冲突的概率不低。最常见的场景是两个动态库都静态链接了同一个第三方库比如 zlib、openssl导致这两个.so都导出了大量相同符号。当进程同时加载这两个库时符号解析可能出现诡异行为——某个库调用compress2却解析到了另一个库的实现。我从实践中学到两条规则。第一如果第三方库要被多个模块共用尽量把它编译成一个独立的共享库libz.so、libcrypto.so而不是每个模块各自静态链接。第二如果实在无法避免静态链接编译第三方库时要加-fvisibilityhidden把大多数符号隐藏只暴露必要的 API。Android 10 的 linker 默认符号解析是“全局优先”但这不代表符号冲突是安全的尤其是在引入 vendor 库时。4.4 运行期dlopen failed与 linker namespace 问题编译时一切正常运行时却打不开库这是 Android 10 最让人头疼的问题之一。动态链接器把系统的库划分到不同 namespacesystem、vendor、product、apex等。app 进程有自己独立的 classloader namespaceJNI 库能链接哪些库是受限制的。我曾经集成一个厂商提供的算法库libalgo.so它依赖了liblog和厂商私有库libvendorapi.so。在 app 进程里用System.loadLibrary(algo)加载直接报dlopen failed: library libvendorapi.so not found编译时LOCAL_SHARED_LIBRARIES明明写了libvendorapi链接也没报错。原因在于app 进程的 linker namespace 里没有包含libvendorapi.so所在的路径vendor 分区所以运行时找不到。解决思路不是改 mk而是调整库的位置或者通过android:extractNativeLibs、jniLibs等方式把依赖打包进 APK或者让 app 在加载前先手动dlopen那个私有库路径。mk 能管的只是“编译期依赖”运行期能不能加载要结合 linker namespace 和 seccomp/SELinux 策略一起评估。4.5 32/64 位混编的典型翻车现场我早期踩过一个小坑mk 里用LOCAL_SRC_FILES写了某个汇编文件.S里面包含 64 位指令。在 arm64 设备上编译没问题但因为某种原因构建系统把模块编成了 32 位直接编译失败。后来我才意识到Android.mk默认会根据TARGET_ARCH决定模块架构但当你同时支持 32/64 位时可以用LOCAL_MULTILIB控制模块编几个架构。LOCAL_MULTILIB : both表示同时编译 32 位和 64 位版本: 32只编 32 位: 64只编 64 位: first编主要架构。如果你的模块引用了$(TARGET_ARCH)来决定编译参数遇到both时要注意这个变量在主架构和副架构的编译命令里是不同的吗实际会有些微不同所以更稳妥的写法是用$(TARGET_ARCH)配合ifeq判断或者用LOCAL_32_BIT_ONLY : true这种更明确的开关。真到了编译失败的时候先看产物目录里的.o文件架构是什么file out/target/product/product/obj/SHARED_LIBRARIES/libhello_intermediates/hello.o这样能迅速定位是架构预期不对还是编译器参数不对。5. 在 mk 之外的几点实践心得5.1 从 Android.mk 迁移到 Android.bp 时的注意点Google 在持续弱化 mk 的地位Android 10 上虽然还用 mk但到了更高版本尤其是 Android 13 之后很多地方已经在强制 bp。如果你现在写新代码我建议优先考虑Android.bp除非你是在维护老模块或对接厂商 SDK。bp 里的动态库模块往往是这样的cc_library_shared { name: libhello, srcs: [hello.c], export_include_dirs: [include], shared_libs: [liblog], }bp 的优势是语法严格、逻辑扁平、容易审查劣势是我们上面提到的很多 mk 技巧比如通过 Makefile 函数动态拼接源文件列表在 bp 里不能用得写genrule或soong插件。从 mk 迁 bp 时最容易忽略的是LOCAL_CPPFLAGS、LOCAL_CFLAGS、LOCAL_CONLYFLAGS的对应关系以及LOCAL_LDFLAGS映射成ldflags的写法。遇到老模块不要机械转换要理解每一行 mk 背后的意图。5.2 动态库链接的架构差异与跨平台陷阱Android 10 的动态库编译工具链用的是 clang从 Android 9 开始默认就从 GCC 切到了 clang交叉编译时系统的build和host要分清楚。我们平时说的“编译动态库”多数指的是 target 版本也就是跑在设备上的但有时候你还要编译 host 版本的动态库比如给 PC 上的编译工具链用。两者不能混用target 库不能直接跑到 host 上host 库也不能打包进系统。在 mk 里区分这两类用的是BUILD_HOST_SHARED_LIBRARY和BUILD_SHARED_LIBRARY。host 库一般不需要交叉编译直接本机 clang 编译可以用来跑一些截图工具、日志分析工具或者模拟器里的某些模块。另一个常见的坑是LOCAL_CFLAGS里写死了-marcharmv8-a结果在 host 编译时报错因为 host 机器的 CPU 指令集不一定兼容。要避免这种问题就不要在 host 模块里加 target 架构相关的编译参数。5.3 建议从一个小模块逐步扩展如果你想彻底掌握 Android.mk 编译动态库这件事我强烈建议从一个最小模块开始而不是一上来就移植一个大库。你可以先写一个没有依赖的libhello.so用mm编译确认产物路径再给它加liblog依赖把日志打印接入 logcat接着加头文件导出写一个调用它的可执行文件最后再尝试预编译库的方式把libhello.so当第三方库集成到自己的模块里。这样一轮走完mk 语法里的核心变量、构建顺序和产物逻辑就都有体感了。我在实际带团队时发现能在 30 分钟内从零写出一个能正常编译、安装、被调用的动态库 mk是最能检验一个人是否真正理解 Android 编译系统的标准。语法本身不复杂难的是建立“构建依赖图”的直觉——知道这一行 mk 最终会变成 Ninja 文件里的哪条规则以及它为什么会在某个步骤报错。这种直觉只能通过踩坑积累所以我一直保留着手写 mk 的习惯哪怕是新项目里能用 bp 的地方我也会先在 mk 里验证一遍逻辑再迁移。Android 10 的编译系统看似庞大但核心规律就是“变量声明 构建规则触发”。掌握动态库编译后静态库、可执行文件、预编译模块无非是在同一个框架里换几个参数。这篇文章从原理到实操再到坑点基本覆盖了我在系统开发中最常用到的场景。剩下那些边边角角的问题就留给读者亲手编译几个模块去体会吧。
返回列表