Android NDK 如何缩减库的大小
Android NDK: how to reduce library size (translate from:link)
当我们刚开始做Algolia的android开发时,二进制文件的大小并不是我主要关注的。事实上我们一开始用的是java,后来出于性能的压迫下才换成了C/C++
后来要在AVelov(一个android应用)中集成我们的库时,才发现这货太大了:850KB,而AVelov整个app才638k!这就意味着AVelov要翻倍的趋势啊
后来我们将Algolia从850KB减小到了307KB,下面就来分享一下我们所做的东西吧
不用exceptions和RTTI
实际上在我们的底层库中我们没有使用异常,鉴于论述完整性,我也把exceptions一起说了
C++ exceptions 和RTTI默认是关闭的,可以通过设置在Application中设置APP_CPPFLAGS可以打开它,赠送一个使用共享的STL:
APP_CPPFLAGS += -fexceptions -frtti
APP_STL := stlport_shared
同时使用exceptions和RTTI可能,会显著增加你的binary size,这货能删就删吧,不要手软。还有另外一个避免使用C++exceptions的原因:C++对异常的支持不够好。例如:在jni层根本不可能catch一个C++exceptions然后再启动一个java异常。下面的code将会crash(将来的NDK toolchain可能会fix,说这话是2013/1/10):
try {
...
}catch () {
env->ThrowNew(env->FindClass("java/lang/Exception"), "Error occured");
}
不用iostream
当我们接到Cyril的反馈,开始想办法减小library size时,我们发现最后一次提交后我们的库直接从850KB涨到了1.35M(此处有大汗!)。首先我们就怀疑是NDK toolchain更新导致的,随之用两个版本就测了一把,发现变化微乎其微。
当我们用二分法回溯commit历史时,逮住了罪魁祸首:
std::cerr << .... << std::endl;
只是这一行就引进了C++的iostream,经我们测试发现如果用了iostream至少会增加300KB。所以用__android_log_print
替换吧:
#include <android/log.h>
#define APPNAME "MyApp"
__android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "The value of 1 + 1 is %d", 1+1);
tips: 需要在Android.mk中加上:LOCAL_LDLIBS := -llog
使用 -fvisibility=hidden
一个非常有效的减小library是使用gcc的visibility feature。这个特性可以让你控制导出在符号表中的函数。jni自带了一个JNIEXPORT的可以标志公开函数的宏。所以需要确认所有的需要导出的函数都有JNIEXPORT:
JNIEXPORT void JNICALL Java_ClassName_MethodName(JNIEnv *env, jobject obj, jstring javaString)
也可以自定义JNIEXPORT:
#define JNIEXPORT __attribute__ ((visibility ("default")))
然后需要在Android.mk中加入:
LOCAL_CPPFLAGS += -fvisibility=hidden
LOCAL_CFLAGS += -fvisibility=hidden
用了这一招之后library已经减小到809KB(-5%),可能根据project的不同略有差别。
用gc-section抛弃不用的函数
这招可以彻底的缩减library的size,直接砍掉了所有没有用途的函数。怎么使用呢,在Android.mk修改C和C++编译选项,请看:
LOCAL_CPPFLAGS += -ffunction-sections -fdata-sections
LOCAL_CFLAGS += -ffunction-sections -fdata-sections
LOCAL_LDFLAGS += -Wl,--gc-sections
这个优化选项只减少了1%(此处又有汗Σ( ° △ °|||)︴)
but如果以上选项贴加在一起,这下就超级牛X了:
LOCAL_CPPFLAGS += -ffunction-sections -fdata-sections -fvisibility=hidden
LOCAL_CFLAGS += -ffunction-sections -fdata-sections -fvisibility=hidden
LOCAL_LDFLAGS += -Wl,--gc-sections
直接减少到了691KB(-18.7%)
删除多余的代码
用-icf=safe链接选项就可以。但是要注意这招有副作用:有可能移除了内联函数,从而影响程序的性能。
在mips架构上没有这个指令,需要在Android.mk上加判断:
ifneq ($(TARGET_ARCH), mips)
LOCAL_LDFLAGS += -Wl,--gc-sections
else
LOCAL_LDFLAGS += -Wl,--gc-sections,--icf=safe
endif
这一步减少了0.8%,所有目前操作共减小到了687KB(-19.2%)
改变toolchain的默认选项
如果你想缩减的更彻底,那就需要修改默认编译选项了。这些编译选项因架构而异,例如:
在arm架构上inline-limit设置到64,x86/mips就是300;arm优化flags设置到-Os(size最优),x86/mips上则要设置为-O2(性能最优)。
由于arm事多力广,我们直接使用了arm配置。下面就是在android toolchain(version r8d)我们使用配置选项:
--- android-ndk-r8d/toolchains/mipsel-linux-android-4.6/setup.mk
+++ android-ndk-r8d.new/toolchains/mipsel-linux-android-4.6/setup.mk
@@ -41,12 +41,12 @@
TARGET_C_INCLUDES :=
$(SYSROOT)/usr/include
-TARGET_mips_release_CFLAGS := -O2
+TARGET_mips_release_CFLAGS := -Os
-g
-DNDEBUG
-fomit-frame-pointer
-funswitch-loops
- -finline-limit=300
+ -finline-limit=64
TARGET_mips_debug_CFLAGS := -O0
-g
--- android-ndk-r8d/toolchains/x86-4.6/setup.mk
+++ android-ndk-r8d.new/toolchains/x86-4.6/setup.mk
@@ -39,13 +39,13 @@
TARGET_CFLAGS += -fstack-protector
-TARGET_x86_release_CFLAGS := -O2
+TARGET_x86_release_CFLAGS := -Os
-g
-DNDEBUG
-fomit-frame-pointer
-fstrict-aliasing
-funswitch-loops
- -finline-limit=300
+ -finline-limit=64
# When building for debug, compile everything as x86.
TARGET_x86_debug_CFLAGS := $(TARGET_x86_release_CFLAGS)
这次使用这些新的flags又缩减了8.5%,和之前的累加在一起减少到了613KB(-27.9%)
限制架构的数量
我们的最终建议是减少支持的架构。如果你有大量的浮点计算出于性能考虑就需要支持armeabi-v7a,但是如果你不需要一个FPU,armeabi也会给出一个近似的结果。然而对于mips处理器...现在市场上还没有用武之地呢~
如果binary size真的对你很重要,你可以只支持armeabi和x86架构(Application.mk):
APP_ABI := armeabi x86
看到了没?砍掉了两个架构,我们的library一下变成了307KB,获得了64%的缩减,这还没有算上iostream增大的1.35M,O(∩_∩)O哈哈~
结论
希望这篇短文能够帮助你缩减android native libraries,毕竟android ndk的默认选项优化的太差了。但是也不要期望和我一样的减幅,这些操作都是因机而异,因code而异的。如果你有其他的缩减binary方法,在评论中分享出来吧!
附注:
使用clang编译native code,使用O3或者Oz选项基本上能一步到位,不能不说clang太牛X了,和gcc根本不在一个量级!