C/C++ Native 包大小测量

Posted 芥末的无奈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C/C++ Native 包大小测量相关的知识,希望对你有一定的参考价值。

C/C++ Native 包大小测量

公司内部有很多 C/C++ native 库,例如 XXSDK、YYYSDK、ZZZSDK 等等,随着业务迭代包体积也在逐渐膨胀,时不时需要来一波包大小裁剪的工作,app 体积控制在一定范围内。此外,在 Tob 业务中,客户对包体积也可能有要求。

裁剪包大小的前提是,我们要获取准确的包大小信息。这里想和大家分享的是如何手动地测试移动端 C/C++ SDK 包大小。在我们公司,习惯在 android 上交付动态库,而在 ios 交付静态库。两种库的测量方式略有不同,因此分开讨论。

测试 demo

开始之前,让我们搭建一个简单的 demo 用于测试:SimpleDecoder,一个简单的 SDK,用于音频文件的解码,其实现是对 dr_libs 的封装,接口包括:

typedef struct SimpleWavDecoder SimpleWavDecoder;

/**
 * Create wave decoder from file path, return NULL if read file failed
 */
SimpleWavDecoder *SD_createFromFile(const char *file_path);

/**
 * Return the number of channels
 */
int SD_getNumChannels(SimpleWavDecoder *decoder);

/**
 * Return the sample rate
 */
int SD_getSampleRate(SimpleWavDecoder *decoder);

/**
 * Decode from wav file and fill the samples to `out_interleave_data`, 
 * returns the number of frames actually decoded.
 */
size_t SD_read(SimpleWavDecoder *decoder, float *out_interleave_data, int num_samples_per_channel);

/**
 * Destroy decoder and release resources
 */
int SD_destroy(SimpleWavDecoder *decoder);

还提供了两个脚本,用于 android 和 ios 库的编译:

  • android_build.sh:安装 NDK 后,将脚本中 ANDROID_NDK_HOME 修改为 NDK 路径。运行脚本后,在 android_64_build/src 下能够找到产物 libsimple_decoder.so
  • ios_build.sh:运行脚本后,在 ios_build/src/ 下能够找到产物 libsimple_decoder.a

此外在 main.cpp 中有一些 api 的测试。

Android .so 动态库包大小测量

运行 android_build.sh 脚本后,我们得到了 libsimple_decoder.so。为了分析 .so 中的各部分大小情况,我们引入 bloaty 工具。

Bloaty

bloaty 是谷歌开源的一款工具,Bloaty会向你展示二进制文件的大小概况,这样你就可以了解是什么占用了里面的空间。

Bloaty对二进制文件进行了深入分析。使用定制的ELF、DWARF和Mach-O分析器,Bloaty旨在准确地将二进制文件的每一个字节归属于产生它的符号或编译单元。它甚至会分解二进制文件,寻找对匿名数据的引用。

按照 README.md 中的安装指南,编译好 bloaty 后就能够使用,不需要额外的依赖。

使用 Bloaty

接着,用 bloaty 对我们的 so 进行一番检测。最简单的命令某过于 bloaty libsimple_decoder.so,输出大概是:

    FILE SIZE        VM SIZE
 --------------  --------------
  21.8%   312Ki   0.0%       0    .debug_info
  21.0%   300Ki   0.0%       0    .debug_loc
  11.9%   170Ki  59.2%   169Ki    .text
  10.7%   152Ki   0.0%       0    .debug_str
   9.8%   139Ki   0.0%       0    .debug_line
   7.4%   106Ki   0.0%       0    .debug_ranges
   4.0%  57.4Ki   0.0%       0    .symtab
   3.9%  55.6Ki   0.0%       0    .strtab
   2.1%  29.8Ki  10.4%  29.8Ki    .rela.dyn
   1.4%  20.6Ki   7.2%  20.6Ki    .eh_frame
   1.0%  14.7Ki   5.1%  14.6Ki    .rodata
   1.0%  13.9Ki   0.0%       0    .debug_abbrev
   0.8%  10.8Ki   3.7%  10.8Ki    .data.rel.ro
   0.8%  10.8Ki   3.7%  10.7Ki    .dynsym
   0.7%  9.51Ki   3.1%  8.97Ki    [18 Others]
   0.6%  8.20Ki   2.8%  8.14Ki    .dynstr
   0.3%  4.98Ki   1.7%  4.92Ki    .eh_frame_hdr
   0.2%  3.41Ki   0.0%       0    [Unmapped]
   0.2%  3.20Ki   1.1%  3.14Ki    .gnu.hash
   0.2%  2.99Ki   1.0%  2.93Ki    .rela.plt
   0.2%  2.88Ki   1.0%  2.82Ki    .hash
 100.0%  1.40Mi 100.0%   287Ki    TOTAL

