精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Android對so體積優化的探索與實踐

原創 精選
移動開發 新聞
本文將先從 so 文件格式講起,結合文件格式分析哪些內容可以優化,然后再具體講解每項優化手段以及注意事項,最后介紹相關的工程實踐經驗。希望能對從事包體積優化的同學有所幫助或啟發。

作者:洪凱 常強 

1. 背景

應用安裝包的體積影響著用戶的下載時長、安裝時長、磁盤占用空間等諸多方面,因此減小安裝包的體積對于提升用戶體驗和下載轉化率都大有益處。Android 應用安裝包其實是一個 zip 文件,主要由 dex、assets、resource、so 等各類型文件壓縮而成。目前業內常見的包體積優化方案大體分為以下幾類:

針對 dex 的優化,例如 Proguard、dex 的 DebugItem 刪除、字節碼優化等;

針對 resource 的優化,例如 AndResGuard、webp 優化等;

針對 assets 的優化,例如壓縮、動態下發等;

針對 so 的優化,同 assets,另外還有移除調試符號等。

隨著動態化、端智能等技術的廣泛應用,在采用上述優化手段后, so 在安裝包體積中的比重依然很高,我們開始思索這部分體積是否能進一步優化。經過一段時間的調研、分析和驗證,我們逐漸摸索出一套可以將應用安裝包中 so 體積進一步減小 30%~60% 的方案。

該方案包含一系列純技術優化手段,對業務侵入性低,通過簡單的配置,可以快速部署生效,目前美團 App 已在線上部署使用。為讓大家能知其然,也能知其所以然,本文將先從 so 文件格式講起,結合文件格式分析哪些內容可以優化。

2. so 文件格式分析

so 即動態庫,本質上是 ELF(Executable and Linkable Format)文件。可以從兩個維度查看 so 文件的內部結構:鏈接視圖(Linking View)和執行視圖(Execution View)。鏈接視圖將 so 主體看作多個 section 的組合,該視圖體現的是 so 是如何組裝的,是編譯鏈接的視角。而執行視圖將 so 主體看作多個 segment 的組合,該視圖告訴動態鏈接器如何加載和執行該 so,是運行時的視角。鑒于對 so 優化更側重于編譯鏈接角度,并且通常一個 segment 包含多個 section(即鏈接視圖對 so 的分解粒度更小),因此我們這里只討論 so 的鏈接視圖。通過 readelf -S 命令可以查看一個 so 文件的所有 section 列表,參考 ELF 文件格式說明,這里簡要介紹一下本文涉及的 section:

  • .text:存放的是編譯后的機器指令,C/C++代碼的大部分函數編譯后就存放在這里。這里只有機器指令,沒有字符串等信息。
  • .data:存放的是初始值不為零的一些可讀寫變量。
  • .bss:存放的是初始值為零或未初始化的一些可讀寫變量。該 section 僅指示運行時需要的內存大小,不會占用 so 文件的體積。
  • .rodata:存放的是一些只讀常量。
  • .dynsym:動態符號表,給出了該 so 對外提供的符號(導出符號)和依賴外部的符號(導入符號)的信息。
  • .dynstr?:字符串池,不同字符串以 '\0' 分割,供 .dynsym 和其他部分使用。
  • .gnu.hash? 和.hash?:兩種類型的哈希表,用于快速查找 .dynsym 中的導出符號或全部符號。
  • .gnu.version、.gnu.version_d、.gnu.version_r?:這三個 section 用于指定動態符號表中每個符號的版本,其中.gnu.version? 是一個數組,其元素個數與動態符號表中符號的個數相同,即數組每個元素與動態符號表的每個符號是一一對應的關系。數組每個元素的類型為 Elfxx_Half?,其意義是索引,指示每個符號的版本。.gnu.version_d? 描述了該 so 定義的所有符號的版本,供.gnu.version? 索引。.gnu.version_r? 描述了該 so 依賴的所有符號的版本,也供 .gnu.version 索引。因為不同的符號可能具有相同的版本,所以采用這種索引結構,可以減小 so 文件的大小。

在進行優化之前,我們需要對這些 section 以及它們之間的關系有一個清晰的認識,下圖較直觀地展示了 so 中各個 section 之間的關系(這里只繪制了本文涉及的 section):

圖片

圖1 so文件結構示意圖

結合上圖,我們從另一個角度來理解 so 文件的結構:想象一下,我們把所有的函數實現體都放到.text 中,.text 中的指令會去讀取 .rodata 中的數據,讀取或修改 .data 和 .bss 中的數據。看上去 so 中有這些內容也足夠了。但是這些函數怎樣執行呢?也就是說,只把這些函數和數據加載進內存是不夠的,這些函數只有真正去執行,才能發揮作用。

我們知道想要執行一個函數,只要跳轉到它的地址就行了。那外界調用者(該 so 之外的模塊)怎樣知道它想要調用函數的地址呢?這里就涉及一個函數 ID 的問題:外部調用者給出需要調用的函數的 ID,而動態鏈接器(Linker)根據該 ID 查找目標函數的地址并告知外部調用者。所以 so 文件還需要一個結構去存儲“ID-地址”的映射關系,這個結構就是動態符號表的所有導出符號。

具體到動態符號表的實現,ID 的類型是“字符串”,可以說動態符號表的所有導出符號構成了一個“字符串-地址“的映射表。調用者獲取目標函數的地址后,準備好參數跳轉到該地址就可以執行這個函數了。另一方面,當前 so 可能也需要調用其他 so 中的函數(例如 libc.so 中的 read、write 等),動態符號表的導入符號記錄了這些函數的信息,在 so 內函數執行之前動態鏈接器會將目標函數的地址填入到相應位置,供該 so 使用。

