Android .so 文件压缩

📅 2026/8/2 12:18:48 👁️ 阅读次数 📝 编程学习
Android .so 文件压缩

精简动态符号表

  • 删除一个符号表项后 so 减小的体积为:符号名字符串长度+ 1 + Elfxx_Sym + Elfxx_Half + Elfxx_Word
  • 如果一个 so 的某些符号,被其他 so 通过 dlsym 方式使用,那么这些符号也应该保留在该 so 的导出符号中(否则会导致运行时异常)。
  • 使用 visibility 和 attribute 控制符号可见性
    • 可以通过给编译器传递 -fvisibility=VALUE 控制全局的符号可见性。
    • visibility 是全局符号可见性开关,attribute 方式是针对单个符号的可见性开关。
    • 只能控制变量或函数是否存在于动态符号表中(即是否删除其动态符号表项),而不会删除其实现体。
    • default:除非对变量或函数特别指定符号可见性,所有符号都在动态符号表中,这也是不使用 -fvisibility 时的默认值。
    • hidden:除非对变量或函数特别指定符号可见性,所有符号在动态符号表中都不可见。
    • CMake 项目的配置
      • set(CMAKE_C_FLAGS “${CMAKE_C_FLAGS} -fvisibility=hidden”)
      • set(CMAKE_CXX_FLAGS “${CMAKE_CXX_FLAGS} -fvisibility=hidden”)
    • ndk-build 项目的配置 LOCAL_CFLAGS += -fvisibility=hidden
    • 针对单个变量或函数
      • attribute((visibility(“hidden”)))
      • int hiddenInt=3;
  • 使用 static 关键字控制符号可见性
    • 当使用 static 表示“该函数或变量仅在本文件可见”时,那么这个函数或变量就不会出现在动态符号表中,但只会删除其动态符号表项,而不会删除其实现体。
  • 使用 exclude libs 移除静态库中的符号
    • 它是传递给链接器的参数,可以使依赖的静态库的符号在动态符号表中不存在。同样,也是只能删除符号表项,实现体仍然会存在于产生的 so 文件中。
    • CMake 项目的配置
      • set(CMAKE_SHARED_LINKER_FLAGS “${CMAKE_SHARED_LINKER_FLAGS} -Wl,–exclude-libs,ALL”)#使所有静态库中的符号都不被导出
      • set(CMAKE_SHARED_LINKER_FLAGS “${CMAKE_SHARED_LINKER_FLAGS} -Wl,–exclude-libs,libabc.a”)#使 libabc.a 的符号都不被导出
    • ndk-build 项目的配置
      • LOCAL_LDFLAGS += -Wl,–exclude-libs,ALL #使所有静态库中的符号都不被导出
      • LOCAL_LDFLAGS += -Wl,–exclude-libs,libabc.a #使 libabc.a 的符号都不被导出
  • 使用 version script 控制符号可见性
    • version script 是传递给链接器的参数,用来指定动态库导出哪些符号以及符号的版本。
    • version script 支持通配符,* 代表0个或者多个字符,? 代表单个字符。比如 my*; 就代表所有以 my 开头的符号。有了通配符的支持,配置 version script 会更加方便。
    • version script 方式可以删除 __bss_start 这样的一些符号(这是链接器默认加上的符号)
    • version script 方式优于 visibility 结合 attribute 的方式。同时,使用了 version script 方式,就不需要使用 exclude libs 方式控制依赖的静态库中的符号是否导出了。
    • 开启 version script 需要先编写一个文本文件,用来指定动态库导出哪些符号。然后将文件的路径传递给链接器即可
      • 查看未优化 so 的导出符号表,找到目标函数被修饰后的符号,然后填写到 version_script.txt 中
      • 在 version_script.txt 中使用 extern 语法
    • CMake 项目的配置 set(CMAKE_SHARED_LINKER_FLAGS “CMAKESHAREDLINKERFLAGS−Wl,−−version−script={CMAKE_SHARED_LINKER_FLAGS} -Wl,--version-script=CMAKESHAREDLINKERFLAGSWl,versionscript={CMAKE_CURRENT_SOURCE_DIR}/version_script.txt”) #version_script.txt 与当前 CMakeLists.txt 同目录
    • ndk-build 项目的配置 LOCAL_LDFLAGS += -Wl,–version-script=${LOCAL_PATH}/version_script.txt #version_script.txt 与当前 Android.mk 同目录