其中,FILE SIZE 列表示 .so 在硬盘中占用了多少空间;VM SIZE 列表示 .so 加载到内存后占用了多少空间。它们两可能非常不同:

  • 有些数据存在于文件中,但没有被加载至内存,例如调试信息。
  • 有些数据被映射到了内存,但不存在于文件之中,主要是 .bss 段中初始化为 0 的部分。

可以看到,总的 FILE SIZE 是 1.4MB,也就是 .so 在硬盘上的大小;VM SIZE 则是 287KB,由于 FILE SIZE 和 VM SIZE 的差异主要是在调试信息,我们可以通过 strip 命令来移除调试信息,可以发现经过 strip 后的 .so 和 VM SIZE 相差不大。

最右边的一列,展示了每个 section 的大小情况。bloaty 默认的分解方式是基于 sections,但也支持其他方式,例如符号(symbols)或者是 segments。例如:

$ bloaty -d symbols libsimple_decoder.so
    FILE SIZE        VM SIZE
 --------------  --------------
  23.6%   337Ki  80.1%   230Ki    [787 Others]
  21.8%   312Ki   0.0%       0    [section .debug_info]
  21.0%   300Ki   0.0%       0    [section .debug_loc]
  10.7%   152Ki   0.0%       0    [section .debug_str]
   9.8%   139Ki   0.0%       0    [section .debug_line]
   7.4%   106Ki   0.0%       0    [section .debug_ranges]
   1.0%  13.9Ki   0.0%       0    [section .debug_abbrev]
   0.7%  10.4Ki   3.6%  10.4Ki    [section .rodata]
   0.5%  7.25Ki   2.5%  7.08Ki    (anonymous namespace)::itanium_demangle::AbstractManglingParser<>::parseExpr()
   0.4%  6.31Ki   2.1%  6.17Ki    drwav__write_or_count_metadata()
   0.4%  6.02Ki   0.0%       0    [section .symtab]
   0.4%  5.84Ki   2.0%  5.67Ki    (anonymous namespace)::itanium_demangle::AbstractManglingParser<>::parseType()
   0.3%  4.91Ki   1.6%  4.72Ki    (anonymous namespace)::itanium_demangle::AbstractManglingParser<>::parseOperatorName()
   0.3%  4.13Ki   1.3%  3.84Ki    drwav_init__internal()
   0.3%  3.75Ki   1.2%  3.58Ki    (anonymous namespace)::itanium_demangle::AbstractManglingParser<>::parseFoldExpr()
   0.3%  3.74Ki   1.3%  3.61Ki    drwav__metadata_process_chunk()
   0.2%  3.41Ki   0.0%       0    [Unmapped]
   0.2%  3.29Ki   1.1%  3.11Ki    (anonymous namespace)::itanium_demangle::AbstractManglingParser<>::parseName()
   0.2%  3.14Ki   1.1%  3.14Ki    [section .gnu.hash]
   0.2%  3.08Ki   1.0%  2.88Ki    (anonymous namespace)::itanium_demangle::AbstractManglingParser<>::parseEncoding()
   0.2%  2.97Ki   1.0%  2.88Ki    drwav_read_pcm_frames_s16__ima()
 100.0%  1.40Mi 100.0%   287Ki    TOTAL

