关于android:云音乐-Android-so-体积治理实践

63次阅读

共计 5443 个字符,预计需要花费 14 分钟才能阅读完成。

本文作者:dyl

背景

软件应用除了性能外,还有许多非性能品质属性须要咱们关注,常见有性能、安全性、可用性、可扩展性等。除此之外,软件的体积也是咱们应该关注的重要品质属性。体积对启动速度、下载安装时长、装置成功率、磁盘空间占用、OOM 异样等都有深刻影响。

最近负责治理云音乐 Android 端 so 的体积,通过钻研摸索总结了一些办法,次要从三个方面着手治理,别离是

  • 优化代码
  • 优化编译链接
  • 优化依赖。

用这些办法进行了一次大面积 so 治理后,so 整体从 30M+ 升高为 20M+,缩小了 30%+ 的体积。本文对这些治理办法和背景常识进行了介绍,以供大家参考。

优化代码

针对代码,次要关注在去反复代码和禁用低廉的 C++ 语言个性。Andorid NDK 下低廉的语言个性包含

  • 异样
  • RTTI
  • iostream 库

去除反复代码

反复的代码,不仅带来体积问题,更是一种代码坏滋味。移除反复代码无论在品质上,还是减小 so 体积上都有好处。咱们可采纳代码动态检测工具检测反复代码,而后以提炼类或函数的重构手法进行解决。

  • 提炼函数:如果一个类的多个函数有反复代码,提炼独立函数,放入类中供其余函数应用。如果多个兄弟子类有反复代码,提炼独立函数,放入父类之中供子类应用。
  • 提炼类:如果不相干类有反复代码,提炼独立类搁置反复代码,供这些类应用。

禁用低廉的 C++ 语言个性

在 Android NDK 下,有许多 C++ 个性是比拟低廉的,在 Android NDK 官网文档亦有提及,要尽量避免应用。次要包含禁用 C++ 异样、禁用 C++ RTTI、防止应用 iostream。

C++ 异样会有一个误导,认为能够捕捉让人头疼的空指针、内存越界等意料之外的谬误,其实并不能。异样机制实际上是一种错误处理框架,捕捉事后定义的谬误,其目标是将失常逻辑和异样逻辑的解决离开,进步代码整洁度。而咱们每定义一处异样,在编译链接后都会插入 C++ 库代码进行扩大,占用比编写的代码更多的空间。因为其性能和体积等问题,在实践中可思考改用返回错误码来代替。

C++ RTTI 机制,在语意层面和多态是矛盾的。C++ 的多态,是通过基类指针指向派生类对象,在 Compile Time 时毋庸晓得理论类型,在 Run Time 时方依据指向的类型,执行对应的虚函数实现,从而让咱们得以从依赖实现改为依赖接口。而 C++ RTTI,则是在 Compie Time 期间得悉基类指针指向的理论类型,也即让咱们从依赖接口改为了依赖实现。此外,编译器实现 RTTI 机制往往会减少 class 的大小,比方为每个 class 产生额定的 RTTI 数据,蕴含类名和基类信息。当咱们应用到 RTTI 时应该认真考量,是否设计上呈现了问题。如果非凡状况须要应用,也要分明背地的体积老本和设计老本。

对于 iostream 库,通过咱们在理论场景中的查看,发现大部分仅仅是应用了 std::cout 输入日志。Android 自身提供有 log 办法,用 <android/log.h> 中的 log 进行日志输入,可移除 iostream 的依赖,从而缩小体积。

优化代码实际办法

禁用上述语言个性,在 NDK Build 下无需特地指定,其默认禁用 C++ 异样和 RTTI。在 CMake 下禁用异样和 RTTI 的编译选项如下:

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions -fno-rtti")

针对 iostream 库的引入,可搜寻整体代码,或用 objdump 反汇编 so 库,确定是否含有 iostream 信息,而后定位批改代码。

objdump -D demo.so | grep iostream

优化编译链接