所以動態符號表是連接當前 so 與外部環境的“橋梁”:導出符號供外部使用,導入符號聲明了該 so 需要使用的外部符號(注:實際上.dynsym中的符號還可以代表變量等其他類型,與函數類型類似,這里就不再贅述)。結合 so 文件結構,接下來我們開始分析 so 中有哪些內容可以優化。

3. so 可優化內容分析

在討論 so 可優化內容之前,我們先了解一下 Android 構建工具(Android Gradle Plugin,下文簡稱 AGP)對 so 體積做的 strip 優化(移除調試信息和符號表)。

AGP 編譯 so 時,首先產生的是帶調試信息和符號表的 so(任務名為 externalNativeBuildRelease),之后對剛產生的帶調試信息和符號表的 so 進行 strip,就得到了最終打包到 apk 或 aar 中的 so(任務名為 stripReleaseDebugSymbols)。

strip 優化的作用就是刪除輸入 so 中的調試信息和符號表。這里說的符號表與上文中的“動態符號表”不同,符號表所在 section 名通常為 .symtab,它通常包含了動態符號表中的全部符號,并且額外還有很多符號。

調試信息顧名思義就是用于調試該 so 的信息,主要是各種名字以 .debug_ 開頭的 section,通過這些 section 可以建立 so 每條指令與源碼文件的映射關系(也就是能夠對 so 中每條指令找到其對應的源碼文件名、文件行號等信息)。之所以叫 strip 優化,是因為其實際調用的是 NDK 提供的的 strip 命令(所用參數為--strip-unneeded)。

注:為什么 AGP 要先編譯出帶調試信息和符號表的 so,而不直接編譯出最終的 so 呢(通過添加-s參數是可以做到直接編譯出沒有調試信息和符號表的 so 的)?原因就在于需要使用帶調試信息和符號表的 so 對崩潰調用棧進行還原。刪除了調試信息和符號表的 so 完全可以正常運行,但是當它發生崩潰時,只能保證獲取到崩潰調用棧的每個棧幀的相應指令在 so 中的位置,不一定能獲取到符號。但是排查崩潰問題時,我們希望得知 so 崩潰在源碼的哪個位置。帶調試信息和符號表的 so 可以將崩潰調用棧的每個棧幀還原成其對應的源碼文件名、文件行號、函數名等,大大方便了崩潰問題的排查。所以說,雖然帶調試信息和符號表的 so 不會打包到最終的 apk 中,但它對排查問題來說非常重要。

AGP 通過開啟 strip 優化,可以大幅縮減 so 的體積,甚至可以達到十倍以上。以一個測試 so 為例,其最終 so 大小為14 KB,但是對應的帶調試信息和符號表的 so 大小為 136 KB。不過在使用中,我們需要注意的是,如果 AGP 找不到對應的 strip 命令,就會把帶調試信息和符號表的 so 直接打包到 apk 或 aar 中,并不會打包失敗。例如缺少 armeabi 架構對應的 strip 命令時提示信息如下:

Unable to strip library 'XXX.so' due to missing strip tool for ABI 'ARMEABI'. Packaging it as is.

除了上述 Android 構建工具默認為 so 體積做的優化,我們還能做哪些優化呢?首先明確我們優化的原則:

  • 對于必須保留的內容考慮進行縮減,減小體積占用;
  • 對于無需保留的內容直接刪除。

基于以上原則,可以從以下三個方面對 so 繼續進行深入優化:

  • 精簡動態符號表:上文已經提到,動態符號表是 so 與外部進行連接的“橋梁”,其中的導出表相當于是 so 對外暴露的接口。哪些接口是必須對外暴露的呢?在 Android 中,大部分 so 是用來實現 Java 的 native 方法的,對于這種 so,只要讓應用運行時能夠獲取到 Java native 方法對應的函數地址即可。要實現這個目標,有兩種方法:一種是使用 RegisterNatives 動態注冊 Java native 方法,一種是按照 JNI 規范定義 java_***? 樣式的函數并導出其符號。RegisterNatives 方式可以提前檢測到方法簽名不匹配的問題,并且可以減少導出符號的數量,這也是 Google 推薦的做法。所以在最優情況下只需導出 JNI_OnLoad?(在其中使用 RegisterNatives 對 Java native 方法進行動態注冊)和 JNI_OnUnload?(可以做一些清理工作)這兩個符號即可。如果不希望改寫項目代碼,也可以再導出 java_***? 樣式的符號。除了上述類型的 so,剩余的 so 通常是被應用的其他 so 動態依賴的,對于這類 so,需要確定所有動態依賴它的 so 依賴了它的哪些符號,僅保留這些被依賴的符號即可。另外,這里應區分符號表項與實現體,符號表項是動態符號表中相應的 Elfxx_Sym? 項(見上圖),實現體是其在 .text、.data?、 .bss、.rodata? 等或其他部分的實體。刪除了符號表項,實現體不一定要被刪除。結合上文 so 文件結構示意圖,可以預估出刪除一個符號表項后 so 減小的體積為:符號名字符串長度+ 1 + Elfxx_Sym? + Elfxx_Half? + Elfxx_Word 。
  • 移除無用代碼:在實際的項目中,有一些代碼在 Release 版中永遠不會被使用到(例如歷史遺留代碼、用于測試的代碼等),這些代碼被稱為 DeadCode。而根據上文分析,只有動態符號表的導出符號直接或間接引用到的所有代碼才需要保留,其他剩余的所有代碼都是 DeadCode,都是可以刪除的(注:事實上.init_array等特殊 section 涉及的代碼也要保留)。刪除無用代碼的潛在收益較大。
  • 優化指令長度:實現某個功能的指令并不是固定的,編譯器有可能能用更少的指令完成相同的功能,從而實現優化。由于指令是 so 的主要組成部分,因此優化這一部分的潛在收益也比較大。