移除无用代码

  • 开启 LTO(Link Time Optimization 链接期优化)
    • LTO 能够在链接目标文件时检测出 DeadCode 并删除它们
    • GCC 和 Clang 均支持 LTO。LTO 方式编译的目标文件中存储的不再是具体机器的指令,而是机器无关的中间表示(GCC 采用的是 GIMPLE 字节码,Clang 采用的是 LLVM IR 比特码)。
    • 如果使用 Clang,编译参数和链接参数中都要开启 LTO,否则会出现无法识别文件格式的问题(NDK22 之前存在此问题)。使用 GCC 的话,只需要编译参数中开启 LTO 即可。
    • 如果项目工程依赖了静态库,可以使用 LTO 方式重新编译该静态库,那么编译动态库时,就能移除静态库中的 DeadCode,从而减小最终 so 的体积。
    • 经过测试,如果使用 Clang,链接器需要开启非 0 级别的优化,LTO 才能真正生效。经过实际测试(NDK 为 r16b),O1 优化效果较差,O2、O3 优化效果比较接近。
    • CMake 项目的配置
      • set(CMAKE_C_FLAGS “${CMAKE_C_FLAGS} -flto”)
      • set(CMAKE_CXX_FLAGS “${CMAKE_CXX_FLAGS} -flto”)
      • set(CMAKE_SHARED_LINKER_FLAGS “${CMAKE_SHARED_LINKER_FLAGS} -O3 -flto”)
    • ndk-build 项目的配置
      • LOCAL_CFLAGS += -flto
      • LOCAL_LDFLAGS += -O3 -flto
  • 开启 GC sections
    • 这是传递给链接器的参数,对无用的 section 进行回收。注意,这里的 section 不是指最终 so 中的 section,而是作为链接器的输入的目标文件中的 section。
    • GC sections 参数通知链接器,仅保留动态符号(及 .init_array 等)直接或者间接引用到的 section,移除其他无用 section。
    • 链接器会把所有输入的目标文件的同类型的 section 进行合并,组装出最终的 so 文件。
    • 编译器默认会把所有函数放到同一个 section 中,把所有相同特点的数据放到同一个 section 中
    • CMake 项目的配置
      • set(CMAKE_C_FLAGS “${CMAKE_C_FLAGS} -fdata-sections -ffunction-sections”)
      • set(CMAKE_CXX_FLAGS “${CMAKE_CXX_FLAGS} -fdata-sections -ffunction-sections”)
      • set(CMAKE_SHARED_LINKER_FLAGS “${CMAKE_SHARED_LINKER_FLAGS} -Wl,–gc-sections”)
    • ndk-build 项目的配置
      • LOCAL_CFLAGS += -fdata-sections -ffunction-sections
      • LOCAL_LDFLAGS += -Wl,–gc-sections

优化指令长度

  • 使用 Oz/Os 优化级别

    • O0 表示不开启优化(这种情况主要是为了便于调试以及更快的编译速度),从 O1 到 O3,优化程度越来越强。Clang 和 GCC 均提供了 Os 的优化级别,其与 O2 比较接近,但是优化了生成产物的体积。而 Clang 还提供了 Oz 优化级别,在 Os 的基础上能进一步优化产物体积。
    • CMake 项目的配置
      • set(CMAKE_C_FLAGS “${CMAKE_C_FLAGS} -Oz”)
      • set(CMAKE_CXX_FLAGS “${CMAKE_CXX_FLAGS} -Oz”)
    • ndk-build 项目的配置 LOCAL_CFLAGS += -Oz
  • 如果项目中没有使用 C++ 的异常机制(例如 try…catch 等),可以通过禁用 C++ 的异常机制,来减小 so 的体积。

    • CMake 项目的配置 set(CMAKE_CXX_FLAGS “${CMAKE_CXX_FLAGS} -fno-exceptions”)
    • ndk-build 默认会禁用 C++ 的异常机制,因此无需特意禁用。如果现有项目开启了 C++ 的异常机制,说明确有需要,需仔细确认后才能禁用。
  • 如果项目中没有使用 C++ 的 RTTI 机制(例如 typeid 和 dynamic_cast 等),可以通过禁用 C++ 的 RTTI ,来减小 so 的体积。

    • CMake 项目的配置 set(CMAKE_CXX_FLAGS “${CMAKE_CXX_FLAGS} -fno-rtti”)
    • ndk-build 默认会禁用 C++ 的 RTTI 机制,因此无需特意禁用。如果现有项目开启了 C++ 的 RTTI 机制,说明确有需要,需仔细确认后才能禁用。
  • so 仅被另外一个 so 动态依赖时,可以将这些 so 合并为一个 so。可以在不修改项目源码的情况下,在编译层面实现 so 的合并。

    • 可以删除部分导入导出动态符号表项
    • 可以删除部分 PLT 表项和 GOT 表项
    • 减少了依赖关系,可以减轻优化的工作量
    • 由于链接器对导出符号拥有了更全的上下文信息,LTO 优化也能取得更好的效果。
  • 当多个 so 以静态方式依赖了某个相同的库时,可以考虑将此库提取成一个单独的 so,原来的几个 so 改为动态依赖该 so。提取多 so 共同依赖库,可以对不同 so 内的相同代码进行合并,从而减小总的 so 体积。

  • 收益

    • 删除了大量的非必要导出符号从而提升了 so 的安全性。
    • 因为 .data .bss .text 等运行时占用内存的 section 减小了,所以也能减小应用运行时的内存占用。
    • 如果优化过程中减少了 so 对外依赖的符号,还可以加快 so 的加载速度。