網站首頁 編程語言 正文
背景
這周在做Yoga包的壓縮工作。Yoga本身是用BUCK腳本編譯的,而最終編譯出幾個包大小大總共約為7M,不能滿足項目中對于APK大小的限制,因此需要對它進行壓縮。
這里先將Yoga編譯腳本用CMAKE重新改寫,以便可以在android studio中直接使用并輸出一個AAR的包。后面又對它進行了壓縮,最終將Yoga包的大小壓縮到200多KB。
下面整理了一些可以用于減少NDK開發中Android SO包大小的方法:
1.STL的使用方式
對于C++的library,引用方式有2種:
- 靜態方式(static)
- 動態方式(shared)
其中,靜態方式在編譯時會將用到的相關代碼直接復制到目的文件中;而動態方式則會將相關的代碼打成so文件,以便多次引用。由于編譯器在編譯時并不能知道所有被引用的地方,所以同時會打入了很多不相關的代碼。
所以,如果項目中引用library的函數較多時,用動態方式可以避免多次拷貝,節省空間。相反,則直接使用靜態方式會更節省空間。
NDK開發中,可以通過gradle的設置來配置:
defaultConfig{
externalNativeBuild{
cmake{
// gnustl_shared 動態
arguments "-DANDROID_STL=gnustl_static"
}
}
}
在Yoga中,項目里的stl使用較少時,安卓運行時使用static的方式,而不是shared,所以這里采用static的方式。在采取了這種方式后,包的大小從2.7M縮減到了2M。
2.不使用Exception和RTTI
C++的exception和RTTI功能在NDK中默認是關閉的,但是可以通過配置打開的。
Android.mk:
APP_CPPFLAGS += -fexceptions -frtti
CMake:
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fexceptions -frtti")
Exception和RTTI會顯著的增加包的體積,所以非必須的時候,沒有必要使用。
RTTI
通過RTTI,能夠通過基類的指針或引用來檢索其所指對象的實際類型,即運行時獲取對象的實際類型。C++通過下面兩個操作符提供RTTI。
(1)typeid:返回指針或引用所指對象的實際類型。
(2)dynamic_cast:將基類類型的指針或引用安全的轉換為派生類型的指針或引用。
在yoga中,RTTI的選項是默認打開的,而代碼中其實并沒有用到相關的功能,這里可以直接關閉。
Exception
使用C++的exception會增加包的大小,而目前JNI對C++的exception的支持是有bug的,比如下面這段代碼就會引起程序的crash(對于低版本的android NDK)。
因此要在程序中引入exception要自己實現相關邏輯,yoga就是這么做的,這個又增加了一些包體大小。對于開發者來說,exception可以幫助快速定位問題,而對于使用者并不是那么重要,這里可以去掉。
try {
...
} catch (std::exception& e) {
env->ThrowNew(env->FindClass("java/lang/Exception"), "Error occured");
}
在yoga中,在關閉RTTI和Exception功能并把exception相關的代碼都去掉后,包的大小從2M縮減到的1.8M。
3.使用 gc-sections去除沒有用到的函數
去除未使用的代碼顯然可以減少包體的大小,而在NDK的開發中,并不需要手動的來做這一點。可以開啟編譯器的gc-sections選項,讓編譯器自動的幫你做到這一點。
編譯器可以配置自動去除未使用的函數和變量,以下是配置方式:
CMake:
# 去除未使用函數與變量
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:
LOCAL_CPPFLAGS += -ffunction-sections -fdata-sections
LOCAL_CFLAGS += -ffunction-sections -fdata-sections
LOCAL_LDFLAGS += -Wl,--gc-sections
4.去除冗余代碼
在NDK中,鏈接器還有一個選項 “-icf = safe”,可以用于去除代碼中的冗余代碼。但是要注意的是,這個選項也有可能去除定義好的inline函數,這里必須要做好權衡。
下面是配置方式:
CMake:
SET_TARGET_PROPERTIES(yoga PROPERTIES LINK_FLAGS "-Wl,--gc-sections,--icf=safe")
Android.mk:
LOCAL_LDFLAGS += -Wl,--gc-sections,--icf=safe
5.設置編譯器的優化flag
編譯器有個優化flag可以設置,分別是-Os(體積最小),-O3(性能最優)等。這里將編譯器的優化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
在采用了3,4,5這幾種方式后,Yoga包的大小從1.8M減少到了1.7M。這里減少的比較少是因為Yoga在這方面已經做的挺好了,其他的庫可能會更有效。
6.設置編譯器的 Visibility Feature
還有個減少包體大小的方法,就是設置編譯器的visibility feature。
Visibility Feature就是用來控制在哪些函數可以在符號表中被輸入,由于C++并不是完全面向對象的,非類的方法并沒有public這種修飾符,因此,要用Visibility Feature來控制哪些函數可以被外部調用。
而JNI提供了一個宏-JNIEXPORT來控制這點。所以只要對函數加上這個宏,像這樣:
// JNIEXPORT就是控制可見的宏
// JNICALL在NDK這里沒有什么意義,只是個標識宏
JNIEXPORT void JNICALL Java_ClassName_MethodName(JNIEnv *env, jobject obj, jstring javaString)
然后在編譯器的FLAGS選項開啟 -fvisibility = hidden 就可以。這樣,不僅可以控制函數的可見性,并且可以減少包體的大小。
CMake:
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden")
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}")
7.設置編譯器的Strip選項
我在把Yoga庫編譯成AAR包的過程中發現,它的體積明顯會大于最后打包進APK的大小,這點非常不合理,但是無法找到原因。
最終搜索到這是谷歌NDK的一個bug,在打AAR包的過程中,無論是debug版本還是release版本,NDK toolchain不會自動的把方便調試的C++ 符號表(Symbol Table)中數據刪除,而只會在打APK包的時候進行這一操作。這就導致了打成的AAR包中的SO體積明顯偏大。
找到原因后這個問題就很好解決了,可以手動的在鏈接選項中加入 strip參數,配置如下所示:
SET_TARGET_PROPERTIES(yoga PROPERTIES LINK_FLAGS "-Wl,--gc-sections,--icf=safe,-s")
在強制進行strip操作后,將Yoga包的體積從1.7M成功減少到了282KB。
8.去除C++代碼中的iostream相關代碼
使用STL中的iostream相關庫會明顯的增加包的體積,而NDK本身是有預編譯庫(android/log.h)可以代替這一功能的,在Yoga這里,用log的函數代替了iostream中的所有函數,如:
//代替所有的iostream庫里函數
//cout << obj->toString() << endl;
__android_log_print(ANDROID_LOG_VERBOSE,"Yoga","Node is: %s",obj->toString().c_str());
在做完代替之后,yoga包的體積從282KB減少到了218KB。
總結
在做完這一系列工作后,最終成功的壓縮了Yoga包的體積,從幾M到最后輸出一個218KB的AAR包提供使用。以上幾種方法并不局限于Yoga包的縮減。在NDK開發中,要縮減SO包的體積都可以按照這幾種方式嘗試一下。
原文鏈接:https://www.androidos.net.cn/doc/2022/8/17/133.html
相關推薦
- 2022-12-04 Dart?異步編程生成器及自定義類型用法詳解_Dart
- 2022-07-26 在Pycharm set ops_config=local之后,直接echo %ops_config
- 2022-10-06 Redis中鍵和數據庫通用指令詳解_Redis
- 2023-05-14 Go結構體的基本使用詳解_Golang
- 2022-09-22 linux進程概念
- 2022-12-23 Android入門之SubMenu的實現詳解_Android
- 2023-07-08 SparkMD5獲取不同圖片的md5顯示相同,解決辦法
- 2022-06-01 Python寫一個字符串數字后綴部分的遞增函數_python
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支