so 可優化內容如下圖所示(可刪除部分用紅色背景標出,可優化部分是.text?),其中 funC、value2、value3、value6 由于分別被需保留部分使用,所以需要保留其實現體,只能刪除其符號表項。funD、value1、value4、value5 可刪除符號表項及其實現體(注:因為 value4 的實現體在.bss?中,而.bss?實際不占用 so 的體積,所以刪除 value4 的實現體不會減小 so 的體積)。圖片

圖2 so可優化部分

在確定了 so 中可以優化的內容后,我們還需要考慮優化時機的問題:是直接修改 so 文件,還是控制其生成過程?考慮到直接修改 so 文件的風險與難度較大,控制 so 的生成過程顯然更穩妥。為了控制 so 的生成過程,我們先簡要介紹一下 so 的生成過程:

圖片

圖3 so文件的生成過程如上圖所示,so 的生成過程可以分為四個階段:

  • 預處理:將 include 頭文件處擴展為實際文件內容并進行宏定義替換。
  • 編譯:將預處理后的文件編譯成匯編代碼。
  • 匯編:將匯編代碼匯編成目標文件,目標文件中包含機器指令(大部分情況下是機器指令,見下文 LTO 一節)和數據以及其他必要信息。
  • 鏈接:將輸入的所有目標文件以及靜態庫(.a 文件)鏈接成 so 文件。

可以看出,預處理和匯編階段對特定輸入產生的輸出基本是固定的,優化空間較小。所以我們的優化方案主要是針對編譯和鏈接階段進行優化。

4. 優化方案介紹

我們對所有能控制最終 so 體積的方案都進行調研,并驗證了其效果,最后總結出較為通用的可行方案。

4.1 精簡動態符號表

使用 visibility 和 attribute 控制符號可見性

可以通過給編譯器傳遞 -fvisibility=VALUE 控制全局的符號可見性,VALUE 常取值為 default 和 hidden:

  • default:除非對變量或函數特別指定符號可見性,所有符號都在動態符號表中,這也是不使用 -fvisibility 時的默認值。
  • hidden:除非對變量或函數特別指定符號可見性,所有符號在動態符號表中都不可見。

CMake 項目的配置方式:

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=hidden")

ndk-build 項目的配置方式:

LOCAL_CFLAGS += -fvisibility=hidden

另一方面,針對單個變量或函數,可以通過 attribute 方式指定其符號可見性,示例如下:

__attribute__((visibility("hidden")))
int hiddenInt=3;

其常用值也是 default 和 hidden,與 visibility 方式意義類似,這里不再贅述。attribute 方式指定的符號可見性的優先級,高于 visibility 方式指定的可見性,相當于 visibility 是全局符號可見性開關,attribute 方式是針對單個符號的可見性開關。這兩種方式結合就能控制源碼中每個符號的可見性。需要注意的是上面這兩種方式,只能控制變量或函數是否存在于動態符號表中(即是否刪除其動態符號表項),而不會刪除其實現體。

使用 static 關鍵字控制符號可見性

在C/C++語言中,static 關鍵字在不同場景下有不同意義,當使用 static 表示“該函數或變量僅在本文件可見”時,那么這個函數或變量就不會出現在動態符號表中,但只會刪除其動態符號表項,而不會刪除其實現體。static 關鍵字相當于是增強的 hidden(因為 static 聲明的函數或變量編譯時只對當前文件可見,而 hidden 聲明的函數或變量只是在動態符號表中不存在,在編譯期間對其他文件還是可見的)。在項目開發中,使用 static 關鍵字聲明一個函數或變量“僅在本文件可見”是很好的習慣,但是不建議使用 static 關鍵字控制符號可見性:無法使用 static 關鍵字控制一個多文件可見的函數或變量的符號可見性。

使用 exclude libs 移除靜態庫中的符號

上述 visibility 方式、attribute 方式和 static 關鍵字,都是控制項目源碼中符號的可見性,而無法控制依賴的靜態庫中的符號在最終 so 中是否存在。exclude libs 就是用來控制依賴的靜態庫中的符號是否可見,它是傳遞給鏈接器的參數,可以使依賴的靜態庫的符號在動態符號表中不存在。同樣,也是只能刪除符號表項,實現體仍然會存在于產生的 so 文件中。CMake 項目的配置方式:

set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--exclude-libs,ALL")#使所有靜態庫中的符號都不被導出
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--exclude-libs,libabc.a")#使 libabc.a 的符號都不被導出

ndk-build 項目的配置方式:

LOCAL_LDFLAGS += -Wl,--exclude-libs,ALL #使所有靜態庫中的符號都不被導出
LOCAL_LDFLAGS += -Wl,--exclude-libs,libabc.a #使 libabc.a 的符號都不被導出

使用 version script 控制符號可見性

version script 是傳遞給鏈接器的參數,用來指定動態庫導出哪些符號以及符號的版本。該參數會影響到上面“so 文件格式”一節中 .gnu.version 和 .gnu.version_d 的內容。我們現在只使用它的指定所有導出符號的功能(即符號版本名使用空字符串)。開啟 version script 需要先編寫一個文本文件,用來指定動態庫導出哪些符號。示例如下(只導出 usedFun 這一個函數):

{
global:usedFun;
local:*;
};

然后將上述文件的路徑傳遞給鏈接器即可(假定上述文件名為version_script.txt)。CMake 項目的配置方式:

set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/version_script.txt") #version_script.txt 與當前 CMakeLists.txt 同目錄

ndk-build 項目的配置方式:

LOCAL_LDFLAGS += -Wl,--version-script=${LOCAL_PATH}/version_script.txt #version_script.txt 與當前 Android.mk 同目錄

