Android ndk之so体积缩减

96
JZaratustra
0.1 2017.09.04 14:54* 字数 2536

零.背景

SDK对体积比较敏感,而且除了接口代码其他全部用Native实现,armeabi下的so大概有800k,目标是缩减40%左右。因此有了这次对SO体积进行缩减的过程。体积的优化主要有四个层面,分别是代码,编译参数,项目结构和优化工具,下面对这四个方面进行说明。

一. 代码层面

代码层面无非是替换部分臃肿的第三方库,精简自己的实现代码,尽量减少stl库的使用,甚至纯粹用c实现。
就我的这个项目而言,代码可优化的地方有三处:

  • json库

之前使用的是JsonCpp这个开源库,功能非常全面,包括简单的从字符串解析,构建成json字符串,对象转json,json转对象,但是有三个2000+代码的实现类给我们的so体积增加了100k左右。因为项目并对json有这么多的依赖,所以这是一个很大的优化点。这里推荐两个比较好用的开源库。

一个是C++实现的Json 11,他的代码量只有700行左右,并且只有两个文件,在其中实现了字符串解析,构建json字符串,对象转json,json转对象四个比较基本的功能,完全能满足项目需求。

另外一个是纯C实现的JSMN,只有200行左右,但是由于接口太过简单,用起来太多麻烦因此就放弃了,下面会提到,如果想摆脱stl库的依赖,这个库是一个比较好的选择。

综合考虑到易用性和体积,经过替换Json 11库之后so大小大概减少了70k(10%左右)。

  • protobuf库

由于项目使用了pb协议和外部通信,因此使用了谷歌的protobuf解析库,也附带了有许多的protobuf协议C文件,包括protobuf解析库和protobuf协议文件编译后占用了大概200k左右的空间,

虽然谷歌只提供了C++的方案,但是还是有人实现了C的方案protobuf-c虽然使用起来没有C++的方案顺手,但是替换后能省下150k(20%)左右空间。

  • stl库依赖

终极方案就是不依赖stl库,使用纯C实现,stl库使用静态链接的方式打入到so中大概会增加300k左右,具体取决于使用了多少stl库。将代码中的map,vector,thread等stl库依赖替换成C实现,或者完全不用。当然这样对整个代码修改就比较多了,而且因为很多库不能用,等于是丧失了C++的很多优势,对以后的迭代效率会有比较多的影响。因此不得不放弃这个方案,如果以后实在有刁钻的客户需要200k以下的so体积,再做此打算吧。

这其实也是个经验,如果对动态链接库的大小特别敏感,比如代码是跑在一些iot设备上的,就要从早期避免或者少使用stl库,甚至使用纯C实现。

这三个点只是针对我的项目的三个优化点,其他业务逻辑代码的优化也没有总结出什么特别的思路,只能特殊情况特殊分析,各位可以根据自己的项目实际情况选择优化项目代码。

二. 编译层面

编译层面有比较多的点,主要是Android.mk和Application.mk两个文件的参数配置,下面给出两个文件的官方参考文档,下面说的参数都可以在官方文档中查阅到。

Application.mk官方说明

Android.mk官方说明

  • rtti参数

LOCAL_CPP_FEATURES := rtti或者LOCAL_CPPFLAGS := -frtti
RTTI(即运行时类型信息),该依赖会增加40k(5%)左右的体积,能不用尽量不用。

  • exceptions参数

LOCAL_CPP_FEATURES := exceptions或者LOCAL_CPPFLAGS := -fexceptions即C++异常,由于C++对异常的支持不够好,因此能不用其实可以不用(该依赖也会增加60k(7%)左右的体积)。fexception和rtti的依赖在ndk编译中默认是关闭的,如果代码中没有使用到这两个c++特性,就忽略上面两个参数。

  • gc-sections参数
LOCAL_CPPFLAGS += -ffunction-sections -fdata-sections
LOCAL_CFLAGS += -ffunction-sections -fdata-sections
LOCAL_LDFLAGS += -Wl,--gc-sections

