编译一个尽量小的so

博客介绍了GCC的`-ffunction-sections`和`-fdata-sections`选项,以及LD的`--gc-sections`选项。这些选项用于将每个函数和数据项放入单独的段,并在链接时删除未使用的段,以减小静态链接文件的大小。文章通过实例展示了如何使用这些选项,并讨论了它们对性能和调试信息的影响,以及与其他编译器选项的配合使用。
  1. 参考文档

  2. gcc -ffunction-sections

    • 文档

      • 原文

        '''
        Place each function or data item into its own section in the output file if the
        target supports arbitrary sections. The name of the function or the name of
        the data item determines the section’s name in the output file.
        
        Use these options on systems where the linker can perform optimizations to
        improve locality of reference in the instruction space. Most systems using the
        ELF object format have linkers with such optimizations. On AIX, the linker
        rearranges sections (CSECTs) based on the call graph. The performance impact
        varies.
        
        Together with a linker garbage collection (linker ‘--gc-sections’ option) these
        options may lead to smaller statically-linked executables (after stripping).
        On ELF/DWARF systems these options do not degenerate the quality of the
        debug information. There could be issues with other object files/debug info
        formats.
        
        Only use these options when there are significant benefits from doing so. When
        you specify these options, the assembler and linker create larger object and
        executable files and are also slower. These options affect code generation. They
        prevent optimizations by the compiler and assembler using relative locations
        inside a translation unit since the locations are unknown until link time. An
        example of such an optimization is relaxing calls to short call instructions.
        '''
        
      • 翻译

        • 如果目标文件支持任意数量的段,那么为每个函数和全局,静态变量创建一个段,并存放对应代码.段名是.text | .data前缀,后跟对应的名字.函数对应.text开头,变量对应.data开头.
        • gcc -c test.c -ffunction-sections -fdata-sections && readelf -e test.o,注意test.c里面的函数变量尽量不要和API重名了.否则结果会异常.
        • 链接器可以执行将调用依赖的代码紧密的拼凑在一起.以此优化本地指令空间. 大多数ELF系统都支持这个选项.有的系统甚至根据调用图来对代码顺序进行重排.
        • ld--gc-sectioins选项,可以创建一个静态链接的文件,代码区仅仅包含自己用到的变量和函数.没有其他额外的符号.
        • ELF/DWARF格式的调试信息和这个编译选项不存在冲突.但是其他格式的可能存在冲突.
        • 真的可以通过这个编译选项获得好处的时候才用,不然,尽量不要用. 这个编译选项会导致汇编器和链接器生成的文件变大,而且可执行文件也会执行得更慢. 这些文件还会影响代码生成. 这个代码禁止相对路径优化. 因为都变成了段代码,不知道位置,只有链接的时候才知道.
    • 案例

      • 代码

        void show(){}
        void cool(){}
        int main()
        {
           show();
           cool();
        }
        
        
        • gcc -c test.c -ffunction-sections -fdata-sections
      • test.o:     file format elf64-x86-64
        
        
        Disassembly of section .text.show:
        
        0000000000000000 <show>:
          0:   55                      push   %rbp
          1:   48 89 e5                mov    %rsp,%rbp
          4:   5d                      pop    %rbp
          5:   c3                      retq
        
        Disassembly of section .text.cool:
        
        0000000000000000 <cool>:
          0:   55                      push   %rbp
          1:   48 89 e5                mov    %rsp,%rbp
          4:   5d                      pop    %rbp
          5:   c3                      retq
        
        Disassembly of section .text.main:
        
        0000000000000000 <main>:
          0:   55                      push   %rbp
          1:   48 89 e5                mov    %rsp,%rbp
          4:   b8 00 00 00 00          mov    $0x0,%eax
          9:   e8 00 00 00 00          callq  e <main+0xe>
          e:   b8 00 00 00 00          mov    $0x0,%eax
         13:   e8 00 00 00 00          callq  18 <main+0x18>
         18:   5d                      pop    %rbp
         19:   c3                      retq
        
        • objdump -d test.o
      • 说明

        • 生成了函数对应的段.
      • 去掉main中的show

        • 编译后的代码gcc -Wl,--gc-sections test.o -o test.out
  3. ld --gc-sections

    • 说明

      • 功能

        • 尽量的保留用到的段,没有用的都删掉.
    • 文档

      • 原文

        Enable garbage collection of unused input sections. It is ignored on targets
        that do not support this option. The default behaviour (of not performing this
        garbage collection) can be restored by specifying ‘--no-gc-sections’ on the
        command line. Note that garbage collection for COFF and PE format targets is
        supported, but the implementation is currently considered to be experimental.--gc-sections’ decides which input sections are used by examining symbols
        and relocations. The section containing the entry symbol and all sections 
        containing symbols undefined on the command-line will be kept, as will sections
        containing symbols referenced by dynamic objects. Note that when building
        shared libraries, the linker must assume that any visible symbol is referenced.
        Once this initial set of sections has been determined, the linker recursively
        marks as used any section referenced by their relocations. See ‘--entry’,--undefined’, and ‘--gc-keep-exported’.
        
        This option can be set when doing a partial link (enabled with option ‘-r’). In
        this case the root of symbols kept must be explicitly specified either by one of
        the options ‘--entry’,--undefined’, or ‘--gc-keep-exported’ or by a ENTRY
        command in the linker script.
        
        As a GNU extension, ELF input sections marked with the SHF_GNU_RETAIN
        flag will not be garbage collected.
        
      • 翻译

        • 允许垃圾回收机制回收未使用的段(即用到的段才放入最终生成产物,没有用到的就删除掉.).对于不支持的平台,这个参数会被ld忽略.默认不开启,可以对一些.o文件开启,对一些关闭(ld --gc-sections a.o --no-gc-sections,就只有a.o才会进行选择性提取).垃圾收集机制对于COFF,PE格式的.o文件同样支持,但是目前是开发体验版,不建议用.
        • --gc-sections会检测哪些断和符号被用到了. 符号表相关的段保留;段中有通过指令指定的未定义的符号(ld -u name指定的符号,表示强制未定义);段中的符号被其他动态对象引用. 编译共享库,所有可见的符号都被当成是被引用了(可以使用可见性控制机制来编译,__attribute__的形式指定某几个,或者编译时 -fvisibility=xxx 设置整个). 第一阶段处理完保留的段,第二阶段就是递归的检测引用到的代码,即间接依赖的段.
        • 当只需要链接部分内容的时候可以使用这个选(-r选项是编译可重定位类型,即.o),那么保留的符号就可以使用--entry,--undefined,--gc-keep-exported,ENTRY来设定.
      • 总结

        • 保留用到的,并且保留依赖的.
        • 一般和-ffunction-sections,-fdata-sections使用.
  4. 编译案例

    • 普通

      • 代码

        void show(){}
        void cool(){}
        int main()
        {
           cool();
        }
        
        
      • 编译后

        • 编译:gcc -c test.c -ffunction-sections -fdata-sections
        • objdump -d test.o | less
        test.o:     file format elf64-x86-64
        
        
        Disassembly of section .text.show:
        
        0000000000000000 <show>:
          0:   55                      push   %rbp
          1:   48 89 e5                mov    %rsp,%rbp
          4:   5d                      pop    %rbp
          5:   c3                      retq
        
        Disassembly of section .text.cool:
        
        0000000000000000 <cool>:
          0:   55                      push   %rbp
          1:   48 89 e5                mov    %rsp,%rbp
          4:   5d                      pop    %rbp
          5:   c3                      retq
        
        Disassembly of section .text.main:
        
        0000000000000000 <main>:
          0:   55                      push   %rbp
          1:   48 89 e5                mov    %rsp,%rbp
          4:   b8 00 00 00 00          mov    $0x0,%eax
          9:   e8 00 00 00 00          callq  e <main+0xe>
          e:   5d                      pop    %rbp
          f:   c3                      retq
        
        
        • 生成:gcc test.o -Wl,-gc-sections -o test.out
        • 查看:objdump -d test.out
        test.out:     file format elf64-x86-64
        
        
        Disassembly of section .text:
        
        00000000004000b0 <cool>:
         4000b0:       55                      push   %rbp
         4000b1:       48 89 e5                mov    %rsp,%rbp
         4000b4:       5d                      pop    %rbp
         4000b5:       c3                      retq
        
        00000000004000b6 <main>:
         4000b6:       55                      push   %rbp
         4000b7:       48 89 e5                mov    %rsp,%rbp
         4000ba:       b8 00 00 00 00          mov    $0x0,%eax
         4000bf:       e8 ec ff ff ff          callq  4000b0 <cool>
         4000c4:       5d                      pop    %rbp
         4000c5:       c3                      retq
        
        
        • 测试实际指令
        gcc -c test.c -ffunction-sections -fdata-sections -fno-builtin
        ld test.o --gc-sections -static -o test.out -e main
        objdump -d test.out | less
        
    • 共享库

      • 代码

        • 同上
      • 编译后

        • gcc -fPIC -shared -c test.c -ffunction-sections -fdata-sections -fno-builtin
        • ld test.o -fPIC -shared --gc-sections -static -o test.so
        test.so:     file format elf64-x86-64
        
        
        Disassembly of section .plt:
        
        0000000000000280 <.plt>:
        280:   ff 35 82 0d 20 00       pushq  0x200d82(%rip)        # 201008 <_GLOBAL_OFFSET_TABLE_+0x8>
        286:   ff 25 84 0d 20 00       jmpq   *0x200d84(%rip)        # 201010 <_GLOBAL_OFFSET_TABLE_+0x10>
        28c:   0f 1f 40 00             nopl   0x0(%rax)
        
        0000000000000290 <cool@plt>:
        290:   ff 25 82 0d 20 00       jmpq   *0x200d82(%rip)        # 201018 <cool+0x200d72>
        296:   68 00 00 00 00          pushq  $0x0
        29b:   e9 e0 ff ff ff          jmpq   280 <.plt>
        
        Disassembly of section .text:
        
        00000000000002a0 <show>:
        2a0:   55                      push   %rbp
        2a1:   48 89 e5                mov    %rsp,%rbp
        2a4:   5d                      pop    %rbp
        2a5:   c3                      retq
        
        00000000000002a6 <cool>:
        2a6:   55                      push   %rbp
        2a7:   48 89 e5                mov    %rsp,%rbp
        2aa:   5d                      pop    %rbp
        2ab:   c3                      retq
        
        00000000000002ac <main>:
        2ac:   55                      push   %rbp
        2ad:   48 89 e5                mov    %rsp,%rbp
        2b0:   b8 00 00 00 00          mov    $0x0,%eax
        2b5:   e8 d6 ff ff ff          callq  290 <cool@plt>
        2ba:   5d                      pop    %rbp
        2bb:   c3                      retq
        
        • 这里没有设置可见性.默认都可见.
    • 不可见

      • 说明

        • 所有符号都不可见
      • 代码

        //#pragma GCC visibility push(hidden)
        void show(){}
        void cool(){}
        //#pragma GCC visibility pop
        int __attribute__((visibility("default"))) main()
        {
           //show();
           cool();
        }
        
        
      • 编译

        • gcc -fPIC -shared -c test.c -ffunction-sections -fdata-sections -fno-builtin -fvisibility=hidden
        • ld test.o -fPIC -shared --gc-sections -static -o test.so -u main
  5. 可见性控制

    • 编译控制

      • 编译选项-fvisibility=[default|internal|hidden|protected]
    • 代码宏定义

      • #pragma GCC visibility push(hidden) | #pragma GCC visibility pop
      • 以栈顶的属性为准.pop为弹栈.栈顶到pop都是以栈顶的为主.
    • 属性

      • __attribute__ ((visibility("default")))
      • ret_type attribute funname(__V_ARGS__)
    • 优先级

      • 属性>代码>编译控制