看上去,version script 是明確地指定需要保留的符號,如果通過 visibility 結合 attribute 的方式控制每個符號是否導出,也能達到 version script 的效果,但是 version script 方式有一些額外的好處:

  1. version script 方式可以控制編譯進 so 的靜態庫的符號是否導出,visibility 和 attribute 方式都無法做到這一點。
  2. visibility 結合 attribute 方式需要在源碼中標明每個需要導出的符號,對于導出符號較多的項目來說是很繁雜的。version script 把需要導出的符號統一地放到了一起,能夠直觀方便地查看和修改,對導出符號較多的項目也非常友好。
  3. version script 支持通配符,*? 代表0個或者多個字符,?? 代表單個字符。比如 my*; 就代表所有以 my 開頭的符號。有了通配符的支持,配置 version script 會更加方便。
  4. 還有非常特殊的一點,version script 方式可以刪除 __bss_start 這樣的一些符號(這是鏈接器默認加上的符號)。

綜上所述,version script 方式優于 visibility 結合 attribute 的方式。同時,使用了 version script 方式,就不需要使用 exclude libs 方式控制依賴的靜態庫中的符號是否導出了。

4.2 移除無用代碼

開啟 LTO

LTO 是 Link Time Optimization 的縮寫,即鏈接期優化。LTO 能夠在鏈接目標文件時檢測出 DeadCode 并刪除它們,從而減小編譯產物的體積。DeadCode 舉例:某個 if 條件永遠為假,那么 if 為真下的代碼塊就可以移除。進一步地,被移除代碼塊所調用的函數也可能因此而變為 DeadCode,它們又可以被移除。能夠在鏈接期做優化的原因是,在編譯期很多信息還不能確定,只有局部信息,無法執行一些優化。但是鏈接時大部分信息都確定了,相當于獲取了全局信息,所以可以進行一些優化。GCC 和 Clang 均支持 LTO。LTO 方式編譯的目標文件中存儲的不再是具體機器的指令,而是機器無關的中間表示(GCC 采用的是 GIMPLE 字節碼,Clang 采用的是 LLVM IR 比特碼)。CMake 項目的配置方式:

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -flto")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -O3 -flto")

ndk-build 項目的配置方式:

LOCAL_CFLAGS += -flto
LOCAL_LDFLAGS += -O3 -flto

使用 LTO 時需要注意幾點:

  1. 如果使用 Clang,編譯參數和鏈接參數中都要開啟 LTO,否則會出現無法識別文件格式的問題(NDK22 之前存在此問題)。使用 GCC 的話,只需要編譯參數中開啟 LTO 即可。
  2. 如果項目工程依賴了靜態庫,可以使用 LTO 方式重新編譯該靜態庫,那么編譯動態庫時,就能移除靜態庫中的 DeadCode,從而減小最終 so 的體積。
  3. 經過測試,如果使用 Clang,鏈接器需要開啟非 0 級別的優化,LTO 才能真正生效。經過實際測試(NDK 為 r16b),O1 優化效果較差,O2、O3 優化效果比較接近。
  4. 由于需要進行更多的分析計算,開啟 LTO 后,鏈接耗時會明顯增加。

開啟 GC sections

這是傳遞給鏈接器的參數,GC 即 Garbage Collection(垃圾回收),也就是對無用的 section 進行回收。注意,這里的 section 不是指最終 so 中的 section,而是作為鏈接器的輸入的目標文件中的 section。

簡要介紹一下目標文件,目標文件(擴展名 .o )也是 ELF 文件,所以也是由 section 組成的,只不過它只包含了相應源文件的內容:函數會放到 .text 樣式的 section 中,一些可讀寫變量會放到 .data  樣式的 section 中,等等。鏈接器會把所有輸入的目標文件的同類型的 section 進行合并,組裝出最終的 so 文件。

GC sections 參數通知鏈接器:僅保留動態符號(及 .init_array等)直接或者間接引用到的 section,移除其他無用 section。這樣就能減小最終 so 的體積。但開啟 GC sections 還需要考慮一個問題:編譯器默認會把所有函數放到同一個 section 中,把所有相同特點的數據放到同一個 section 中,如果同一個 section 中既有需要刪除的部分又有需要保留的部分,會使得整個 section 都要保留。

所以我們需要減小目標文件 section 的粒度,這需要借助另外兩個編譯參數 -fdata-sections 和 -ffunction-sections ,這兩個參數通知編譯器,將每個變量和函數分別放到各自獨立的 section 中,這樣就不會出現上述問題了。實際上 Android 編譯目標文件時會自動帶上 -fdata-sections 和 -ffunction-sections 參數,這里一并列出來,是為了突出它們的作用。CMake 項目的配置方式:

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fdata-sections -ffunction-sections")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fdata-sections -ffunction-sections")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--gc-sections")

ndk-build 項目的配置方式:

LOCAL_CFLAGS += -fdata-sections -ffunction-sections
LOCAL_LDFLAGS += -Wl,--gc-sections

4.3 優化指令長度

使用 Oz/Os 優化級別

編譯器根據輸入的 -Ox 參數決定編譯的優化級別,其中 O0 表示不開啟優化(這種情況主要是為了便于調試以及更快的編譯速度),從 O1 到 O3,優化程度越來越強。Clang 和 GCC 均提供了 Os 的優化級別,其與 O2 比較接近,但是優化了生成產物的體積。而 Clang 還提供了 Oz 優化級別,在 Os 的基礎上能進一步優化產物體積。綜上,編譯器是 Clang,可以開啟 Oz 優化。如果編譯器是 GCC,則只能開啟 Os 優化(注:NDK 從 r13 開始默認編譯器從 GCC 變為 Clang,r18 中正式移除了 GCC。GCC 不支持 Oz 是指 Android 最后使用的 GCC4.9 版本不支持 Oz 參數)。Oz/Os 優化相比于 O3 優化,優化了產物體積,性能上可能有一定損失,因此如果項目原本使用了 O3 優化,可根據實際測試結果以及對性能的要求,決定是否使用 Os/Oz 優化級別,如果項目原本未使用 O3 優化級別,可直接使用 Os/Oz 優化。CMake 項目的配置方式(如果使用 GCC,應將 Oz 改為 Os):

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Oz")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Oz")

