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 += -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根本不在一個量級!