如果 so 中包含调试信息(没有经过 strip )bloaty 还能够看到编译单元的大小大小情况,
如果你的库比较复杂,包含很多 .c/.cpp,那这个命令非常有用,可以让你看到每个 .c/.cpp 的大小,进行精确的评估和裁剪:

$ bloaty -d compileunits libsimple_decoder.so
    FILE SIZE        VM SIZE
 --------------  --------------
  54.6%   780Ki  26.4%  75.9Ki    /Volumes/Android/buildbot/src/android/ndk-release-r21/external/libcxx/../../external/libcxxabi/src/cxa_demangle.cpp
  16.0%   228Ki  30.6%  87.8Ki    /Users/bytedance/Documents/develop/how_to_test_package_size/src/simple_decoder.cpp
   4.2%  59.5Ki   2.8%  7.97Ki    /Volumes/Android/buildbot/src/android/ndk-release-r21/external/libcxx/../../external/libcxxabi/src/private_typeinfo.cpp
   3.9%  55.3Ki   3.6%  10.3Ki    /Volumes/Android/buildbot/src/android/gcc/toolchain/build/../gcc/gcc-4.9/libgcc/unwind-dw2.c
   2.9%  41.0Ki   2.8%  8.12Ki    /Volumes/Android/buildbot/src/android/gcc/toolchain/build/../gcc/gcc-4.9/libgcc/unwind-dw2-fde-dip.c
   2.4%  34.4Ki   7.2%  20.8Ki    [31 Others]
   2.1%  29.7Ki  10.3%  29.7Ki    [section .rela.dyn]
   2.1%  29.5Ki   1.3%  3.80Ki    /Volumes/Android/buildbot/src/android/ndk-release-r21/external/libcxx/../../external/libcxxabi/src/cxa_exception.cpp
   1.9%  26.7Ki   0.0%       0    [section .symtab]
   1.9%  26.5Ki   1.2%  3.51Ki    /Volumes/Android/buildbot/src/android/ndk-release-r21/external/libcxx/../../external/libcxxabi/src/cxa_personality.cpp
   1.3%  18.8Ki   0.0%       0    [section .strtab]
   1.1%  15.4Ki   0.8%  2.20Ki    /Volumes/Android/buildbot/src/android/ndk-release-r21/external/libcxx/src/new.cpp
   1.0%  14.6Ki   5.1%  14.6Ki    [section .rodata]
   0.8%  11.9Ki   0.3%    1016    /Volumes/Android/buildbot/src/android/ndk-release-r21/external/libcxx/../../external/libcxxabi/src/fallback_malloc.cpp
   0.8%  10.8Ki   3.7%  10.8Ki    [section .data.rel.ro]
   0.6%  8.98Ki   0.3%     800    /Volumes/Android/buildbot/src/android/ndk-release-r21/external/libcxx/../../external/libcxxabi/src/cxa_default_handlers.cpp
   0.6%  8.91Ki   0.3%     869    /Volumes/Android/buildbot/src/android/ndk-release-r21/external/libcxx/../../external/libcxxabi/src/cxa_handlers.cpp
   0.5%  7.52Ki   0.4%  1.13Ki    /Volumes/Android/buildbot/src/android/ndk-release-r21/external/libcxx/../../external/libcxxabi/src/stdlib_typeinfo.cpp
   0.5%  7.37Ki   0.2%     557    /Volumes/Android/buildbot/src/android/ndk-release-r21/external/libcxx/../../external/libcxxabi/src/cxa_exception_storage.cpp
   0.5%  7.05Ki   0.5%  1.41Ki    /Volumes/Android/buildbot/src/android/ndk-release-r21/external/libcxx/../../external/libcxxabi/src/stdlib_exception.cpp
   0.4%  6.09Ki   2.1%  6.09Ki    [section .dynsym]
 100.0%  1.40Mi 100.0%   287Ki    TOTAL

以上就是 bloaty 的简单使用,更多细节的使用请参阅使用文档,例如对比两个 .so 获得增量、对结果进行排序、指定输出数量(默认为20)等等。

分析与裁剪

上面简单使用中,默认显示 20 行数据,为了更准确的分析,我们最好导出全部数据,-n 0 可以将所有数据导出,例如:

