胡琪

为今天工作,为明天投资,为未来孵化一些东西!

【NDK开发之】SDK体积优化最佳实践

先交代下背景,我们的SDK是一个包含so的aar,主要包括java代码以及so 2大部分,基本不含资源文件。为了安全我们将SDK的核心逻辑放到so中用c/c++代码实现。但是由于我们是SDK,在给我们客户接入的过程中发现某些客户会非常在意体积。尽管实际上我们的体积相对竞品而言已经小了约1M了,但是某些客户仍然要求(期望但不强制)接入我们的SDK后体积增加不能超过500kb同时包含5种主流abi。所以为了给客户更好的用户体验,需要考虑优化SDK的体积,经过一番调研和实践之后减小的体积还是比较客观的。

SDK体积优化的几个方向与不同优化时期的选择策略

首先需要明确的是SDK体积优化的几个方向,然后判断从哪个方面优化能最大程度的减小体积。然后进行测试验证。

  • 代码优化:体积优化首先要考虑的就是代码层面,这也是体积过大的主要原因。所以这往往是我们初次考虑体积优化时的主要方向。往往也是初次优化时效果最好的。
  • 编译优化:其次就是编译选项,主要是对c/c++代码而言,不同的编译选项对打出的so体积是存在影响的这种优化相对代码优化而言体积减小往往是比较微小的,所以这方面的优化往往是初次从代码层面优化完成后已无代码优化空间后考虑的方向。但是因为so往往包括各种不同abi类型的,而作为SDK提供方,客户一般往往会要求包含主流的5种abi类型。所以积少成多,可减少的体积还是比较可观的
  • 混淆优化:主要是针对java层代码优化,SDK中的非核心逻辑往往是java代码写的,此时可以通过混淆优化来减小体积。

下面就从这3个方面进行详细讲解。

代码层面优化

主要包括去除冗余代码,不要使用一些体积过大的三方库。使用c语言函数库代替使用c++标准库。

去除冗余代码

不管怎样在我们的代码中往往都隐藏着冗余代码,而去除这些冗余代码对程序的逻辑是没任何影响的,同时也能减小一部分体积,因此首当其冲就应该考虑去除冗余代码。

  • 如果代码中多个地方包含相同代码,将这些代码抽取到一个函数中,然后在不同的地方调用该函数
  • 将代码中用不到的冗余方法去除掉:我见过很多人在创建一个新项目时将原先的旧项目中的工具类直接拷贝到新项目中使用。而事实上虽然新项目可以用到旧项目中的某些工具类方法,但是新项目和旧项目往往是有很大不同的,这也就造成了工具类中的很多方法是冗余的。同理其他的一些类中也可能会存在一些冗余代码。如果是java代码可以通过配置混淆优化在打包时将这些没用到的冗余方法去除掉。但是c++代码在编译时可能没这么智能(当然也可能可以不过我们不知道),因此去除这些冗余代码,积少成多也能减小一部分体积。

对三方库进行筛选

很多时候我们使用某个三方库往往只是使用其中一个很小一部分的功能,而三方库一方面本身会提供超出我们需求很多的功能(想想你使用的App是不是绝大部分都这样?),另一方面三方库为了开发者接入使用更方便,即使对同一个功能往往也会用多种重载函数形式提供对外接口。这也会增加冗余代码。因此我们可以从这2方面入手减小因三方库而导致增加的体积。

  • 选择体积尽可能小的三方库:注意这里说的体积小不仅仅只是指三方库的代码量少或者体积小,还需要注意该三方库是否引入了一些会造成编译后体积大大增加的其他机制。因为即使三方库的体积比较小,但是三方库可能引用了一些其他机制会造编译后成体积大大增加。以我自己开发的易盾环境安全SDK为例,因为SDK中使用到了json库,最开始选用的是知名度比较高的jsoncpp。起初没意识到这个库会导致体积大大增加,后来在进行编译层面的优化时选择了关闭fexceptions参数,即关闭c++异常选项。没想到关闭之后编译出错了,后来发现是因为jsoncpp的代码实现中用到了c++的异常处理相关的库,所以后来就使用json11这个库代替jsoncpp库了,替换之后体积大大减小,体积减小了几百kb,约减小了一半。而很显然单是jsoncpp的那些代码不可能增加这么大的体积,这些增加的体积绝大部分都是引入c++异常机制导致的。
  • 剔除三方库中冗余的代码:如果三方库本身是开源的,且其开源协议允许我们修改其代码,那么我们可以考虑将三方库中某些用不到的功能给剔除掉。也就是从三方库中提取出我们需要的那部分代码做成做小依赖。

