1. 程式人生 > >Android NDK 如何縮減庫的大小

Android NDK 如何縮減庫的大小

當我們剛開始做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根本不在一個量級!