bloaty -d compileunits -n 0 libsimple_decoder.so > compileunits.txt
bloaty -d symbols -n 0 libsimple_decoder.so > symbols.txt

compileunits.txt 可以发现,除了 simple_decoder.cpp 外,还有很多 c++ 标准库的 .c/.cpp 被编译进来了,例如 new.cpp。这是因为编译 android so 库我们使用了静态的 stl 库(参考 C++ 库支持)。现在 Android App 中多数使用的共享 STL 库,因此我们的 .so 也可以改用共享 STL 库进行编译,修改 android_build.sh 中 cmake 命令,添加 -DANDROID_STL=c++_shared

...
cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \\
      -DANDROID_ABI=arm64-v8a \\
      -DBUILD_SHARED_LIBS=ON \\
      -DANDROID_STL=c++_shared \\
      -S . -B android_64_build
...

使用 bloaty 查看下新编译的 .so 发现 stl 的编译单元不见了,到大小也从 287KB 减小到了 122KB

$ bloaty -d compileunits libsimple_decoder.so
    FILE SIZE        VM SIZE
 --------------  --------------
  63.3%   218Ki  71.8%  87.8Ki    /Users/bytedance/Documents/develop/how_to_test_package_size/src/simple_decoder.cpp
  16.2%  55.8Ki   8.5%  10.4Ki    /Volumes/Android/buildbot/src/android/gcc/toolchain/build/../gcc/gcc-4.9/libgcc/unwind-dw2.c
  11.9%  41.1Ki   6.6%  8.12Ki    /Volumes/Android/buildbot/src/android/gcc/toolchain/build/../gcc/gcc-4.9/libgcc/unwind-dw2-fde-dip.c
   1.6%  5.67Ki   4.6%  5.67Ki    [section .rodata]
   1.1%  3.87Ki   0.0%       0    [Unmapped]
   0.9%  3.16Ki   0.0%       0    [section .symtab]
   0.6%  2.12Ki   0.0%       0    [ELF Headers]
   0.6%  1.97Ki   1.6%  1.97Ki    [section .rela.plt]
   0.5%  1.89Ki   0.7%     924    [17 Others]
   0.4%  1.42Ki   0.0%       0    [section .debug_str]
   0.4%  1.34Ki   1.1%  1.34Ki    [section .plt]
   0.4%  1.27Ki   0.0%       0    [section .strtab]
   0.4%  1.23Ki   0.0%       0    [section .debug_abbrev]
   0.3%  1.08Ki   0.9%  1.08Ki    [section .hash]
   0.3%  1.01Ki   0.8%  1.01Ki    [section .data]
   0.3%     968   0.8%     968    [section .gnu.hash]
   0.3%     912   0.7%     912    [section .dynsym]
   0.2%     712   0.6%     712    [section .got]
   0.1%     528   0.4%     528    [LOAD #0 [RX]]
   0.1%     496   0.4%     496    [section .dynamic]
   0.1%     427   0.3%     427    [section .dynstr]
 100.0%   345Ki 100.0%   122Ki    TOTAL

现在 unwind-dw2.cunwind-dw2-fde-dip.c 看起来非常的可疑,这两个文件是处理异常的,但我们的实现中并未有异常处理的逻辑,似乎是因为使用 unique_ptr 导致的,因此我们修改 simple_decoder.cpp 实现,移除对 unique_ptr 的使用:

SimpleWavDecoder* SD_createFromFile(const char* file_path)

    SimpleWavDecoder* decoder =  new SimpleWavDecoder;
    if(!drwav_init_file(&decoder->wav, file_path, NULL))
        delete decoder;
        decoder = nullptr;
    
    return decoder;

重新编译,并用 bloaty 查看,两个 .c 文件已经被移除了,包大小也降低到 100KB

$ bloaty -d compileunits libsimple_decoder.so
    FILE SIZE        VM SIZE
 --------------  --------------
  89.7%   205Ki  85.5%  85.9Ki    /Users/bytedance/Documents/develop/how_to_test_package_size/src/simple_decoder.cpp
   2.4%  5.50Ki   5.5%  5.50Ki    [section .rodata]
   1.2%  2.67Ki   0.0%       0    [section .symtab]
   0.7%  1.71Ki   1.7%  1.71Ki    [section .rela.plt]
   0.7%  1.69Ki   0.0%       0    [ELF Headers]
   0.5%  1.17Ki   1.2%  1.17Ki    [section .plt]
   0.5%  1.15Ki   0.0%       0    [Unmapped]
   0.4%  1.00Ki   0.0%       0    [section .strtab]
   0.4%    1024   1.0%    1024    [section .data]
   0.4%     956   0.9%     956    [section .gnu.hash]
   0.4%     920   0.9%     920    [section .hash]
   0.4%     891   0.0%       0    [section .debug_abbrev]
   0.4%     856   0.7%     674    [12 Others]
   0.3%     821   0.0%       0    [section .debug_str]
   0.3%     768   0.7%     768    [section .dynsym]
   0.3%     616   0.6%     616    [section .got]
   0.2%     527   0.5%     527    [LOAD #0 [RX]]
   0.2%     496   0.5%     496    [section .dynamic]
   0.1%     342   0.0%       0
   0.1%     321   0.0%       0    [section .shstrtab]
   0.1%     315   0.3%     315    [section .dynstr]
 100.0%   229Ki 100.0%   100Ki    TOTAL

以上就是利用 bloaty 进行分析与裁剪的过程,可以总结为:

  1. 编译 .so
  2. 利用 bloaty 进行分析,找出可以优化的部分
  3. 进行优化,重复步骤 1 至 3

iOS .a 静态库包大小测量

.a 包大小测量比起 .so 会麻烦一些,.a 本质上是一顿 .o 的集合,例如常见的 libm.a ,它里头可能有几十上百个 .o,当可执行程序使用到了 .a 中的符号时,可执行程序才会找到对应的 .o 并将它拷贝到程序中。

也就是说,对于静态链接,可执行程序是用多少取多少,可能你的 .a 有 1MB 大小,但可执行程序只用到其中一个符号,最终占用可能只有 1KB。因此为了准确获取 .a 的保体积,最好的办法写一个测试程序,该程序调用了所有接口,这样一来 .a 中所有符号都会被链接到程序中。

具体的,为了排除其他因素的干扰,我们可以这么做:

  1. 编译一个 main_base,可以是一个空的程序,或者并未调用任何 .a 中符号的程序
  2. 接着,编译个 main_target,里头基于 main_base 的代码,调用了所有 .a 中的符号
  3. 最后用 bloaty 查看两者的增量,即可获得符号的大小情况

以我们的 demo 为例,编译 main.cpp 得到 main,接着注释 main.cpp 中测试代码编译得到main_base
可以得到:

$ bloaty main -- main_base
    FILE SIZE        VM SIZE
 --------------  --------------
 +89e4% +69.5Ki +89e4% +69.5Ki    __TEXT,__text
  [NEW] +16.0Ki  [NEW] +16.0Ki    [__DATA_CONST]
  [NEW] +15.8Ki  [NEW] +15.8Ki    [__DATA]
 +97e2% +4.55Ki +97e2% +4.55Ki    String Table
  [NEW] +2.65Ki  [NEW] +2.65Ki    __TEXT,__const
 +52e2% +2.45Ki +52e2% +2.45Ki    Symbol Table
 +38e2% +1.76Ki +38e2% +1.76Ki    Export Info
   +95%    +784  +109%    +784    [Mach-O Headers]
  [NEW]    +328  [NEW]    +328    Lazy Binding Info
  +372%    +268  +372%    +268    __TEXT,__unwind_info
 +26e2%    +208 +26e2%    +208    Function Start Addresses
  [NEW]    +196  [NEW]    +196    __TEXT,__cstring
  [NEW]    +186  [NEW]    +186    __TEXT,__stub_helper
  [NEW]    +164  [NEW]    +164    Indirect Symbol Table
  [NEW]    +152  [NEW]    +152    __DATA,__la_symbol_ptr
  [NEW]    +114  [NEW]    +114    __TEXT,__stubs
  [NEW]    +112  [NEW]    +112    Binding Info
  [NEW]    +108  [NEW]    +108    [5 Others]
  [NEW]     +48  [NEW]     +48    Weak Binding Info
  [NEW]      +4 -60.8% -9.64Ki    [__LINKEDIT]
 -64.2% -9.70Ki -63.7% -9.70Ki    [__TEXT]
  +654%  +105Ki  +300% +96.0Ki    TOTAL

可以看到增加了约 96KB

基于 Xcode 的 .a 测试方法

Xcode 有一个神奇的选项,可以导出一个叫 “Link Map File” 的东东,它是一个记录链接相关信息的纯文本文件,里面记录了可执行文件的路径、CPU架构、目标文件、符号等信息。接着使用 LinkMap 分析工具,就能够获取到具体包大小情况了。我们来具体地操作一遍。

0. 编译 iOS 库

以我们 demo 为例,运行 ios_build.sh 编译 SDK,得到 .h 和 .a,目录结构如下:

how_to_test_package_size
|--ios_build
   |--src
      |--libsimple_decoder.a
|--include
   |-- simple_decoder.h

1. 新建 ios 工程

打开 Xcode -> File -> Project -> iOS -> App,
接着输入 project name,Interface 选择 “Storyboard”,Language 选择 “Objective-C”,

接着,设置 Build Settings 中 “Header Search Paths” 为 simple_decoder.h 所在目录:

设置 Build Settings 中 “Library Search Paths” 为 libsimple_decoder.a 所在目录:

然后,将 libsimple_decoder.a 添加到 target 链接中,直接用鼠标将 .a 拖进去就可以了:

创建后,修改 main.mmain.mm,在 main.mm 中调用 simple_decoder.h 中所有接口:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#include "simple_decoder.h"

int main(int argc, char * argv[]) 
    NSString * appDelegateClassName;
    @autoreleasepool 
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        
        
            const char* path = "abc";
            SimpleWavDecoder* decoder = SD_createFromFile(path);
            SD_getSampleRate(decoder);
            SD_getNumChannels(decoder);
            SD_read(decoder, NULL, 100);
            SD_destroy(decoder);
        
    
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);

最后点击编译,没有编译错误即可

2. 打开 Write Link Map File

在 Build Setting 中,打开 Write Link Map File

打开开关后重新编译,这时候就能够生产 Link Map 文件了,它默认保存的位置比较难找,你可以修改 Path to Link Map File 来选择保存位置。在我电脑中,它的默认位置在:

/Users/bytedance/Library/Developer/Xcode/DerivedData/test_simple_decoder_size-cqumhwnbsjvelubcwgasfhawlbxd/Build/Intermediates.noindex/test_simple_decoder_size.build/Debug-iphoneos/test_simple_decoder_size.build/test_simple_decoder_size-LinkMap-normal-arm64.txt

3. 分析 Link Map File

网上有很多 Link Map File 分析工具了,这里我使用的是 LinkMap,下载代码后打开 xcode 工程编译运行即可。最后选择 link map file 进行分析,得到:

总结

本文介绍如何测量 .so 和 .a 方法。具体的,在测量 .so 时,利用 bloaty 进行分析,bloaty 工具很灵活,可以从 symbols、segments等不同角度给出包体积;而为了测量 iOS 中 .a 包大小我们需要建立一个 iOS demo,并在 demo 上调用所有接口,接着导出 Link Map 进行分析。

以上是关于C/C++ Native 包大小测量的主要内容,如果未能解决你的问题,请参考以下文章

多平台Native库打入JAR包发布实战

如何调整 pub 包“flutter_native_splash”中的图像大小?

使用UDP和TCP测量网络延迟

使用可拖动按钮调整图像大小 - React Native

我的C/C++语言学习进阶之旅NDK开发之Native层使用fopen打开Android设备上的文件

我的C/C++语言学习进阶之旅NDK开发之Native层使用fopen打开Android设备上的文件