ndk-build 項目的配置方式(如果使用 GCC,應將 Oz 改為 Os):

LOCAL_CFLAGS += -Oz

4.4 其他措施

禁用 C++ 的異常機制

如果項目中沒有使用 C++ 的異常機制(例如try...catch等),可以通過禁用 C++ 的異常機制,來減小 so 的體積。CMake 項目的配置方式:

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions")

ndk-build 默認會禁用 C++ 的異常機制,因此無需特意禁用(如果現有項目開啟了 C++ 的異常機制,說明確有需要,需仔細確認后才能禁用)。

禁用 C++ 的 RTTI 機制

如果項目中沒有使用 C++ 的 RTTI 機制(例如 typeid 和 dynamic_cast 等),可以通過禁用 C++ 的 RTTI ,來減小 so 的體積。CMake 項目的配置方式:

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti")

ndk-build 默認會禁用 C++ 的 RTTI 機制,因此無需特意禁用(如果現有項目開啟了 C++ 的 RTTI 機制,說明確有需要,需仔細確認后才能禁用)。

合并 so

以上都是針對單個 so 的優化方案,對單個 so 進行優化后,還可以考慮對 so 進行合并,能夠進一步減小 so 的體積。具體來講,當安裝包內某些 so 僅被另外一個 so 動態依賴時,可以將這些 so 合并為一個 so。例如 liba.so 和 libb.so 僅被 libx.so 動態依賴,可以將這三個 so 合并為一個新的 libx.so。合并 so 有以下好處:

  1. 可以刪除部分動態符號表項,減小 so 總體積。具體來講,就是可以刪除 liba.so 和 libb.so 的動態符號表中的所有導出符號,以及 libx.so 的動態符號表中從 liba.so 和 libb.so 中導入的符號。
  2. 可以刪除部分 PLT 表項和 GOT 表項,減小 so 總體積。具體來講,就是可以刪除 libx.so 中與 liba.so、libb.so 相關的 PLT 表項和 GOT 表項。
  3. 可以減輕優化的工作量。如果沒有合并 so,對 liba.so 和 libb.so 做體積優化時需要確定 libx.so 依賴了它們的哪些符號,才能對它們進行優化,做了 so 合并后就不需要了。鏈接器會自動分析引用關系,保留使用到的所有符號的對應內容。
  4. 由于鏈接器對原 liba.so 和 libb.so 的導出符號擁有了更全的上下文信息,LTO 優化也能取得更好的效果。

可以在不修改項目源碼的情況下,在編譯層面實現 so 的合并。

提取多 so 共同依賴庫

上面“合并 so”是減小 so 總個數,而這里是增加 so 總個數。當多個 so 以靜態方式依賴了某個相同的庫時,可以考慮將此庫提取成一個單獨的 so,原來的幾個 so 改為動態依賴該 so。例如 liba.so 和 libb.so 都靜態依賴了 libx.a,可以優化為 liba.so 和 libb.so 均動態依賴 libx.so。提取多 so 共同依賴庫,可以對不同 so 內的相同代碼進行合并,從而減小總的 so 體積。這里典型的例子是 libc++ 庫:如果存在多個 so 都靜態依賴 libc++ 庫的情況,可以優化為這些 so 都動態依賴于 libc++_shared.so。

4.5 整合后的通用方案

通過上述分析,我們可以整合出普通項目均可使用的通用的優化方案,CMake 項目的配置方式(如果使用 GCC,應將 Oz 改為 Os):

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Oz -flto -fdata-sections -ffunction-sections")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Oz -flto -fdata-sections -ffunction-sections")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -O3 -flto -Wl,--gc-sections -Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/version_script.txt") #version_script.txt 與當前 CMakeLists.txt 同目錄

ndk-build 項目的配置方式(如果使用 GCC,應將 Oz 改為 Os):

LOCAL_CFLAGS += -Oz -flto -fdata-sections -ffunction-sections
LOCAL_LDFLAGS += -O3 -flto -Wl,--gc-sections -Wl,--version-script=${LOCAL_PATH}/version_script.txt #version_script.txt 與當前 Android.mk 同目錄

其中 version_script.txt 較為通用的配置如下,可根據實際情況添加需要保留的導出符號:

{
global:JNI_OnLoad;JNI_OnUnload;Java_*;
local:*;
};

說明:version script 方式指定所有需要導出的符號,不再需要 visibility 方式、attribute 方式、static 關鍵字和 exclude libs 方式控制導出符號。是否禁用 C++ 的異常機制和 RTTI 機制、合并 so 以及提取多 so 共同依賴庫取決于具體項目,不具有通用性。至此,我們總結出一套可行的 so 體積優化方案。但在工程實踐中,還有一些問題要解決。

5. 工程實踐

支持多種構建工具

美團有眾多業務使用了 so,所使用的構建工具也不盡相同,除了上述常見的 CMake 和 ndk-build,也有項目在使用 Make、Automake、Ninja、GYP 和 GN 等各種構建工具。不同構建工具應用 so 優化方案的方式也不相同,尤其對大型工程而言,配置復雜性較高。基于以上原因,每個業務自行配置 so 優化方案會消耗較多的人力成本,并且有配置無效的可能。為了降低配置成本、加快優化方案的推進速度、保證配置的有效性和正確性,我們在構建平臺上統一支持了 so 的優化(支持使用任意構建工具的項目)。業務只需進行簡單的配置即可開啟 so 的體積優化。

配置導出符號的注意事項

