tft每日頭條

 > 生活

 > 界面優化4個要點

界面優化4個要點

生活 更新时间:2024-12-26 11:22:36
1 特效包體積之于抖音

1.1 一句話解釋包體積是什麼?

包體積主要指的是應用安裝包大小的體積,比如 App Store 裡的安裝包顯示的安裝大小。

1.2 為什麼要優化包體積?

随着應用的能力更新叠代,應用安裝包體積将逐步增大,用戶下載應用消耗流量産生資費進一步增長,用戶下載意願會相對下降;另一方面,随着包體積增大,安裝應用的時間會相對變長,影響用戶使用感受;對于ROM較小的低端手機,應用解壓後内存占用更大,部分手機管家會提示内存不足提示卸載,直接影響用戶使用。

1.3 特效側在抖音裡的包體積貢獻

抖音目前由多條業務線組成,每條業務線都類似中台的角色,特效中台是抖音其中一環;目前,特效由 effect 和 lab 聚合為EffectSDK,作為一條獨立業務線結算包體積在抖音中的占比。

1.4 特效側的包體積組成

EffectSDK 的包體積由兩方面組成:二進制文件(即可執行文件)、其他資源文件(圖片、配置文件等)。二進制文件主要是由代碼生成的可執行文件,資源文件指代的如内置的模型文件、素材文件、配置文件等。

作為中台,特效 EffectSDK 中二進制代碼占用了絕大多數體積。與抖音、頭條等應用做包體積優化思路不同,特效在資源壓縮等部分能做得比較少;由于特效是作為中台對抖音進行業務支持,通過庫的形式提供特效能力,在無用資源删除、無用代碼去除、代碼優化上有較大空間。因此,特效側性能優化主要側重于在支持多功能的基礎上盡量減小包體積,提升代碼質量,實現代碼效率與代碼體積的平衡。

界面優化4個要點(特效側用戶體驗優化實戰)1

2 包體積優化的背景知識

特效側在抖音裡的能力由 C 代碼編寫支撐,編譯後生成靜态庫,最後鍊接至可執行文件中。從代碼至二進制文件的過程中,由編譯器為我們做好預處理、編譯、彙編、鍊接等過程,最後 Android 端生成 ELF 格式文件,iOS 端生成 Mach-O 文件。ELF 格式的文件有四種,包括可重定位文件(Relocatable File)、可執行文件(Executable File)、共享目标文件(Shared Object File)、核心轉儲文件(Core Dump File),其中,共享目标文件,即 xxx.so 文件,包含可在兩種上下文中鍊接的代碼和數據,鍊接編輯器可以将它和其它可重定位文件和共享目标文件一起處理,生成另外一個目标文件;另外,動态鍊接器(Dynamic Linker)可能将它與某個可執行文件以及其它共享目标一起組合,創建進程映像。特效側即以共享目标文件(libeffect.so)的形式做好抖音特效拍攝能力支撐。

界面優化4個要點(特效側用戶體驗優化實戰)2

界面優化4個要點(特效側用戶體驗優化實戰)3

界面優化4個要點(特效側用戶體驗優化實戰)4

由于ELF文件參與程序的鍊接與執行,通常有兩種視圖方式:一種是鍊接視圖,一種是執行視圖(下述左圖);編譯器和鍊接器會按照鍊接視圖,以節區(section)為單位,按節區頭部表(section header table)形成節區的集合;加載器将按照執行視圖,将文件以段(segment)為單位,按照程序頭部表(program header table)将其視為段的集合。通常,可重定位文件(xxx.o)将包含節區頭部表,可執行文件(xxx.exe)将包含程序頭部表,共享目标文件(xxx.so)兩者都包含。

界面優化4個要點(特效側用戶體驗優化實戰)5

界面優化4個要點(特效側用戶體驗優化實戰)6

下面是使用 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; }

界面優化4個要點(特效側用戶體驗優化實戰)7

3 包體積優化技巧

在了解了基礎的包體積組成後,我們可以針對性的對編譯選項、代碼進行調整,以優化包體積。

iOS/Android 均可以通過優化編譯選項來優化代碼體積。整理了常用的一些。

3.1 編譯優化

3.1.1 使用 Oz 替代 Os

編譯選項

    • 用-Oz替代-Os
    • 示例:

set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Oz")

3.1.2 減小 unused code 的體積

編譯選項

    • -ffunction-sections
    • 把每個function放到自己的 COMDAT 段(COMDAT 段被多個目标文件所定義的輔助段。該段的作用是将在多個已編譯模塊中重複的代碼和數據的邏輯塊組合在一起。COMDAT 在 C 的虛函數表和模闆的編譯鍊接中,起着非常重要的作用。)
    • 支持 Linux/OS X,不支持windows
    • -fdata-sections
    • 為源文件中每個變量啟用一個 elf section 的生成
  • 示例:

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")

鍊接選項

    • -Wl, --gc-sections( Android 端)
    • 當編譯器選擇用-ffunction-sections, -fdata-sections編譯文件時,靜态的庫體積将增大,此時調用-Wl, --gc-sections,能消除dead段沒有用到的code和data的體積。
    • -dead_strip( iOS 端)
  • 示例:

