9、预编译模块:预编译库的集成、PREBUILT_ 变量、预编译 APK

好,咱们今天聊聊预编译模块。说白了,就是那些我们已经编译好的二进制文件——比如 .so 库、.jar 包、或者现成的 .apk——怎么集成到 Android Build 系统里。

你可能会问:为什么不直接源码编译?嗯,原因很多。最常见的是厂商不愿意给源码,只给了一个 .so。或者像高通、MTK 的硬件抽象层,人家就只提供二进制。我在项目里遇到过好几次,客户拿来的第三方 SDK 就是一堆 .so.jar,没有源码。这时候,你就得靠预编译模块了。

9.1 预编译模块的基本概念

Android Build 系统里,预编译模块用 BUILD_PREBUILT 或者 BUILD_MULTI_PREBUILT 来声明。它的核心思想是:告诉编译系统,这个文件我已经编译好了,你直接把它拷贝到目标镜像里就行。

我个人的习惯是,把所有预编译文件放在 vendor/device/ 目录下,单独建一个 prebuilt/ 文件夹。这样结构清晰,也方便管理。

核心原则:预编译模块不参与编译,只参与安装。系统会把它当成一个“黑盒”,直接打包进镜像。

9.2 PREBUILT_ 变量详解

预编译模块里,有几个关键的 PREBUILT_ 变量。我刚开始接触时也搞混过,后来踩了几个坑才理清楚。

变量名 作用 示例
LOCAL_SRC_FILES 指定预编译文件的路径 LOCAL_SRC_FILES := libfoo.so
LOCAL_MODULE 模块名称,安装后的文件名 LOCAL_MODULE := libfoo
LOCAL_MODULE_CLASS 指定模块类型(SHARED_LIBRARIES、APPS、JAVA_LIBRARIES 等) LOCAL_MODULE_CLASS := SHARED_LIBRARIES
LOCAL_MODULE_TAGS 模块标签,决定是否参与编译 LOCAL_MODULE_TAGS := optional
LOCAL_MODULE_SUFFIX 文件后缀,通常不需要手动设置 .so.apk
LOCAL_PREBUILT_JNI_LIBS 预编译的 JNI 库(用于 APK) LOCAL_PREBUILT_JNI_LIBS := lib/armeabi-v7a/libnative.so

这里我特别想提一下 LOCAL_MODULE_CLASS。这个变量决定了文件会被安装到哪个目录。比如 SHARED_LIBRARIES 会放到 /system/lib/vendor/libAPPS 会放到 /system/app/system/priv-app。我曾经因为忘记设置这个变量,导致库文件被放错了位置,运行时一直报 dlopen failed。嗯,那会儿排查了好久。

9.3 预编译库的集成

咱们先看一个最简单的例子:集成一个预编译的 .so 库。

# Android.mk
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := libfoo
LOCAL_SRC_FILES := libfoo.so
LOCAL_MODULE_CLASS := SHARED_LIBRARIES
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE_SUFFIX := .so
include $(BUILD_PREBUILT)

这段代码干了什么呢?它告诉编译系统:把 libfoo.so 拷贝到目标系统的 /system/lib 目录下,模块名就叫 libfoo

如果你有多个架构的库,比如 armeabi-v7aarm64-v8a,可以用 BUILD_MULTI_PREBUILT

include $(CLEAR_VARS)
LOCAL_MODULE := libfoo
LOCAL_SRC_FILES_arm := lib/armeabi-v7a/libfoo.so
LOCAL_SRC_FILES_arm64 := lib/arm64-v8a/libfoo.so
LOCAL_MODULE_CLASS := SHARED_LIBRARIES
LOCAL_MODULE_TAGS := optional
include $(BUILD_MULTI_PREBUILT)

我个人建议,如果库文件不多,尽量用 BUILD_MULTI_PREBUILT。它自动处理架构选择,省心不少。

小技巧:如果你的预编译库依赖其他库,记得用 LOCAL_SHARED_LIBRARIES 声明依赖关系。否则运行时可能会因为找不到符号而崩溃。

9.4 预编译 APK

