
一、基础优化(AGP 默认支持)
AGP Strip 优化
- 原理 :通过 NDK 的
strip --strip-unneeded命令,删除 SO 中的调试信息(.debug_* section) 和符号表(.symtab) ,仅保留运行必需的动态符号表(.dynsym); - 作用:大幅缩减 SO 体积(示例:带调试信息的 136KB SO 优化后仅 14KB);
- 风险点 :若 AGP 找不到对应 ABI 的
strip工具(如 armeabi),会直接打包带调试信息的 SO,需关注编译日志(提示:Unable to strip library 'XXX.so' due to missing strip tool for ABI 'ARMEABI'); - 配置 :AGP 默认自动执行(任务
stripReleaseDebugSymbols),无需额外配置。
二、核心优化技术
2.1 精简动态符号表(减少 "符号表项 + 字符串池" 体积)
2.1.1 Visibility 全局控制
-
原理 :通过编译器参数
-fvisibility=VALUE控制全局符号可见性,减少非必要导出;default(默认):所有符号默认导出;hidden:所有符号默认隐藏,仅显式指定的符号导出。
-
配置方式:
-
CMake 项目:
cmake
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=hidden") -
ndk-build 项目:
makefile
LOCAL_CFLAGS += -fvisibility=hidden
-
-
补充 :单个符号可通过
__attribute__((visibility("hidden")))单独配置(优先级高于全局),示例:c
运行
__attribute__((visibility("hidden"))) int hiddenInt = 3; // 不导出该变量
2.1.2 Static 关键字控制
- 原理 :
static声明的函数 / 变量仅在当前文件可见,不进入动态符号表(仅删除符号表项,不删实现体); - 局限性:无法控制跨文件可见的符号,仅适用于单文件内部符号,不推荐作为主要方案。
2.1.3 Exclude Libs(控制静态库符号)
-
原理:通过链接器参数,排除依赖静态库(.a)中的符号导出(visibility/static 无法实现);
-
配置方式:
-
CMake 项目:
cmake
# 排除所有静态库符号导出 set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--exclude-libs,ALL") # 仅排除libabc.a的符号导出 set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--exclude-libs,libabc.a") -
ndk-build 项目:
makefile
LOCAL_LDFLAGS += -Wl,--exclude-libs,ALL
-
2.1.4 Version Script(推荐,精确控制)
-
原理 :通过链接器参数指定导出符号,支持静态库符号、通配符、删除默认符号(如
__bss_start),是最灵活的方案; -
核心优势:
- 统一管理导出符号,便于维护;
- 支持通配符(如
Java_*匹配所有 JNI 静态注册符号); - 可删除链接器默认添加的冗余符号。
-
配置步骤:
-
编写
version_script.txt(示例):txt
{ global: # 需导出的符号 JNI_OnLoad; # 动态注册JNI必需 JNI_OnUnload; # 资源清理必需 Java_*; # 静态注册JNI符号(可选) local: *; # 其余符号均隐藏 }; -
关联构建工具:
-
CMake 项目(文件与 CMakeLists.txt 同目录):
cmake
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/version_script.txt") -
ndk-build 项目(文件与 Android.mk 同目录):
makefile
LOCAL_LDFLAGS += -Wl,--version-script=${LOCAL_PATH}/version_script.txt
-
-
-
注意事项:
-
若符号通过
dlsym动态调用,需显式添加到global; -
C++ 符号需处理 "符号修饰":可通过
nm -D --defined-only abc.so | grep 函数名查看真实符号,或用extern "C++"语法(示例):txt
{ global: extern "C++" { MyClass::start*; # 通配符匹配重载 "MyClass::stop()";# 精确匹配(括号空格需一致) }; local: *; };
-
2.2 移除无用代码(DeadCode,减少.text/.data 体积)
2.2.1 LTO(Link Time Optimization,链接期优化)
-
原理:链接时基于全局信息检测 DeadCode(如永远为假的 if 分支、未调用的函数)并删除;编译期仅能获取局部信息,无法实现该优化;
-
目标文件格式:存储中间表示(IR)------GCC 用 GIMPLE,Clang 用 LLVM IR;
-
配置方式:
-
CMake 项目:
cmake
# 编译阶段开启LTO set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -flto") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto") # 链接阶段开启LTO(Clang必须,GCC可选)+ O3优化 set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -O3 -flto") -
ndk-build 项目:
makefile
LOCAL_CFLAGS += -flto LOCAL_LDFLAGS += -O3 -flto
-
-
注意事项:
- Clang 需同时在编译 / 链接阶段开启 LTO(NDK22 + 修复格式报错问题);
- 依赖的静态库需用 LTO 重新编译,才能删除其内部 DeadCode;
- 链接耗时会显著增加(需额外分析 IR)。
2.2.2 删除无用 Sections
-
原理:链接器仅保留动态符号直接 / 间接引用的 Section,删除无用 Section;需配合编译器参数减小 Section 粒度,避免 "有用 + 无用代码在同一 Section";
-
关键前提 :编译器参数
-fdata-sections(变量单独放 Section)、-ffunction-sections(函数单独放 Section)------AGP 默认已添加; -
配置方式:
-
CMake 项目:
cmake
# 显式声明Section粒度(AGP默认已加,可选) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fdata-sections -ffunction-sections") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fdata-sections -ffunction-sections") # 链接器开启GC Sections set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--gc-sections") -
ndk-build 项目:
makefile
LOCAL_CFLAGS += -fdata-sections -ffunction-sections LOCAL_LDFLAGS += -Wl,--gc-sections
-
2.3 优化指令长度(减少.text 体积)
-
原理:通过编译器优化级别,用更少的机器指令实现相同功能(优先减体积,性能可能略有损失);
-
核心参数:
Oz:仅 Clang 支持,在Os基础上进一步优化体积;Os:GCC/Clang 均支持,优化体积(接近O2性能);
-
配置方式 (Clang 用
Oz,GCC 用Os):-
CMake 项目:
cmake
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Oz") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Oz") -
ndk-build 项目:
makefile
LOCAL_CFLAGS += -Oz # GCC改为-Os
-
-
建议 :若项目原用
O3(性能优先),需测试Oz/Os的性能损失;若无明确优化级别,直接使用Oz(Clang)。
三、辅助优化措施
3.1 禁用 C++ 异常机制
-
原理 :若项目未使用
try...catch,禁用异常可删除相关冗余代码; -
配置方式:
-
CMake 项目:
cmake
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions") -
ndk-build 项目:默认禁用,无需配置(若已开启,需确认无依赖后禁用)。
-
3.2 禁用 C++ RTTI 机制
-
原理 :若项目未使用
typeid/dynamic_cast,禁用 RTTI 可删除相关元数据; -
配置方式:
-
CMake 项目:
cmake
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti") -
ndk-build 项目:默认禁用,无需配置。
-
3.3 合并 SO
-
适用场景 :多个 SO 存在 "单向依赖"(如
liba.so/libb.so仅被libx.so依赖); -
收益:
- 删除被依赖 SO 的导出符号表和依赖 SO 的导入符号表;
- 减少 PLT/GOT 表项(动态链接结构);
- 提升 LTO 优化效果(链接器获取完整上下文)。
3.4 提取多 SO 共同依赖库
- 适用场景 :多个 SO 静态依赖同一库(如
libc++.a); - 收益 :将共同依赖库提取为独立 SO(如
libc++_shared.so),改为动态依赖,避免代码重复; - 示例 :多个 SO 静态依赖
libc++.a→ 统一动态依赖libc++_shared.so。
四、工程实践关键技术
4.1 多构建工具支持
- 方案:在构建平台统一集成优化能力(支持 CMake/ndk-build/Make/GN 等),业务仅需简单配置(如勾选开关、指定导出符号),避免手动配置错误;
- 目标:降低配置成本,确保优化方案一致性。
4.2 优化效果验证
-
工具 :通过
nm命令查看导出符号是否符合预期; -
命令:
bash
# 查看SO的导出符号(--defined-only:仅显示当前SO定义的符号) nm -D --defined-only abc.so -
示例输出 (符合预期:仅保留
JNI_OnLoad和 JNI 符号):plaintext
00000658 T JNI_OnLoad 00000668 T Java_com_package_DemoActivity_stringFromJNI
4.3 崩溃堆栈解析
- 原理:优化仅删除动态符号表,未修改调试信息(.debug_*)和符号表(.symtab);
- 方案:编译时保留带调试信息的 SO,上传至 Crash 平台,崩溃时可还原源码文件名、行号、函数名。