1.1 一句話解釋包體積是什麼?
包體積主要指的是應用安裝包大小的體積,比如 App Store 裡的安裝包顯示的安裝大小。
1.2 為什麼要優化包體積?
随着應用的能力更新叠代,應用安裝包體積将逐步增大,用戶下載應用消耗流量産生資費進一步增長,用戶下載意願會相對下降;另一方面,随着包體積增大,安裝應用的時間會相對變長,影響用戶使用感受;對于ROM較小的低端手機,應用解壓後内存占用更大,部分手機管家會提示内存不足提示卸載,直接影響用戶使用。
1.3 特效側在抖音裡的包體積貢獻
抖音目前由多條業務線組成,每條業務線都類似中台的角色,特效中台是抖音其中一環;目前,特效由 effect 和 lab 聚合為EffectSDK,作為一條獨立業務線結算包體積在抖音中的占比。
1.4 特效側的包體積組成
EffectSDK 的包體積由兩方面組成:二進制文件(即可執行文件)、其他資源文件(圖片、配置文件等)。二進制文件主要是由代碼生成的可執行文件,資源文件指代的如内置的模型文件、素材文件、配置文件等。
作為中台,特效 EffectSDK 中二進制代碼占用了絕大多數體積。與抖音、頭條等應用做包體積優化思路不同,特效在資源壓縮等部分能做得比較少;由于特效是作為中台對抖音進行業務支持,通過庫的形式提供特效能力,在無用資源删除、無用代碼去除、代碼優化上有較大空間。因此,特效側性能優化主要側重于在支持多功能的基礎上盡量減小包體積,提升代碼質量,實現代碼效率與代碼體積的平衡。
2 包體積優化的背景知識
特效側在抖音裡的能力由 C 代碼編寫支撐,編譯後生成靜态庫,最後鍊接至可執行文件中。從代碼至二進制文件的過程中,由編譯器為我們做好預處理、編譯、彙編、鍊接等過程,最後 Android 端生成 ELF 格式文件,iOS 端生成 Mach-O 文件。ELF 格式的文件有四種,包括可重定位文件(Relocatable File)、可執行文件(Executable File)、共享目标文件(Shared Object File)、核心轉儲文件(Core Dump File),其中,共享目标文件,即 xxx.so 文件,包含可在兩種上下文中鍊接的代碼和數據,鍊接編輯器可以将它和其它可重定位文件和共享目标文件一起處理,生成另外一個目标文件;另外,動态鍊接器(Dynamic Linker)可能将它與某個可執行文件以及其它共享目标一起組合,創建進程映像。特效側即以共享目标文件(libeffect.so)的形式做好抖音特效拍攝能力支撐。
由于ELF文件參與程序的鍊接與執行,通常有兩種視圖方式:一種是鍊接視圖,一種是執行視圖(下述左圖);編譯器和鍊接器會按照鍊接視圖,以節區(section)為單位,按節區頭部表(section header table)形成節區的集合;加載器将按照執行視圖,将文件以段(segment)為單位,按照程序頭部表(program header table)将其視為段的集合。通常,可重定位文件(xxx.o)将包含節區頭部表,可執行文件(xxx.exe)将包含程序頭部表,共享目标文件(xxx.so)兩者都包含。
下面是使用 binutils 工具查看 effect_sdk.so 中的 section 部分信息:
$ greadelf -h libeffect_sdk.so
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: AArch64
Version: 0x1
Entry point address: 0x0
Start of program headers: 64 (bytes into file)
Start of section headers: 22954168 (bytes into file)
FLAGS: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
number of program headers: 8
Size of section headers: 64 (bytes)
Number of section headers: 29
Section header string table index: 28
$ greadelf -S libeffect_sdk.so
There are 29 section headers, starting at offset 0x15e40b8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note.androi[...] NOTE 0000000000000200 00000200
0000000000000098 0000000000000000 A 0 0 4
[ 2] .note.gnu.bu[...] NOTE 0000000000000298 00000298
0000000000000024 0000000000000000 A 0 0 4
[ 3] .dynsym DYNSYM 00000000000002c0 000002c0
00000000000107e8 0000000000000018 A 4 1 8
[ 4] .dynstr STRTAB 0000000000010aa8 00010aa8
000000000001b0f9 0000000000000000 A 0 0 1
[ 5] .gnu.hash GNU_HASH 000000000002bba8 0002bba8
000000000000347c 0000000000000000 A 3 0 8
[ 6] .hash HASH 000000000002f028 0002f028
0000000000004c18 0000000000000004 A 3 0 8
... ...
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
p (processor specific)
通常每個節區(section)負責不同的功能,存儲在不同的位置,節區的大小是代碼編譯後大小的反饋。說到底,特效側最終的包體積由 section 和 headers 的大小共同決定。優化包體積,即是優化代碼的編寫效率、編譯方式,減少各個節區的大小。
int gInitVar = 24; //-- .data section
int gUninitedVar; //-- .bss section
void func(int i)
{
printf("%d\n", i); //-- .text section
}
int main(void)
{
static int sVar = 23; //-- .data section
static int sVar1; //-- .bss section
int a = 1;
int b;
func(sVar sVar1 a b); //-- .text section
return 0;
}
3 包體積優化技巧
在了解了基礎的包體積組成後,我們可以針對性的對編譯選項、代碼進行調整,以優化包體積。
iOS/Android 均可以通過優化編譯選項來優化代碼體積。整理了常用的一些。
3.1 編譯優化
3.1.1 使用 Oz 替代 Os
編譯選項
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Oz")
3.1.2 減小 unused code 的體積
編譯選項
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ffunction-sections -fdata-sections -fvisibility=hidden -g")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ffunction-sections -fdata-sections -fvisibility=hidden -g")
鍊接選項
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--gc-sections")
3.1.3 開啟鍊接優化
編譯選項
鍊接選項
$ clang -Os -fuse-ld=lld -flto test.c
ld.lld: error: -plugin-opt=Os: number expected, but got 's'
clang-9: error: linker command failed with exit code 1 (use -v to see invocation)
$ clang -Oz -fuse-ld=lld -flto test.c
ld.lld: error: -plugin-opt=Oz: number expected, but got 'z'
clang-9: error: linker command failed with exit code 1 (use -v to see invocation)
if (NOT DEFINED ENV{DISABLE_LTO})
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -flto -fPIC")
endif()
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl, --gc-sections -fuse-ld=gold -Wl, --icf=safe -O2 -flto")
if (NOT DEFINED ENV{DISABLE_LTO})
message(STATUS "DISABLE_LTO=$ENV{DISABLE_LTO} LTO enabled")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fuse-ld=gold -Wl, --icf=safe -O2 -flto")
else()
message(STATUS "DISABLE_LTO=$ENV{DISABLE_LTO} LTO disabled")
endif()
3.1.4 關閉 exception 和 rtti
編譯選項
if(!running)
{
// throw std::runtime_error("runtime error") // 不可用
errCode = getRuntimeError();
return errCode;
}
DerivedTarget &target = getTargetPtr();
// dynamic_cast<BasicTarget *>(target.get())->fun(); // 不可再用
static_cast<BasicTarget *>(target.get())->fun();
3.1.5 自動删除引入的靜态庫中的符号
鍊接選項
(如果 iOS 要往庫裡引入符号,需要手動開啟-reexport-l$(UR_LIB)選項)
if ("${CMAKE_BUILD_TYPE}" STREQUAL "Release" AND ANDROID)
foreach(LIB ${LINK_LIB_LIST})
set(CMAKE_SHARED_LINKER_FLAGS "{CMAKE_SHARED_LINKER_FLAGS} -Wl,--exclude-libs,lib{LIB}.a")
endforeach()
endif()
目前特效在 Android 端均采用了這個選項。
3.1.6 減少符号表
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ffunction-sections -fdata-sections -fvisibility=hidden -g")
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ffunction-sections -fdata-sections -fvisibility=hidden -g")
目前特效側均使用-fvisibility=hidden
3.1.7 動态鍊接c
動态鍊接 libstdc 庫,避免增大庫文件。
3.2 代碼優化
一句話總結:代碼量越少,包體積越小,從經驗來看100行代碼大概占用1~5K體積;超出這個行/體積 比,代碼肯定有問題。
3.2.1 不要有無效的判斷邏輯( if...else... )
可以采用表驅動的方法實現 if else ,減少不必要的代碼引用。
3.2.2 減少模闆展開、宏展開
模闆展開非常占據體積,尤其是對于同一種形式的代碼,template 會擴充為多個不同的類。此時最好把公共的部分提取出來,聲明為一個 static method。
如下面的綁定變量的方法:
template <typename T>
static void bindArgs(const Demo& d, T func)
{
auto m = createFun(func);
m->mName = d.name
for (auto i = 0; i < m->getArgc(); i)
{
if (i < d.args.size())
m->mArgTypes[i].name = d.args[i];
}
}
template <typename T>
static void bindArgs(const Demo& d, T func, const Var& arg1)
{
auto m = createFun(func);
if (!m)
return;
m->mValues.push_back(arg1);
for (auto i = 0; i < m->getArgc(); i)
{
if (i < d.args.size())
m->mArgTypes[i].name = d.args[i];
}
}
// static void bindArgs(const Demo& d, T func, const Var& arg1, const Var& arg2)
// {
可修改為:
// bindArgs 提取出來
static void bindArgs(const Demo& d, Fun* m)
{
for (auto i = 0; i < m->getArgc(); i)
{
if (i < d.args.size())
m->mArgTypes[i].name = d.args[i];
}
}
template <typename T>
static void bindArgs(const Demo& d, T func)
{
auto m = createFun(func);
m->mName = d.name;
bindArgs(d, m);
}
template <typename T>
static void bindArgs(const Demo& d, T func, const Var& arg1)
{
auto m = createFun(func);
if (!m)
return;
m->mValues.push_back(arg1);
bindArgs(d, m);
}
3.2.3 避免不必要的 stl/std 使用
比如,部分回調可以使用函數指針:std::function <>作為一個 class ,它的體積成本必然比 void * fun 這樣一個函數指針要來的高;
// using FunInstantiate = std::function<FunInterface*()>; // 不再使用
using FunInstantiate = FunInterface*(*)();
比如,常量字符串引用時可以采用 const char* 類型,避免編譯器調用隐式拷貝構造;
// void DemoClass::fun(const std::string &name, const DemoPtr &demoPtr) // 不再使用
void DemoClass::fun(const char* name, const DmoePtr &demoPtr)
{
//...
}
3.2.4 頭文件不要出現 const、static 變量的定義
頭文件中 const / static 型的變量,會被引入至對應的 cpp 文件,相當于每一份.o 都引入了一長串常量字符串。
3.2.5 不要出現大的數組
大的數組會占用數組大小的體積。
3.2.6 減少不必要的虛基類/虛函數
// class Child : virtual public Parent // 不再使用
class Child : public Parent
{
//...
}
4.1 為什麼要做包體積監測工具
抖音每個版本都會有非常多的新能力更新換代,每次更新每個需求均會導緻包體積的變更。為了能更好的監測包體積的變化、确認包體積增長的原因,提升 ROI ,引入包體積監測工具,更直觀的确認包體積增長原因,攔截異常增長,輸出每個每個需求帶來的包體積增長大小、包體積增長原因,及時給出包體積告警、定位異常增量 case ,減緩包體積增長,推動業務優化。
4.2 如何進行包體積監測
特效側目前使用的包體積監測工具來源于 google 的開源二進制文件體積分析工具 bloaty ,用于分析二進制文件(xxx.exe, xxx.bin)、共享目标文件(xxx.so)、對象文件(xxx.o)和靜态庫(xxx.a),支持ELF\Mach-O\WebAssembly 格式。它能梳理出文件中各部分的體積組成,拆分出各個 section 大小,結合symbol信息,反推出各方法、源文件的包體積大小。
以特效側 libeffect_sdk.so 為例,對 .so 文件進行組件單元、源文件分析,截取部分輸出結果:
FILE SIZE
--------------
10.3% 2.25Mi [section .rela.dyn]
7.2% 1.58Mi [section .rodata]
7.2% 1.57Mi Bindings.cpp
3.9% 877Ki [section .data.rel.ro]
2.0% 445Ki [section .text]
1.9% 418Ki [section .gcc_except_table]
1.0% 213Ki base/EffectManager.cpp
0.7% 149Ki bef_info_sticker_api.cpp
0.6% 140Ki base/RenderManager.cpp
0.6% 138Ki Runtime/Engine/Foundation/Bindings.cpp
...
利用上述工具,即可較為清晰的定位各文件帶來的包體積增長。
4.2.1 包體積監控工具工作流程
包體積監測工具是當前特效需求上車前必過的一環。所有需求在 MR(merge request)提出、CI 打包完成後都會經過包體積的檢查,僅包體積增量符合預期的需求允許跟版合入,所有包體積增量與需求一一對應,記錄在案。
4.2.2 包體積監測工具的分析能力
包體積分析工具支持單個文件分析和版本叠代對比分析。
對于單文件分析,由于特效側主要通過 .so 文件進行交付,在每個 MR 打包完成後,工具将自動獲取對應的 .so 文件和 .so.symbol 文件後,對庫文件的包體積組成、包體積來源進行分析,輸出所有方法函數、節區(section)、編譯單元(xxx.cpp)帶來的包體積大小,确認大小後通過關鍵字匹配确認包體積的增量來源模塊,給出最後的各模塊單元、編譯單元的包體積 profile 。
另一方面,由于特效側能力總是通過需求更新叠代的,每次有實質性的需求提交時,将會對比上一版本與當前版本的包體積差異,做好每個版本需求帶來的增量來源記錄。當版本比對結果帶來的增量超過預期值時,将調起通訊 api ,将包體積超标信息發出進行報警。
4.2.3 包體積數據記錄本
所有需求的包體積增量将記錄在包體積記錄本中:當服務收到需求事件時,将調用 bits/meego 接口,請求需求信息和包大小預設 exp_pack_size 增量寫入 mr_pkg_size 表;等到本地出包完成後,實際的包大小增量 real_pack_size 将被記錄入 mr_pkg_size 表,并将預期值與實際增量進行對比。
最終,所有的包體積增量與曆史的需求增量來源被記錄在案,并通過表查詢接口,在網頁端可根據需求名 / 時間段 / 分支名 / commit id 等條件按圖索骥,确認包體積增長來源。
5 總結
經過上述代碼體積優化積累、實時體積監控、需求增量落實到人三位一體,控制特效側包體積有序增長,提升代碼效能。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!