预编译 APK 也很常见。比如厂商提供的系统应用,或者一些测试工具。写法如下:

include $(CLEAR_VARS)
LOCAL_MODULE := MyApp
LOCAL_SRC_FILES := MyApp.apk
LOCAL_MODULE_CLASS := APPS
LOCAL_MODULE_TAGS := optional
LOCAL_CERTIFICATE := platform
include $(BUILD_PREBUILT)

这里有个关键点:LOCAL_CERTIFICATE。它指定了 APK 的签名方式。如果是系统应用,一般用 platform 签名;如果是第三方应用,可以用 PRESIGNED,表示保留原有签名。

我曾经遇到过一个问题:客户给的 APK 是 PRESIGNED 的,但我忘了设置这个变量,结果系统重新签了名,导致应用安装失败。嗯,从那以后我每次都会检查签名方式。

对于包含 JNI 库的 APK,可以用 LOCAL_PREBUILT_JNI_LIBS

include $(CLEAR_VARS)
LOCAL_MODULE := MyAppWithNative
LOCAL_SRC_FILES := MyAppWithNative.apk
LOCAL_MODULE_CLASS := APPS
LOCAL_MODULE_TAGS := optional
LOCAL_CERTIFICATE := platform
LOCAL_PREBUILT_JNI_LIBS := \
    lib/armeabi-v7a/libnative.so \
    lib/arm64-v8a/libnative.so
include $(BUILD_PREBUILT)

这样,编译系统会把 APK 里的 JNI 库提取出来,放到正确的目录下。

9.5 预编译模块的依赖与顺序

预编译模块之间也可能有依赖关系。比如你的 .so 依赖另一个预编译的 .so。这时候,你需要用 LOCAL_REQUIRED_MODULES 来声明:

include $(CLEAR_VARS)
LOCAL_MODULE := libfoo
LOCAL_SRC_FILES := libfoo.so
LOCAL_MODULE_CLASS := SHARED_LIBRARIES
LOCAL_REQUIRED_MODULES := libbar
include $(BUILD_PREBUILT)

这样,编译系统会确保 libbar 也被安装到镜像里。

注意:LOCAL_REQUIRED_MODULES 只保证安装顺序,不保证加载顺序。运行时依赖还是得靠 dlopen 或者 LD_LIBRARY_PATH 来处理。

9.6 知识体系图

下面这张图总结了预编译模块的核心逻辑:

预编译模块核心逻辑 预编译文件 .so / .jar / .apk Android.mk 声明 PREBUILT_ 变量 编译系统处理 拷贝 + 签名 目标镜像 /system /vendor /product 关键 PREBUILT_ 变量 LOCAL_SRC_FILES → 源文件路径 LOCAL_MODULE_CLASS → 模块类型(SHARED_LIBRARIES / APPS / JAVA_LIBRARIES) LOCAL_CERTIFICATE → 签名方式(platform / PRESIGNED) LOCAL_REQUIRED_MODULES → 依赖的其他预编译模块

9.7 常见问题与避坑指南

  • 文件路径不对:检查 LOCAL_SRC_FILES 的路径是否相对于 LOCAL_PATH。我经常犯这个错,尤其是文件放在子目录时。
  • 签名问题:预编译 APK 一定要确认签名方式。如果是系统应用,用 platform;如果是第三方,用 PRESIGNED
  • 架构不匹配:如果你的设备是 arm64,但只提供了 armeabi-v7a 的库,系统会尝试用兼容模式运行。但性能会打折扣。最好提供对应架构的库。
  • 依赖缺失:预编译库的依赖不会自动解析。你需要手动用 LOCAL_REQUIRED_MODULESLOCAL_SHARED_LIBRARIES 声明。

我的经验:每次集成预编译模块后,我都会用 adb shell ls -l /system/lib/libfoo.so 确认文件是否真的被安装。有时候编译成功了,但文件没拷贝进去,那才叫一个头疼。

好了,预编译模块的内容就这些。你想想看,其实核心就是三件事:告诉系统文件在哪、告诉系统装到哪、告诉系统依赖谁。掌握了这三步,大部分预编译场景都能搞定。


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