注意事項有以下兩點:

  1. 如果一個 so 的某些符號,被其他 so 通過 dlsym 方式使用,那么這些符號也應該保留在該 so 的導出符號中(否則會導致運行時異常)。
  2. 編寫 version_script.txt? 時需要注意 C++ 等語言對符號的修飾,不能直接把函數名填寫進去。符號修飾就是把一個函數的命名空間(如果有)、類名(如果有)、參數類型等都添加到最終的符號中,這也是 C++ 語言實現重載的基礎。有兩種方式可以把 C++ 的函數添加到導出符號中:第一種是查看未優化 so 的導出符號表,找到目標函數被修飾后的符號,然后填寫到 version_script.txt 中。例如有一個 MyClass 類:
class MyClass{
void start(int arg);
void stop();
};

要確定 start 函數真正的符號可以對未優化的 libexample.so 執行以下命令。因為 C++ 對符號修飾后,函數名是符號的一部分,所以可以通過 grep 加快查找:

圖片

圖4 查找 start 函數真正符號可以看到 start 函數真正的符號是 _ZN7MyClass5startEi。如果想導出該函數,version_script.txt 相應位置填入 _ZN7MyClass5startEi 即可。第二種方式是在 version_script.txt 中使用 extern 語法,如下所示:

{
global:
extern "C++" {
MyClass::start*;
"MyClass::stop()";
};
local:*;
};

上述配置可以導出 MyClass 的 start 和 stop 函數。其原理是,鏈接時鏈接器對每個符號進行 demangle(解構,即把修飾后的符號還原為可讀的表示),然后與 extern "C++" 中的條目進行匹配,如果能與任一條目匹配成功就保留該符號。

匹配的規則是:有雙引號的條目不能使用通配符,需要全字符串完全匹配才可以(例如 stop 條目,如果括號之間多一個空格就會匹配失敗)。對于沒有雙引號的條目能夠使用通配符(例如 start 條目)。

查看優化后 so 的導出符號

業務對 so 進行優化之后,需要查看最終的 so 文件中保留了哪些導出符號,驗證優化效果是否符合預期。在 Mac 和 Linux 下均可使用下述命令查看 so 保留了哪些導出符號:

nm -D --defined-only xxx.so

例如:

圖片

圖5 nm命令查看so文件的導出符號可以看出,libexample.so 的導出符號有兩個:JNI_OnLoad 和 Java_com_example_MainActivity_stringFromJNI。

解析崩潰堆棧

本文的優化方案會移除非必要導出的動態符號,那 so 如果發生崩潰的話是不是就無法解析崩潰堆棧了呢?答案是完全不會影響崩潰堆棧的解析結果。“so 可優化內容分析”一節已經提過,使用帶調試信息和符號表的 so 解析線上崩潰,是分析 so 崩潰的標準方式(這也是 Google 解析 so 崩潰的方式)。本文的優化方案并未修改調試信息和符號表,所以可以使用帶調試信息和符號表的 so 對崩潰堆棧進行完整的還原,解析出崩潰堆棧每個棧幀對應的源碼文件、行號和函數名等信息。業務編譯出 release 版的 so 后將相應的帶調試信息和符號表的 so 上傳到 crash 平臺即可。

6. 方案收益

優化 so 對安裝包體積和安裝后占用的本地存儲空間有直接收益,收益大小取決于原 so 冗余代碼數量和導出符號數量等具體情況,下面是部分 so 優化前后占用安裝包體積的對比:

圖片

下面是上述 so 優化前后占用本地存儲空間的對比:

圖片

7. 總結與規劃

對 so 體積進行優化不僅能夠減小安裝包體積,而且能獲得以下收益:

  • 刪除了大量的非必要導出符號從而提升了 so 的安全性。
  • 因為 .data.bss.text 等運行時占用內存的 section 減小了,所以也能減小應用運行時的內存占用。
  • 如果優化過程中減少了 so 對外依賴的符號,還可以加快 so 的加載速度。

我們對后續工作做了如下的規劃:

  • 提升編譯速度。因為使用 LTO、gc sections 等會增加編譯耗時,計劃調研 ThinLTO 等方案對編譯速度進行優化。
  • 詳細展示保留各個函數/數據的原因。
  • 進一步完善平臺優化 so 的能力。?
責任編輯:張燕妮 來源: 美團技術團隊
相關推薦

2022-06-01 09:18:37

抖音ReDex算法優化

2022-10-28 13:41:51

字節SDK監控

2023-07-19 22:17:21

Android資源優化

2022-04-28 09:36:47

Redis內存結構內存管理

2024-11-13 21:18:02

2023-10-31 12:50:35

智能優化探索

2022-08-12 12:23:28

神經網絡優化

2024-12-05 12:01:09

2017-05-18 11:43:41

Android模塊化軟件

2024-01-03 16:29:01

Agent性能優化

2022-05-07 15:51:47

Android資源文件文件名

2022-08-21 21:28:32

數據庫實踐

2024-12-26 09:27:51

2024-12-18 10:03:30

2021-12-08 10:35:04

開源監控Zabbix

2023-10-27 12:16:23

游戲發行平臺SOP

2023-06-30 13:10:54

數據聚合網關

2023-01-05 07:54:49

vivo故障定位

2017-09-08 17:25:18

Vue探索實踐

2017-09-11 16:34:00

點贊
收藏

51CTO技術棧公眾號

