【C++】【C++面试】Android SO 体积优化技术点梳理

一、基础优化(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),是最灵活的方案;

  • 核心优势

    1. 统一管理导出符号,便于维护;
    2. 支持通配符(如Java_*匹配所有 JNI 静态注册符号);
    3. 可删除链接器默认添加的冗余符号。
  • 配置步骤

    1. 编写version_script.txt(示例):

      txt

      复制代码
      {
          global:  # 需导出的符号
              JNI_OnLoad;    # 动态注册JNI必需
              JNI_OnUnload;  # 资源清理必需
              Java_*;        # 静态注册JNI符号(可选)
          local: *; # 其余符号均隐藏
      };
    2. 关联构建工具:

      • 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 体积)

  • 原理:链接时基于全局信息检测 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
  • 注意事项

    1. Clang 需同时在编译 / 链接阶段开启 LTO(NDK22 + 修复格式报错问题);
    2. 依赖的静态库需用 LTO 重新编译,才能删除其内部 DeadCode;
    3. 链接耗时会显著增加(需额外分析 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依赖);

  • 收益

    1. 删除被依赖 SO 的导出符号表和依赖 SO 的导入符号表;
    2. 减少 PLT/GOT 表项(动态链接结构);
    3. 提升 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 平台,崩溃时可还原源码文件名、行号、函数名。
相关推荐
万能的小裴同学5 小时前
C++ 鸭科夫手柄适配
开发语言·c++·算法
代码AC不AC5 小时前
【C++】哈希表实现 - 链地址法/哈希桶
c++·哈希算法·哈希·哈希桶·链地址法
小杰帅气5 小时前
STL_List简单使用
开发语言·c++·list
清辞8535 小时前
C++数据结构(链表和list)
数据结构·c++·链表
西哥写代码5 小时前
基于dcmtk的dicom工具 第十章 读取dicom文件图像数据并显示
c++·mfc·dcmtk·vs2017
拉不动的猪6 小时前
闭包实际项目中应用场景有哪些举例
前端·javascript·面试
Fox爱分享6 小时前
腾讯面试:都知道0.1+0.2≠0.3,为啥 0.1+0.1 却等于 0.2?
面试
lsnm8 小时前
C++新手项目-JsonRPC框架
开发语言·c++·1024程序员节
给大佬递杯卡布奇诺8 小时前
FFmpeg 基本数据结构 AVPacket分析
数据结构·c++·ffmpeg·音视频