Android NDK 減少 so 庫體積方法總結
1. 背景
基于亞馬遜 AVS Device SDK 改造的全鏈路語音 SDK 最終編譯的動態(tài)庫有幾十個,單架構動態(tài)庫大小有幾十兆,之前在Iot設備中勉強跑著,但是這個體積對于手機應用來說是致命的,各個模塊費事費力能優(yōu)化個幾K的體積就不錯了,我這直接給上個幾十兆的,APP平臺方肯定無法接受。
但是一是有業(yè)務需求,二是自己又想把SDK推到手機APP,提高用戶量,驗證SDK的穩(wěn)定性和交互體驗,所以開始了漫長的瘦身過程,最后單架構壓縮到了五兆一下,雖然還是有點大,但是比起之前有了很大的提升。
2. 刪除無用模塊
AVS Device SDK是主要應用在音響的控制臺程序,而且代碼是跨平臺的,所以一是有很多為了跨平臺做的冗余,二是有很多我們根本用不到的模塊。
比如為了做本地存儲引入了一個Sqlite的動態(tài)庫,我們本身也用不到本地存儲,像鬧鐘設置之類的放到APP層即可,而且就算是需要存儲也完全可以使用Android和iOS平臺提供的Sqlite。刪除用不到的模塊是包體積優(yōu)化空間最大最快的。
3. 第三方庫替換為Android/iOS平臺提供能力
AVS Device SDK在Android平臺基于ffmpeg做解碼實現(xiàn)了音頻播放器,對于我們的場景主要使用用播放器來播放TTS,而TTS是和服務協(xié)商好固定的mp3格式,完全沒有必要為了一個mp3解碼引入一個龐大的ffmpeg庫。
這里我們使用Android平臺提供的Jni層的媒體庫來做音頻解碼。而且即使是Android平臺JNI層不支持,也可以單獨依賴一個mp3解碼庫,而不是龐大的ffmpeg。對于整個包體積來說,第三方模塊往往相對來說是比較大的。
4. 使用strip
使用NDK toolchain可以把調試的C++ 符號表(Symbol Table)中數(shù)據(jù)刪除,我們一般我們打成APK會自動幫我們做這個工作,當然也可以手動設置:
手動的在鏈接選項中加入 strip參數(shù),配置如下所示:
SET_TARGET_PROPERTIES(yoga PROPERTIES LINK_FLAGS "-Wl,-s")
也可以手動執(zhí)行ndk提供的aarch64-linux-android-strip命令移除動態(tài)庫中的調試信息,這種方式除了前面方法外優(yōu)化體積最高的方式,比如libLibSampleApp.so從48M直接優(yōu)化到了992k。
4. 設置編譯器的優(yōu)化flag
編譯器有個優(yōu)化flag可以設置,分別是-Os(體積最小),-O3(性能最優(yōu))等。這里將編譯器的優(yōu)化flag設置為-Os,以便減少體積。
CMake:
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Os")
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}")
Android.mk
LOCAL_CPPFLAGS += -Os
LOCAL_CFLAGS += -Os
除了直接刪除占用體積較大的模塊外,編譯器優(yōu)化是排下來優(yōu)化空間最大的方法。設置完-Os后占用提交較大的前幾個庫體積對比:
| 庫名 | 優(yōu)化前體積 | 優(yōu)化后體積 |
|---|---|---|
| libLibSampleApp.so | 48M | 33M |
| libAVSCommon.so | 28M | 22M |
| libDefaultClient.so | 14M | 9.9M |
5. 使用 gc-sections去除沒有用到的函數(shù)
有些時候代碼量比較大的時候我們沒辦法手動發(fā)現(xiàn)無用的函數(shù),這個時候可以可以開啟編譯器的gc-sections選項,讓編譯器自動的幫你做到這一點。
編譯器可以配置自動去除未使用的函數(shù)和變量,以下是配置方式:
CMake:
# 去除未使用函數(shù)與變量
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ffunction-sections -fdata-sections")
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}")
# 設置去除未使用代碼的鏈接flag
SET_TARGET_PROPERTIES(yoga PROPERTIES LINK_FLAGS "-Wl,--gc-sections")
Android.mk:
OCAL_CPPFLAGS += -ffunction-sections -fdata-sections
LOCAL_CFLAGS += -ffunction-sections -fdata-sections
LOCAL_LDFLAGS += -Wl,--gc-sections
6. 設置編譯器的 Visibility Feature
Visibility Feature就是用來控制在哪些函數(shù)可以在符號表中被輸入,由于C++并不是完全面向對象的,非類的方法并沒有public這種修飾符,因此,要用Visibility Feature來控制哪些函數(shù)可以被外部調用。而JNI提供了一個宏-JNIEXPORT來控制這點。所以只要對函數(shù)加上這個宏,像這樣:
// JNIEXPORT就是控制可見的宏
// JNICALL在NDK這里沒有什么意義,只是個標識宏
JNIEXPORT void JNICALL Java_ClassName_MethodName(JNIEnv *env, jobject obj, jstring javaString)
然后在編譯器的FLAGS選項開啟 -fvisibility = hidden 就可以。這樣,不僅可以控制函數(shù)的可見性,并且可以減少包體的大小。
7. 去除C++代碼中的iostream等直接IO相關代碼
使用STL中的iostream相關庫會明顯的增加包的體積,而Android本身是有預編譯庫(android/log.h)可以代替輸入到控制臺的工具的。在我們的SDK中由于之前是控制臺程序所以用到了輸入輸出,編譯的時候沒有把這塊排除出去,造成了一定的體積冗余。
8. STL的使用方式
對于C++的library,引用方式有2種:
靜態(tài)方式(static)
動態(tài)方式(shared)
其中,靜態(tài)方式在編譯時會將用到的相關代碼直接復制到目的文件中;而動態(tài)方式則會將相關的代碼打成so文件,以便多次引用。由于編譯器在編譯時并不能知道所有被引用的地方,所以同時會打入了很多不相關的代碼。
所以,如果項目中引用library的函數(shù)較多時,用動態(tài)方式可以避免多次拷貝,節(jié)省空間。相反,則直接使用靜態(tài)方式會更節(jié)省空間。由于我們SDK的模塊特別多,再加上整體APK里面已經(jīng)有其他業(yè)務引入了動態(tài)庫,所以我們用動態(tài)庫的方式。
9. 不使用Exception和RTTI
關于這兩點在網(wǎng)上看到的沒有實踐過,不過拿過來可以作為包體積持續(xù)優(yōu)化的參考。
RTTI
通過RTTI,能夠通過基類的指針或引用來檢索其所指對象的實際類型,即運行時獲取對象的實際類型。C++通過下面兩個操作符提供RTTI。
(1)typeid:返回指針或引用所指對象的實際類型。
(2)dynamic_cast:將基類類型的指針或引用安全的轉換為派生類型的指針或引用。
RTTI的選項是默認關閉的的,而代碼中其實并沒有用到相關的功能,這里可以直接關閉。
Exception
使用C++的exception會增加包的大小,而目前JNI對C++的exception的支持是有bug的,比如下面這段代碼就會引起程序的crash(對于低版本的android NDK)。因此要在程序中引入exception要自己實現(xiàn)相關邏輯,但是這樣又會增加包體大小。對于開發(fā)者來說,exception可以幫助快速定位問題,而對于使用者并不是那么重要,這里可以去掉。
10 總結
本文介紹了刪除無用模塊,平臺能力替代第三方庫,使用 strip,設置編譯器優(yōu)化的 flag,使用gc-sections去除沒有用到的函數(shù),設置可見性,去除iostream等有助于動態(tài)庫體積優(yōu)化的方法。
原文鏈接: https://juejin.cn/post/7084238491089027079
推薦閱讀:
NDK | 帶你梳理 JNI 函數(shù)注冊的方式和時機
Android NDK 開發(fā):Java 與 Native 相互調用