在编译链接方面,重点是治理 so 的导出符号,减少相应的编译链接优化选项,提供足够的信息给编译器,由它在编译链接期间进行体积优化。
要理解 so 的导出符号,须要理解 ELF 文件格式。ELF 文件有多个分段(section),能够通过 readelf 命令查看细节。咱们关注的分段为:

  • .text:寄存编译后的机器代码
  • .date:寄存已初始化的全局 / 动态变量
  • .dynsym:动静符号表,蕴含导出符号和导入符号
  • .dynstr:动静符号字符串表
  • .symtab:整体符号表
  • .debug:调试信息表

其中动静符号表(.dynsym)是咱们关注的重点,它记录了动静库的导入导出符号,咱们须要确保导出的是必要且残缺的符号汇合,去除不必要的导出符号。

对于代码段(.text)和数据段(.data),在默认编译选项下,产出的指标文件会将多个函数会集到一个代码段,多个变量放到一个数据段,最初合入到 so 中。咱们须要通过编译链接选项,帮忙编译器只合入用到的函数和变量。

整体符号表(.symtab)和调试信息(.debug),则蕴含了丰盛残缺的符号信息,在剖析 Crash 堆栈时可还原符号。咱们保留一份带有 .symtab 和 .debug 的 so,并在公布时执行 strip 移除这些符号调试信息。就即能够公布小体积的 so,也能够在呈现 Crash 时用大体积 so 还原堆栈符号。

限定动静符号表

ELF 中的动静符号表(.dynsym),记录了动静库的导入导出符号。在 Linux/Android NDK 下,编译器默认将函数和全局变量,及引入应用的动态库的函数和全局变量,作为本人的动静符号全副导出,使用者在应用时也无需任何非凡操作。尽管不便,但也容易导致 so 蕴含许多不应该导出的函数符号,甚至将外部应用的其余动态库的函数也进行导出。咱们须要对导出的动静符号做出限度,确保只裸露内部依赖的符号,能够无效缩减动静符号表以及相干表项。对于第三方或无奈明确导出符号的 so,则强制不导出其它的动态库符号。咱们也强制要求不导出 C++ 库的符号。见下图示意:

<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/27897322138/2fea/4f36/67bc/5e63a0ef53dcec3fe8758535352fe0ab.jpg” width=”70%” height=”70%” />

移除未应用函数和变量

默认编译选项下的指标文件在编译后,会将多个函数会集到一个代码段,多个变量放到一个数据段。以代码段来说,其含有多个函数,哪怕咱们只用到其中一个函数,这个代码段就要整个保留,在链接阶段会整体合入 so,从而合入了并未应用的函数,增大了体积。数据段也是如此。咱们能够通过选项告知编译器用更细粒度分段,让一个函数占一个代码段,一个变量占一个数据段,并告知编译器回收未应用的代码段和数据段,从而移除并未应用的函数和变量。见下图示意:

<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/27897322653/f6c7/64c1/f2d6/5b0f011a16b66f9195105fd4607bbc78.jpg” width=”70%” height=”70%” />

精简 JNI 原生接口符号

在 Android NDK 下的 JNI 原生接口注册形式有两种,别离是动态注册和动静注册。动态注册是以“Java+ 包名 + 类名 + 办法名”定义 native 办法,由 runtime 本人扫描注册。动静注册则是在 cpp 文件中定义 JNI_OnLoad 办法,咱们在此办法中调用 RegsiterNative 注册 JNI 接口。采纳动静注册,对于反对 JNI 的 so 只须要导出 JNI_OnLoad JNI_OnUnload Java_* 可无效升高体积(规模更小、速度更快的共享库)。 RegsiterNatives 动静注册办法,可参考 Google 官网动静注册代码。

优化编译链接实际办法

交融上述的限度动静符号表,细化代码段和数据段,并回收未应用分段,能够让编译器移除没有被“导出函数”间接或间接依赖的函数和变量,从而大幅缩小 so 的体积。

限度导出符号办法

可采纳 version script 的办法,这也是 NDK 官网示例的办法。具体来说咱们编写一个相似 json 的文件,指明要导出的函数,并在链接选项中退出此脚本文件即可。version script 文件示例如下(留神导出类须要 extern “C++”,防止名称润饰问题):