<think>嗯,用户询问的是Android Studio编译生成的APK文件大小异常的原因及解决方法。这确实是个常见问题,值得深入分析。 用户可能正在开发Android应用,突然发现生成的APK文件比预期大很多,这会影响应用下载和安装体验。从引用内容看,用户似乎已经掌握了基本打包流程,但遇到了文件大小问题。 关于APK文件过大的原因,需要从多个维度考虑。首先是资源文件问题,未压缩的图片、视频资源会显著增大体积。其次是代码优化,ProGuard或R8配置不当会导致无用代码未被剔除。还有依赖库问题,特别是引入大型第三方库时容易失控。最后是构建配置,比如未启用资源缩减或分ABI构建。 解决方法方面,应该系统性地建议用户排查。先从资源入手,检查图片是否使用WebP格式,音频是否压缩。然后分析依赖,用Android Studio的APK分析工具查看占比大的模块。代码优化方面要确保minifyEnabled开启。对于原生库,分ABI打包很关键。用户可能不知道这些优化选项默认不开启,需要明确指导配置位置。 用户可能还关心持续监控方法,可以建议在build.gradle中设置size阈值告警。如果用户目标是应用商店发布,还要提一下AAB格式的优势。 考虑到用户技术背景,回复要包含具体操作路径(如Build > Analyze APK)和配置代码示例(如splits配置)。同时提醒常见误区,比如误以为debug包大小就是release包大小。 最后需要强调,文件大小优化是持续过程,建议结合CI定期检查。如果用户有后续问题,可以再深入讨论具体模块的优化方案。</think>Android Studio 编译生成的 APK 文件大小异常(过大)是常见问题,通常由以下原因引起,并有对应的解决方法: --- ### **一、主要原因分析** 1. **未优化资源文件** * **图片/音频/视频未压缩:** 高分辨率图片、未压缩的音频/视频是体积膨胀的主因。 * **冗余资源:** 项目中包含未使用的图片、布局文件、字符串等资源。 * **未使用矢量图:** 能用 `VectorDrawable`(.xml)代替的位图(.png, .jpg)未替换。 2. **未启用代码压缩与混淆** * **未开启 R8/ProGuard:** 默认情况下,代码混淆和优化(移除未使用的类/方法/字段)在 Release 构建中**不会自动启用**,导致无用代码、调试信息、冗长符号名保留。 3. **包含大量或未优化的依赖库** * **大型库:** 引入功能庞大但实际只用其小部分功能的库(如完整版 Google Play Services)。 * **重复库:** 不同依赖可能包含相同库的不同版本。 * **未裁剪原生库:** `.so` 原生库通常按 ABI(如 armeabi-v7a, arm64-v8a)打包,未配置会包含所有支持 ABI 的库副本。 4. **调试信息与符号未剥离** * **未剥离 Native 符号:** 编译原生代码(C/C++)时,调试符号(`symbols`)未从 `.so` 文件中移除。 * **包含调试资源:** Debug 构建包含额外调试信息(如行号映射)。 5. **构建配置不当** * **未启用资源缩减:** `shrinkResources` 选项未开启,无法自动移除未使用的资源。 * **未启用配置过滤:** 未过滤不需要的语言资源或屏幕密度资源。 * **Multi-Dex 限制:** 方法数超限导致启用 Multi-Dex 会增加额外开销。 6. **包含不必要的元数据/文件** * 测试代码、示例文件、大型文档意外打包。 --- ### **二、解决方法与优化策略** 1. **启用代码压缩与混淆** * 在 `app` 模块的 `build.gradle` 中配置: ```gradle android { buildTypes { release { minifyEnabled true // 启用代码压缩、混淆和优化 shrinkResources true // 启用资源缩减(依赖 minifyEnabled) proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } ``` * **验证混淆规则:** 确保 `proguard-rules.pro` 正确保留必要的类/方法(如 Activity、View、JNI 方法、反射类、序列化类)。 2. **优化资源文件** * **压缩图片:** * 使用 **WebP** 格式替代 PNG/JPG(Android Studio 右键图片 > `Convert to WebP`)。 * 使用工具(如 TinyPNG, pngquant)离线压缩无法转换的图片。 * **移除未使用资源:** * 使用 **Lint**:`Analyze > Run Inspection by Name > Unused resources`。 * 使用 **资源缩减** (`shrinkResources true`)。 * **优先使用矢量图:** 简单图标、可绘制图形尽量用 `VectorDrawable`。 * **限制支持的语言与密度:** 在 `build.gradle` 中配置 `resConfigs`: ```gradle android { defaultConfig { resConfigs "en", "zh-rCN" // 只打包英文和简体中文资源 resConfigs "xxhdpi", "xxxhdpi" // 只打包常见高密度资源(按需选择) } } ``` 3. **优化依赖库** * **使用按需依赖:** 特别是 Google Play Services: ```gradle implementation 'com.google.android.gms:play-services-location:20.0.1' // 只引入位置服务 // 而不是 implementation 'com.google.android.gms:play-services:20.0.1' ``` * **检查重复依赖:** 运行 `./gradlew app:dependencies` 查看依赖树,移除冲突或重复项。 * **评估库必要性:** 是否能用更轻量库替代?功能是否可自行实现? 4. **分ABI打包原生库** * 在 `build.gradle` 中配置 `splits`: ```gradle android { splits { abi { enable true // 启用ABI分包 reset() include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' // 指定支持的ABI universalApk false // 不生成包含所有ABI的通用APK } } } ``` * **剥离原生调试符号:** 确保 Release 构建配置正确(通常 CMake/ndk-build 默认在 Release 下会剥离)。 5. **启用资源缩减** * 确保 `minifyEnabled true` 和 `shrinkResources true` 同时开启。 * **定义保留规则:** 如需动态加载资源,在 `res/raw/keep.xml` 中指定保留的资源: ```xml <?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools" tools:keep="@layout/used_layout_*, @drawable/keep_me" /> ``` 6. **使用 APK 分析工具** * **Android Studio 内置分析器:** 1. 生成 Release APK (`Build > Build Bundle(s) / APK(s) > Build APK`)。 2. 将 APK 拖入 Android Studio 窗口,或使用 `Build > Analyze APK...`。 * **分析内容:** * 查看 **`classes.dex`**:代码体积是否过大?检查混淆效果。 * 查看 **`res`**:哪些图片/资源占用最多?是否有优化空间? * 查看 **`lib`**:原生库是否按 ABI 分包?单一库是否过大? * 查看 **`assets`**:是否有不必要的大文件? * 查看 **依赖项占比**:哪些第三方库体积最大?能否优化? 7. **考虑使用 Android App Bundle** * **AAB 优势:** 上传到 Google Play 后,商店会动态生成针对用户设备(语言、屏幕密度、ABI)优化的 APK,显著减少下载大小。 * **生成方式:** `Build > Generate Signed Bundle / APK... > Android App Bundle`。 8. **清理构建缓存** * 有时旧的缓存文件可能导致问题:`File > Invalidate Caches / Restart... > Invalidate and Restart`。 --- ### **三、关键检查点总结** | 问题类型 | 检查点 | 工具/配置 | |-------------------|-----------------------------------------------------------------------|--------------------------------------------------------------------------| | **资源过大** | 图片格式(WebP优先)、未使用资源、矢量图使用情况、语言/密度过滤 | Lint, `shrinkResources`, `resConfigs`, Analyze APK | | **代码过大** | 是否开启混淆/优化 (`minifyEnabled`)、ProGuard规则是否正确、依赖库大小 | `minifyEnabled true`, `proguard-rules.pro`, Analyze APK (classes.dex) | | **原生库过大** | 是否分 ABI 打包、是否剥离调试符号 | `splits.abi`, Analyze APK (lib/), NDK Release 配置 | | **依赖库臃肿** | 是否使用按需依赖、是否存在重复库、能否替换轻量库 | `./gradlew app:dependencies`, Analyze APK (查看具体库大小) | | **元数据/文件** | APK中是否包含测试文件、文档、无用 assets | Analyze APK | --- **建议步骤:** 1. **生成 Release APK**(确保签名配置正确)[^1][^3]。 2. **立即使用 Analyze APK** 定位体积最大的部分。 3. **根据分析结果**,按优先级应用上述优化策略(如启用压缩混淆、优化资源、分ABI)。 4. **重复步骤1-3**,比较优化前后 APK 大小。 5. **考虑发布 AAB** 以获得最佳的商店分发优化效果。 通过系统性地应用这些策略,通常能将 APK 体积显著缩减。优化是一个持续过程,每次添加新功能或依赖后都应重新审视大小。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值