set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--gc-sections")

3.1.3 開啟鍊接優化

編譯選項

    • -flto Oz

鍊接選項

    • -O3 -flto
    • lto為 link-time optimization ,在編譯和鍊接時需要同時開啟。編譯時,會将各文件寫入專有的 section ,再鍊接時将它倆視為同一單元進行轉換和優化。但有個缺點,會在一定程度上拖慢編譯速度
    • 注意:lto編譯時可以和-Oz共存,但鍊接時隻能跟O1/O2/O3共存,無法和Oz/Os共存,如果同時開啟了,将會報下面的錯誤:

$ 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

編譯選項

    • -fno-exceptions
    • 當開啟-fno-rtti開關時,将禁用 rtti 機制,減小包體積。
    • -fno-rtti
    • 當開啟-fno-exceptions 開關時,将禁用 exception 機制,減小包體積。
    • 上述兩種屬于比較激進的做法,同時也需要代碼配合,但在能保障代碼正确性和穩定性的情況下,也能較大幅度的優化包體積。目前特效側已經盡量避免不必要的 rtti 和 exception 機制。
  • 注意:缺少異常處理和 rtti ,需要 coder 能寫出更高品質的代碼。
    • -fno-excpetion需要配合一定的代碼修改:

if(!running) { // throw std::runtime_error("runtime error") // 不可用 errCode = getRuntimeError(); return errCode; }

    • -fno-rtti也需要配合一定代碼修改:

DerivedTarget &target = getTargetPtr(); // dynamic_cast<BasicTarget *>(target.get())->fun(); // 不可再用 static_cast<BasicTarget *>(target.get())->fun();

3.1.5 自動删除引入的靜态庫中的符号

鍊接選項

    • -Wl,--exclude-libs,ALL(Android端)
    • 删除庫"ALL"裡自動導出的符号(這裡ALL替換成不需要的庫名,比如--exclude-libs lib,lib,...)
  • 注意:iOS 不支持這個鍊接選項,因為 macOS 将--exclude-libs作為默認選項

(如果 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 減少符号表

  • -fvisibility=hidden
    • 可隐藏符号的可見性,防止符号沖突,同時減小包體積。
  • 注意:出錯時上層可能無法第一時間定位問題

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 包體積監測工具

4.1 為什麼要做包體積監測工具

抖音每個版本都會有非常多的新能力更新換代,每次更新每個需求均會導緻包體積的變更。為了能更好的監測包體積的變化、确認包體積增長的原因,提升 ROI ,引入包體積監測工具,更直觀的确認包體積增長原因,攔截異常增長,輸出每個每個需求帶來的包體積增長大小、包體積增長原因,及時給出包體積告警、定位異常增量 case ,減緩包體積增長,推動業務優化。

界面優化4個要點(特效側用戶體驗優化實戰)8

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個要點(特效側用戶體驗優化實戰)9

4.2.2 包體積監測工具的分析能力

包體積分析工具支持單個文件分析和版本叠代對比分析。

對于單文件分析,由于特效側主要通過 .so 文件進行交付,在每個 MR 打包完成後,工具将自動獲取對應的 .so 文件和 .so.symbol 文件後,對庫文件的包體積組成、包體積來源進行分析,輸出所有方法函數、節區(section)、編譯單元(xxx.cpp)帶來的包體積大小,确認大小後通過關鍵字匹配确認包體積的增量來源模塊,給出最後的各模塊單元、編譯單元的包體積 profile 。

另一方面,由于特效側能力總是通過需求更新叠代的,每次有實質性的需求提交時,将會對比上一版本與當前版本的包體積差異,做好每個版本需求帶來的增量來源記錄。當版本比對結果帶來的增量超過預期值時,将調起通訊 api ,将包體積超标信息發出進行報警。

界面優化4個要點(特效側用戶體驗優化實戰)10

界面優化4個要點(特效側用戶體驗優化實戰)11

4.2.3 包體積數據記錄本

所有需求的包體積增量将記錄在包體積記錄本中:當服務收到需求事件時,将調用 bits/meego 接口,請求需求信息和包大小預設 exp_pack_size 增量寫入 mr_pkg_size 表;等到本地出包完成後,實際的包大小增量 real_pack_size 将被記錄入 mr_pkg_size 表,并将預期值與實際增量進行對比。

最終,所有的包體積增量與曆史的需求增量來源被記錄在案,并通過表查詢接口,在網頁端可根據需求名 / 時間段 / 分支名 / commit id 等條件按圖索骥,确認包體積增長來源。

界面優化4個要點(特效側用戶體驗優化實戰)12

5 總結

經過上述代碼體積優化積累、實時體積監控、需求增量落實到人三位一體,控制特效側包體積有序增長,提升代碼效能。

,

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关生活资讯推荐

热门生活资讯推荐

网友关注

Copyright 2023-2024 - www.tftnews.com All Rights Reserved