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/lib,APPS 会放到 /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-v7a 和 arm64-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 知识体系图
下面这张图总结了预编译模块的核心逻辑:
9.7 常见问题与避坑指南
- 文件路径不对:检查
LOCAL_SRC_FILES的路径是否相对于LOCAL_PATH。我经常犯这个错,尤其是文件放在子目录时。 - 签名问题:预编译 APK 一定要确认签名方式。如果是系统应用,用
platform;如果是第三方,用PRESIGNED。 - 架构不匹配:如果你的设备是
arm64,但只提供了armeabi-v7a的库,系统会尝试用兼容模式运行。但性能会打折扣。最好提供对应架构的库。 - 依赖缺失:预编译库的依赖不会自动解析。你需要手动用
LOCAL_REQUIRED_MODULES或LOCAL_SHARED_LIBRARIES声明。
我的经验:每次集成预编译模块后,我都会用 adb shell ls -l /system/lib/libfoo.so 确认文件是否真的被安装。有时候编译成功了,但文件没拷贝进去,那才叫一个头疼。
好了,预编译模块的内容就这些。你想想看,其实核心就是三件事:告诉系统文件在哪、告诉系统装到哪、告诉系统依赖谁。掌握了这三步,大部分预编译场景都能搞定。
公众号:蓝海资料掘金营,微信deep3321