用c语言的库函数代替c++ stl

虽然c++的stl库函数使用起来很方便,但是因为c++是面向对象的语言,很多时候仅仅只是为了使用c++标准库的某一个函数的功能而需要include某个c++文件则会大大增加体积。而c语言是非面向对象的语言,其代码组织关系是通过函数组织的。因此引入c语言的头文件然后配合某些编译选项就能够达到只将用到的函数链接到最终的so中而没用到的函数不链接进去,从而达到相比c++而言打出的so体积更小的效果。这一点在我对易盾环境安全SDK的so优化中体现非常明显。在我自己开发的环境安全SDK中使用到了文件读写操作。开始使用的是c++标注库的流操作,也就是include<iostream>文件。然后换成c语言库的fopen等文件操作函数之后体积大大减小了。

编译层面优化

编译层面的优化是对体积进行二次优化的首选,因为初次优化之后代码层面的优化基本上已无可优化空间。而对于NDK而言,编译工具包括gcc和clang。所以这里分2种情况讨论下不同编译选项对so体积的影响。

gcc编译器编译优化

对于x86架构使用-Os选项

gcc在编译的时候可以指定很多编译选项,可以参看这里:https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html,而某些选项使用比较频繁,因此gcc将这些常用的编译选项进行组合归纳为了几个编译优化等级,在命令行编译时通过-o参数指定,几个等级如下所示:

  • -o0:这是默认的优化等级。作用是减少编译时间,同时保留调试信息。因此体积会相对较大
  • -o1/-o2/-o3:1-3每一个高等级的优化相对前一个等级而言都保留了前一个等级的所有优化参数,同时新增加了一些自身等级的优化参数。具体可以参看前面提到的gcc编译选项官方介绍文档。注意优化强度随着等级增加而增强
  • -Os:Optimize for size的意思,专为优化体积而设置的选项,注意前面几个等级的优化-ox中的o是小写,而此处的O是大写。-Os优化参数是在-o2等级的基础上关闭了一些选项,使编译器根据代码大小而不是执行速度进行调优,并执行旨在减少代码大小的进一步优化。因此这是我们进行体积优化的关键编译选项。注意该选项会开启-finline-functions参数。

那么编译选项为何能够影响打出的so体积呢?这是因为对于不同的abi类型默认的编译选项是不同的,arm默认的就是Os,而x86默认是-O2。而通过前面的介绍我们知道很显然-Os选项编译的体积会较小。

关闭C++异常机制选项,如果引用的三方库依赖异常机制则考虑替换该三方库

前面在将代码层面优化时提到过c++的异常机制会导致编译的so体积大大增加。因此推荐关闭该编译选项。默认情况下如果代码中未使用异常机制,在编译so的时候不会链接c++异常机制相关的功能。但是为了防止项目中依赖的三方库引用了c++异常机制而我们毫不知情,所以推荐手动关闭该编译选项,这样一旦三方库中使用了c++中相关的异常捕捉功能则会导致编译失败。这样我们就可以考虑使用具备相同功能的三方库代替原三方库。

clang编译器编译优化

混淆与压缩优化

混淆优化主要是指SDK中的java代码部分,如果SDK中本身也包含了大量的java代码,那么开启proguard混淆也能减小一部分体积。

压缩优化是指用一些压缩工具对so进行压缩,但是因为在安卓端包含so的SDK往往是aar格式的,而aar本身就是一个zip压缩包,因此即使未对so进行压缩,当将so打入到aar中之后本身也会被压缩。所以so的压缩优化意义不是很大。

打赏

点赞