国产在线观看免费一区| 欧美hentaied在线观看| 日韩欧美一区二区三区| 午夜午夜精品一区二区三区文| 亚洲天堂手机版| 好吊视频一区二区三区四区| 亚洲人精品午夜在线观看| 亚洲va在线va天堂va偷拍| 啦啦啦中文在线观看日本| 26uuu国产电影一区二区| 国产精品爽爽爽爽爽爽在线观看| 2021亚洲天堂| 国产亚洲精品美女久久久久久久久久| 欧美一二三区精品| 又色又爽又高潮免费视频国产| 1区2区在线观看| 国产亚洲人成网站| 国产九色精品| 国产孕妇孕交大片孕| 一区二区三区国产在线| 欧美乱妇高清无乱码| 成人无码av片在线观看| 欧美电影在线观看免费| 欧美一区二区三区人| 国产天堂在线播放| 美女高潮视频在线看| 亚洲人成精品久久久久久| 欧美精品欧美精品| 色综合视频在线| 国产精品 欧美精品| 国产国语刺激对白av不卡| www.天天色| 欧美高清不卡| 粗暴蹂躏中文一区二区三区| 国产福利在线导航| 妖精视频一区二区三区| 亚洲电影第1页| 欧美午夜精品一区二区| 国产精品一区二区三区av| 欧美色综合天天久久综合精品| 亚洲熟妇国产熟妇肥婆| 国产探花视频在线观看| 一区二区三区四区中文字幕| 中国一区二区三区| 麻豆影视在线观看_| 中文字幕精品一区| 亚洲国产精品www| 黄色毛片在线观看| 国产欧美精品一区二区色综合朱莉| 精品欧美一区二区久久久伦| 人妻中文字幕一区| 成人aa视频在线观看| 国产青春久久久国产毛片| 高清乱码毛片入口| a在线欧美一区| 国产一级精品aaaaa看| a在线观看视频| 韩国一区二区视频| av在线不卡一区| 亚洲第一页在线观看| 国产91精品在线观看| 国产伦精品一区二区三区免 | 国产精品美女| 国产91精品久久久久久久| 久久久久久91亚洲精品中文字幕| 99精品视频免费| 91av在线免费观看视频| 国产午夜精品一区二区理论影院| 中文亚洲免费| 日韩美女免费观看| 伊人网av在线| 国产成人精品一区二区三区四区 | 亚洲成av人片乱码色午夜| 久久精品国产一区| 久久久久成人精品无码| 亚洲精品美女91| 青青草国产精品一区二区| jizz国产在线| 国产精品资源在线| 国产伦精品一区二区三区照片91| 无码国产精品96久久久久| 久久久久久久久久美女| 亚洲三级一区| 大香伊人中文字幕精品| 色妹子一区二区| 亚洲色图偷拍视频| 91免费精品国偷自产在线在线| 精品视频在线播放免| 青青草自拍偷拍| 午夜激情一区| 国产精品久久久久不卡| 精品国产乱码一区二区三| 99久久99久久免费精品蜜臀| 日本亚洲导航| 日本h片在线观看| 色哟哟一区二区在线观看| 91精品视频国产| 亚洲成aⅴ人片久久青草影院| 在线观看精品国产视频| 精品在线视频观看| 免费人成在线不卡| 国产伦精品一区二区三区四区视频 | 国产在视频一区二区三区吞精| 欧美一区二区三区色| 国产中年熟女高潮大集合| 欧美69wwwcom| 国产精品视频精品| 色综合免费视频| 亚洲色图19p| 久久久久免费精品| 91精品国产自产精品男人的天堂 | 免费在线观看视频一区| 成人情视频高清免费观看电影| 丁香在线视频| 欧美日韩国产黄| 中文字幕 欧美 日韩| 日韩国产一区二区| 日本中文字幕成人| 日本高清视频www| 中文字幕亚洲一区二区va在线| 久久国产亚洲精品无码| 亚洲1区在线| 久久精品99久久香蕉国产色戒 | 51午夜精品国产| 国产高清一区二区三区四区| 亚洲精品乱码| 国产女主播一区二区三区| 黄网站在线播放| 在线观看www91| 91网站免费入口| 亚洲一卡久久| 久草一区二区| 老司机深夜福利在线观看| 精品国产亚洲一区二区三区在线观看 | 欧洲亚洲视频| 98精品国产高清在线xxxx天堂| 精品国产无码AV| 亚洲精品亚洲人成人网| 久久99爱视频| 99欧美视频| 成人精品一区二区三区电影黑人| 69久久久久| 欧美性受极品xxxx喷水| 性欧美一区二区| 裸体一区二区| 日本不卡免费新一二三区| 久久人体大尺度| 亚洲欧洲国产伦综合| 欧美a视频在线观看| 国产精品r级在线| 国产福利视频一区| 免费的毛片视频| 久久中文娱乐网| 亚洲乱码中文字幕久久孕妇黑人| 美女av一区| 欧美有码在线观看| 日本啊v在线| 欧美午夜精品久久久久久人妖| 少妇精品一区二区三区| 乱码第一页成人| 亚洲精品中文字幕在线| 四虎国产精品永久在线国在线| 日韩一级黄色av| av中文字幕播放| 亚洲国产综合色| 瑟瑟视频在线观看| 欧美aaaaaa午夜精品| 欧美日韩视频免费在线观看| 欧美a级大片在线| 97在线观看视频| 成人一区二区不卡免费| 欧美电影一区二区三区| 久久久99精品| 久久久精品影视| 天天干天天综合| 欧美视频二区| 欧美三级网色| 久久久久亚洲精品中文字幕| 韩国精品美女www爽爽爽视频| 免费人成在线观看网站| 欧美日韩国产系列| 欧美激情精品久久| 久久久久久久久久久久久久久99 | 欧美大片免费| 在线观看精品自拍私拍| 成 人 免费 黄 色| 欧美午夜片欧美片在线观看| www.黄色com| 成人动漫一区二区三区| 精品久久久久久无码国产| 欧美成人首页| 日韩美女一区| 亚洲精品在线a| 国产成人jvid在线播放| 国产成人在线视频免费观看| 亚洲精品videossex少妇| 精品国产青草久久久久96| 亚洲午夜影视影院在线观看| 一色道久久88加勒比一| 成人午夜视频在线观看| 色片在线免费观看| 亚洲欧美日韩专区| 黄色一级视频播放| 国产精品欧美在线观看| av蓝导航精品导航| 欧美成人一二区| 欧美亚洲另类制服自拍| av网站在线看| 在线电影欧美日韩一区二区私密| 亚洲国产999| 欧美乱妇一区二区三区不卡视频| 亚洲天堂男人av| 亚洲午夜激情av| 99成人在线观看| 久久久久久久网| 国产xxxx视频| 国内精品第一页| 在线观看的毛片| 香蕉精品999视频一区二区| 免费的一级黄色片| 羞羞答答成人影院www| 欧美一区免费视频| 欧美在线关看| 国产久一道中文一区| 欧美欧美在线| 91久久精品美女| 四虎国产精品永久在线国在线| 日产精品久久久一区二区福利| 国产剧情av在线播放| 欧美国产极速在线| av在线麻豆| 久久成人在线视频| 日韩大片在线永久免费观看网站| 亚洲人午夜精品免费| 青青草超碰在线| 日韩精品小视频| 四虎精品一区二区三区| 精品久久人人做人人爽| 国精产品乱码一区一区三区四区| 欧美一区二区美女| 国产成人av免费看| 91精品在线免费观看| 国产精品久久无码一三区| 精品视频色一区| 国产九色91回来了| 欧美日韩精品系列| 91成品人影院| 欧美丰满一区二区免费视频| 国产精品一区二区av白丝下载| 7777女厕盗摄久久久| 国产精品久久久久久在线| 欧美日韩精品电影| 国产精品视频无码| 日韩一级免费一区| 亚洲国产成人在线观看| 日韩av在线网| 九色在线观看视频| 色婷婷av一区二区三区在线观看| 思思99re6国产在线播放| 久久久精品在线| 日本片在线观看| 国a精品视频大全| 在线日韩影院| 国产一区二区在线免费视频| 麻豆国产一区二区三区四区| 国产三级精品在线不卡| 一本色道久久综合亚洲精品酒店 | 成人免费毛片糖心| 国产精品卡一卡二| 免费一级片在线观看| 图片区日韩欧美亚洲| 中文人妻av久久人妻18| 欧美日韩在线三区| 亚洲黄色在线播放| 亚洲社区在线观看| 黄色成人在线| 97色在线视频观看| 国产91在线播放精品| 97久久夜色精品国产九色| 天天躁日日躁狠狠躁欧美| 色综合视频二区偷拍在线 | 亚洲美免无码中文字幕在线| 久久久噜噜噜| 99精品视频国产| 99精品国产99久久久久久白柏| 色一情一交一乱一区二区三区| 中文字幕亚洲电影| 日韩免费黄色片| 欧美日韩精品电影| 天天操天天干天天操| 日韩中文在线中文网在线观看| 直接在线观看的三级网址| 欧美与欧洲交xxxx免费观看| 99tv成人影院| 免费在线观看91| 91成人精品视频| 99精品免费在线观看| 国产麻豆午夜三级精品| 亚洲码无人客一区二区三区| 亚洲精品久久嫩草网站秘色| 一级黄色av片| 亚洲成av人影院在线观看| jizzjizz在线观看| 欧美亚洲在线播放| 久久69av| 亚洲欧洲精品一区二区三区波多野1战4 | 国产欧美一区视频| 国产午夜免费视频| 欧美一区二区三区在| 每日更新在线观看av| 欧美激情一二区| 欧美成人毛片| 台湾成人av| 亚洲综合国产| 欧美肉大捧一进一出免费视频| 亚洲另类在线一区| 在线观看色网站| 在线日韩中文字幕| 成人性生活视频| 国产中文一区二区| 好看的日韩av电影| www.成人黄色| 中国色在线观看另类| 免费黄色av片| 日韩精品免费在线播放| 青春草在线免费视频| 亚洲999一在线观看www| 色乱码一区二区三区网站| 欧美激情精品久久久久久小说| 99re成人精品视频| 在线观看中文字幕视频| 亚洲精品动漫久久久久| 黑人极品ⅴideos精品欧美棵| 亚洲va欧美va在线观看| 亚洲字幕久久| 五月天开心婷婷| 国产精品乱码人人做人人爱| 中文字幕在线观看免费| 国产亚洲一级高清| 福利精品一区| 性欧美大战久久久久久久免费观看| 久热re这里精品视频在线6| 亚洲天堂网一区二区| 欧美性少妇18aaaa视频| 美女毛片在线看| 国产精品美女视频网站| 成人网18免费网站| 性欧美videossex精品| 国产精品久久毛片av大全日韩| 糖心vlog精品一区二区| 日韩在线观看精品| 999色成人| 日韩极品视频在线观看| 成人深夜福利app| 国产精品99精品| 日韩国产欧美区| 国产精品久久久久av电视剧| 视频一区二区三| 精品无码三级在线观看视频| 午夜免费激情视频| 亚洲大胆美女视频| sis001欧美| 亚洲精品视频一二三| 激情五月激情综合网| 国产一级免费观看| 日韩精品免费观看| 日本成人一区二区| av在线免费观看国产| 26uuu另类欧美| 亚洲一区二区人妻| 久久91亚洲精品中文字幕奶水| 欧美激情极品| 国产九九在线视频| 亚洲综合色噜噜狠狠| 激情在线视频| 成人国产精品av| 在线日本高清免费不卡| 欧美日韩高清丝袜| 91精品啪在线观看国产60岁| 黑森林国产精品av| 天堂av一区二区| 东方aⅴ免费观看久久av| 日日夜夜操视频| 欧美精品免费看| 天海翼精品一区二区三区| 久久这里只精品| 亚洲国产精品视频| 91在线品视觉盛宴免费| 懂色av一区二区三区在线播放| 久久精品官网| 唐朝av高清盛宴| 亚洲人午夜精品| 综合久久成人| www.com黄色片| 日韩欧美国产成人| 日日夜夜天天综合入口| 亚洲精品一区二区三| 不卡视频在线观看| 国产一区二区三区视频免费观看| 7777精品视频|