2026/8/2 12:07:26

Android .so 文件压缩

Android .so 文件压缩 精简动态符号表删除一个符号表项后 so 减小的体积为符号名字符串长度 1 Elfxx_Sym Elfxx_Half Elfxx_Word如果一个 so 的某些符号被其他 so 通过 dlsym 方式使用那么这些符号也应该保留在该 so 的导出符号中否则会导致运行时异常。使用 visibility 和 attribute 控制符号可见性可以通过给编译器传递 -fvisibilityVALUE 控制全局的符号可见性。visibility 是全局符号可见性开关attribute 方式是针对单个符号的可见性开关。只能控制变量或函数是否存在于动态符号表中即是否删除其动态符号表项而不会删除其实现体。default除非对变量或函数特别指定符号可见性所有符号都在动态符号表中这也是不使用 -fvisibility 时的默认值。hidden除非对变量或函数特别指定符号可见性所有符号在动态符号表中都不可见。CMake 项目的配置set(CMAKE_C_FLAGS “${CMAKE_C_FLAGS} -fvisibilityhidden”)set(CMAKE_CXX_FLAGS “${CMAKE_CXX_FLAGS} -fvisibilityhidden”)ndk-build 项目的配置 LOCAL_CFLAGS -fvisibilityhidden针对单个变量或函数attribute((visibility(“hidden”)))int hiddenInt3;使用 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-scriptCMAKES​HAREDL​INKERF​LAGS−Wl,−−version−script{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 同目录移除无用代码开启 LTOLink 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 为 r16bO1 优化效果较差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 -fltoLOCAL_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-sectionsLOCAL_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 的加载速度。