这个参数可以说是意外之喜了,居然能缩减大约200k(20%)左右的提示,因此简单研究了一下这个参数设置的原理。

GCC链接操作是以section作为最小的处理单元的,只要一个section中有某个符号被引用,该section就会被加入。如果没有加入-ffunctipn-sections-fdata-sections所有的function和data都会放到同一个section中生成.o文件。使用了两个参数后,编译生成的.o文件将会拆分成许多个section,每个section只包含一个function,并且该section的名字为function的名字。因为插入了很多个section声明,所以自然加入两个参数后,生成的.o文件都会比较大,但是在后面的链接阶段,这样的拆分就会很有很大的作用。

在链接阶段,由于上述的拆分操作,链接过程中可以很方便的标记出哪些function被使用到,哪些function没有被使用到,这样就可以将没有使用到的function剔除出最后生成的so库中,由于生成的so不会将section的拆分保留,所以上面因为section产生的体积增加就会消失,并且无用的function会被删除,这样整个so的体积就会大大减小了。其实整个过程有点类似于java proguard的optimize过程,但是Java会有反射,动态加载等过程,所以optimize用得会比较少。C++貌似没有类似机制(由于不是很精通C/C++所以不太肯定),否则也会有坑。

  • icf=safe删除多余代码
LOCAL_LDFLAGS += --icf=safe

加入--icf=safe链接参数可以删除多余的代码,但是有一定风险,可能删除掉内联声明,造成性能损失。

  • Os参数

关于这个参数详细的说明可以参考官方说明

Options That Control Optimization

这里做一下简单的说明O<numbers>是在执行效率上进行优化,从13开启不同的优化等级,这样会增加编译时间和增加编译后的库的大小,具体可以参考上面给出网站查看13的优化等级分别开启了什么优化方式。
O0则是调试模式,最大限度的减少编译时间和让调试产生预期的结果。
Os的意思就是针对size进行优化,这个选项可以最大限度的减少生成代码的体积,也就是最后能让动态库的体积最小。

arm默认的就是Os,而x86默认是-O2,这里可以通过设置

TARGET_x86_release_CFLAGS := -Os     

这样可以有效的减小so体积,但是效率可能会慢一点。

三. 项目结构层面

如果项目拥有两个动态库,可以采用共享stl库的方式进行编译,主要是修改Applocation.mk文件参数改为xxx_shared,这样就会在编译后产生自己的动态库以及libxx_shared.sostl动态库,这个就是引用的动态链接库,如果需要编译两个动态库,那只需要引用一个stl动态库即可,但是要注意System.load时必须要优先加载stl动态库。

其次就是根据不同的厂商提供不同的so了,一般来说armeabi-v7a针对多浮点运算会有比较大的优化,如果native代码涉及到不多的浮点数据运算,可以直接去掉armeabi-v7a的支持,只用armeabi。在接下来就是x86的动态库,其实现在就算在模拟器下也会兼容armeabi的运行,因此x86并不是必要提供的。所以其实一般的应用而言,armeabi的so足够支持所有的机器运行,而更少的so能大大的减少应用的体积。

四. 优化工具 - UPX

最后提供比较另类的思路,使用UPX

UPX-the Ultimate Packer for eXecutables

UPX是一款的可执行程序文件压缩器,压缩过的可执行文件体积缩小50%-70%。以前PC时代经常会作为一款加壳器,当然是做过修改的。我们这有专人负责研究这块,我使用后发现能将体积减少20~40%取决于设置多少的压缩比。可以说效果非常的好,但是直接用官网提供的工具在Android上会有一些兼容问题,但是都能解决。这块不方便透露更多,有兴趣的同学自己去踩坑填坑吧。

五. 总结

最后通过上述的各种方式,so体积减少到只有300k左右,打包进apk里面大概只增加了150k左右的体积(因为打包成apk后会在进行一层zip压缩),目的达到了。因为对运行效率不太敏感,因此也没有去测试减少体积后带来的多少的性能损耗,后续有需求在进行这方面的测试吧。

Android开发
Web note ad 1