12、预编译模块管理:预编译APK、预编译库、预编译可执行文件、签名配置

说到预编译模块,我估计很多刚接触AOSP的同学都会觉得有点懵。明明源码都在那里,为什么还要搞预编译?其实道理很简单——你想想看,有些商业软件是不开源的,比如Google自家的GMS套件、高通的某些驱动库。你拿不到源码,只能拿到编译好的.so或者.apk。这时候,预编译模块管理就成了你的救命稻草。

我个人习惯把预编译模块分成三类:预编译APK、预编译库(.so/.a)、预编译可执行文件。它们的管理方式有共通之处,但细节上各有各的坑。今天我们就一个一个说清楚。

12.1 预编译模块的核心机制

在AOSP的构建系统中,预编译模块本质上是一个“假编译”过程。你提供一个二进制文件,然后通过Android.bpAndroid.mk告诉构建系统:这个文件我已经编译好了,你直接把它打包进镜像就行。

它的核心逻辑可以用下面这张图来概括:

预编译模块管理核心流程 二进制文件 (.apk / .so / .a / 可执行文件) 构建描述文件 (Android.bp / Android.mk) 构建系统(Soong/Make) 解析 + 复制 + 签名 输出到目标镜像 (system.img / vendor.img / product.img) 签名配置(仅APK需要)

说白了,预编译模块就是让构建系统帮你做“搬运工”的工作。但搬运也有搬运的门道——文件放哪里、要不要签名、依赖关系怎么处理,这些都是学问。

12.2 预编译APK

预编译APK是日常工作中最常遇到的。比如你要集成一个第三方应用,对方只给了你一个MyApp.apk,没有源码。这时候你就需要写一个预编译模块。

12.2.1 Android.bp写法

android_app_import {
    name: "MyApp",
    apk: "MyApp.apk",
    privileged: true,
    certificate: "platform",
    dex_preopt: {
        enabled: true,
    },
    required: ["libnative.so"],
}

这里有几个关键点我要强调一下:

  • name:模块名,必须唯一。我习惯用包名或者应用名的大驼峰写法。
  • apk:指定APK文件路径,相对于Android.bp所在目录。
  • privileged:设为true时,APK会被安装到/system/priv-app/目录,拥有系统级权限。
  • certificate:签名配置,后面会详细讲。
  • dex_preopt:是否对Dex进行预优化。我建议开启,能显著提升应用启动速度。
注意:如果你把privileged设为true,但APK本身没有系统级权限声明,系统会直接拒绝安装。我曾经踩过这个坑,排查了半天才发现是权限声明不匹配。

12.2.2 Android.mk写法(老项目兼容)

虽然现在Google主推Android.bp,但很多老项目还在用Android.mk。你最好两种都会写:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE := MyApp
LOCAL_SRC_FILES := MyApp.apk
LOCAL_MODULE_CLASS := APPS
LOCAL_PRIVILEGED_MODULE := true
LOCAL_CERTIFICATE := platform
LOCAL_DEX_PREOPT := true

include $(BUILD_PREBUILT)

嗯,这里要注意LOCAL_MODULE_CLASS必须设为APPS,否则构建系统不会把它当成APK来处理。

12.3 预编译库(.so / .a)

预编译库主要分两种:动态库(.so)和静态库(.a)。它们的处理方式略有不同。

12.3.1 预编译动态库

cc_prebuilt_library_shared {
    name: "libnative",
    srcs: ["libnative.so"],
    shared_libs: ["liblog", "libutils"],
    target: {
        android_arm64: {
            srcs: ["libnative_arm64.so"],
        },
        android_arm: {
            srcs: ["libnative_arm.so"],
        },
    },
}

这里有个小技巧——我建议你尽量按架构分开提供so文件。如果只提供一个通用的so,构建系统会把它复制到所有架构的镜像里,导致空间浪费。我在一个项目里就遇到过这种情况,一个30MB的so被复制了4次,白白浪费了90MB的system分区空间。

12.3.2 预编译静态库

cc_prebuilt_library_static {
    name: "libmystatic",
    srcs: ["libmystatic.a"],
    export_include_dirs: ["include"],
}

静态库的预编译相对简单,因为它不需要链接其他动态库。但要注意export_include_dirs——如果你不把头文件目录导出来,其他模块引用这个库时会找不到头文件。

12.4 预编译可执行文件

预编译可执行文件在AOSP里用得不多,但偶尔也会遇到。比如你要集成一个第三方的命令行工具,或者一个测试用的二进制文件。

cc_prebuilt_binary {
    name: "my_tool",
    srcs: ["my_tool"],
    init_rc: "my_tool.rc",
    target: {
        android_arm64: {
            srcs: ["my_tool_arm64"],
        },
    },
}

这里有个容易忽略的点——可执行文件必须设置可执行权限。构建系统默认会保留源文件的权限属性,但如果你是从Windows系统拷贝过来的文件,权限可能会丢失。我建议你在Android.bp里显式指定:

cc_prebuilt_binary {
    name: "my_tool",
    srcs: ["my_tool"],
    // 确保文件有可执行权限
    post_install_cmd: "chmod 755 $(TARGET_OUT)/bin/my_tool",
}

12.5 签名配置

签名是预编译APK管理中最容易出问题的环节。AOSP提供了几种预定义的签名密钥:

签名类型 密钥文件 用途
platform platform.pk8 / platform.x509.pem 系统级应用,共享系统UID
shared shared.pk8 / shared.x509.pem 需要共享UID的应用
media media.pk8 / media.x509.pem 媒体相关应用
testkey testkey.pk8 / testkey.x509.pem 开发测试用(默认)
PRESIGNED 无(保留原签名) 保留APK原有的签名
关键点:如果你不指定certificate,默认使用testkey。但发布版本必须使用platform或其他正式密钥,否则应用无法获得系统权限。

我在一个项目中遇到过这样的问题:预编译的APK在userdebug版本上运行正常,但切到user版本就崩溃了。排查后发现是签名问题——user版本强制校验签名,而我们的APK用的是testkey,在user版本上不被信任。

12.5.1 自定义签名

有时候你需要使用自己的签名密钥。比如公司内部有统一的签名规范:

android_app_import {
    name: "MyApp",
    apk: "MyApp.apk",
    certificate: ":my_company_key",
    certificate_key: "vendor/company/security/mykey.pk8",
    certificate_pem: "vendor/company/security/mykey.x509.pem",
}

这里certificate的值以冒号开头,表示引用一个自定义的签名配置。对应的密钥文件路径要写对,否则构建会报错。

12.6 避坑指南

最后,我总结几个常见的坑,都是我曾经踩过的:

  1. 路径问题:预编译文件的路径是相对于Android.bp的,不是相对于源码根目录的。别搞混了。
  2. 架构不匹配:如果你只提供了arm64的so,但目标设备是arm32的,构建不会报错,但运行时必崩。
  3. 签名冲突:同一个APK不能同时被两个模块引用并签名,否则构建系统会报重复签名错误。
  4. 依赖缺失:预编译库如果依赖其他动态库,必须在shared_libs里显式声明,否则链接时会找不到符号。
  5. 文件权限:预编译可执行文件一定要有可执行权限,否则adb shell进去会提示"Permission denied"。
小建议:每次添加预编译模块后,先执行m -j32编译一次,然后检查输出目录(out/target/product/xxx/system/)里文件是否到位。不要等到刷机了才发现问题。

好了,预编译模块管理就讲到这里。说白了就是三件事:告诉构建系统文件在哪、文件要放哪、要不要签名。掌握了这些,你就能轻松应对各种预编译场景了。


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