{
    global: gValue;
            *someFuncs*;
            extern "C++" {
                CSemaphore::*;
                CCritical::*;
            };
    local: *;
};

CMake 编译链接选项如下:

# 以 version script 指定导出函数
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,versionscript=${CMAKE_CURRENT_SOURCE_DIR}/funcs.map")            

# 不导出所有引入的动态库的符号
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--exclude-libs,ALL")

NDK build 编译链接选项如下:

# 以 version script 指定导出函数
LOCAL_LDFLAGS += -Wl,--version-script=${LOCAL_PATH}/funcs.map 

# 不导出所有引入的动态库的符号
LOCAL_LDFLAGS += -Wl,--exclude-libs,ALL

帮助编译器移除未应用函数和变量

咱们通过减少编译选项,可让编译器回收未应用的代码段和数据段,如下:

  1. 指定分段选项,此举会让编译器在编译指标文件或动态库时,将单函数和单变量放入单个独立的段。

    • -ffunction-sections
    • -fdata-sections
  2. 指定回收选项,此举会让编译器在链接阶段执行 DeadCode 检测,辨认出未应用的函数和变量,进而移除未应用的段。

    • –gc-sections
# CMake 编译选项
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -flto -fdata-sections -ffunction-sections")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto -fdata-sections -ffunction-sections")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -O3 -flto -Wl,--gc-sections")
# NDK Build 编译选项:LOCAL_CFLAGS += -Oz -flto -fdata-sections -ffunction-sections
LOCAL_LDFLAGS += -O3 -flto -Wl,--gc-sections

优化依赖

当一个动态库被多个 so 反复依赖时,会引入多份动态库代码,能够提取这个反复依赖库为独立 so,供其余 so 库共用。当一个 so 库仅仅被另一个 so 库依赖时,会产生相干导入 / 导出符号表项,可通过合并两个 so 去除导入导出符号。这两个办法是依赖的治理思路。

在实践中,咱们重点推动了 libc++ 的依赖治理。一个利用不应应用多个 c++ 运行时,在 Android 下 libc++ 版本与 NDK 版本是绝对应的,对立 NDK 版本及 libc++ 依赖形式十分重要,波及的不仅仅是体积问题,还可能导致 App Crash 或者其余奇怪问题。通过检测咱们发现大部分自研 so 库都是采纳动态依赖 libc++_static 且版本不一,所以重点推动了 NDK 版本的对立,并对立动静依赖 libc++_shared。对于因为历史起因无奈降级 NDK 版本的则放弃动态链接 libc++_static,但要确保不导出其符号。

对立 libc++ 的依赖形式

  • 确定对立的 NDK 版本以及相应的 libc++_shared.so,在 module 级束缚 ndkVersion 为对立版本。
  • 公布根底 aar 包,内含 libc++_shared.so。
  • 在功能性 so 工程中,动静链接 libc++_shared.so。

    # Module 级的 build.gradle,此举会主动将 libc++_shared.so 打入 aar 包
    DANDROID_STL=c++_shared
    
    # ndk-build 下,在 Application.mk 中退出
    APP_STL := c++_shared
  • 公布功能性 so aar 包时,排除本身的 c++_shared.so,以防止抵触

    packagingOptions {exclude '**/libc++_shared.so'}
  • 如果不能对齐对立版本的 ndk,则采纳动态链接 C++ 库的形式。因为 so 默认会将本人引入的动态库作为本人的导出符号全副导出,所以须要排除 C++ 库的符号。

    # 不导出 C++ 库的符号
    LOCAL_LDFLAGS += -Wl,--exclude-libs,libc++_static.a -Wl,--exclude-libs,libc++abi.a

总结

通过上述办法,可能无效的治理和管制 so 的体积。除此之外,还须要一直开掘反复依赖的性能;同时为了避免劣化,须要在 CI/CD 机制中加上相干符号和编译选项的检测。这也须要咱们继续进行关注和欠缺。

参考资料

  • Android NDK 文档 针对中间件供应商的倡议

本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!

正文完
 0