1 特效包体积之于抖音

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-hlibeffect_sdk.so
ELFHeader:
Magic:7f454c46020101000000000000000000
Class:ELF64
Data:2'scomplement,littleendian
Version:1(current)
OS/ABI:UNIX-SystemV
ABIVersion:0
Type:DYN(Sharedobjectfile)
Machine:AArch64
Version:0x1
Entrypointaddress:0x0
Startofprogramheaders:64(bytesintofile)
Startofsectionheaders:22954168(bytesintofile)
Flags:0x0
Sizeofthisheader:64(bytes)
Sizeofprogramheaders:56(bytes)
Numberofprogramheaders:8
Sizeofsectionheaders:64(bytes)
Numberofsectionheaders:29
Sectionheaderstringtableindex:28
$greadelf-Slibeffect_sdk.so
Thereare29sectionheaders,startingatoffset0x15e40b8:
SectionHeaders:
[Nr]NameTypeAddressOffset
SizeEntSizeFlagsLinkInfoAlign
[0]NULL000000000000000000000000
00000000000000000000000000000000000
[1].note.androi[...]NOTE000000000000020000000200
00000000000000980000000000000000A004
[2].note.gnu.bu[...]NOTE000000000000029800000298
00000000000000240000000000000000A004
[3].dynsymDYNSYM00000000000002c0000002c0
00000000000107e80000000000000018A418
[4].dynstrSTRTAB0000000000010aa800010aa8
000000000001b0f90000000000000000A001
[5].gnu.hashGNU_HASH000000000002bba80002bba8
000000000000347c0000000000000000A308
[6].hashHASH000000000002f0280002f028
0000000000004c180000000000000004A308
......
KeytoFlags:
W(write),A(alloc),X(execute),M(merge),S(strings),I(info),
L(linkorder),O(extraOSprocessingrequired),G(group),T(TLS),
C(compressed),x(unknown),o(OSspecific),E(exclude),
p(processorspecific)

通常每个节区(section)担任不同的功用,存储在不同的位置,节区的巨细是代码编译后巨细的反馈。说到底,特效侧最终的包体积由 section 和 headers 的巨细一起决定。优化包体积,便是优化代码的编写功率、编译办法,削减各个节区的巨细。

intgInitVar=24;//--.datasection
intgUninitedVar;//--.bsssection
voidfunc(inti)
{
printf("%d\n",i);//--.textsection
}
intmain(void)
{
staticintsVar=23;//--.datasection
staticintsVar1;//--.bsssection
inta=1;
intb;
func(sVar+sVar1+a+b);//--.textsection
return0;
}

特效侧用户体验优化实战 —— 包体积篇

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-fltotest.c
      ld.lld:error:-plugin-opt=Os:numberexpected,butgot's'
      clang-9:error:linkercommandfailedwithexitcode1(use-vtoseeinvocation)
      $clang-Oz-fuse-ld=lld-fltotest.c
      ld.lld:error:-plugin-opt=Oz:numberexpected,butgot'z'
      clang-9:error:linkercommandfailedwithexitcode1(use-vtoseeinvocation)
      
  • 示例:

    • if(NOTDEFINEDENV{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(NOTDEFINEDENV{DISABLE_LTO})
      message(STATUS"DISABLE_LTO=$ENV{DISABLE_LTO}+++LTOenabled")
      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}+++LTOdisabled")
      endif()
      

3.1.4 关闭 exception 和 rtti

  • 编译选项
    • -fno-exceptions
    • 当敞开-fno-rtti开关时,将禁用 rtti 机制,减小包体积。
    • -fno-rtti
    • 当敞开-fno-exceptions 开关时,将禁用 exception 机制,减小包体积。
    • 上述两种归于比较激进的做法,一起也需要代码合作,但在能保障代码正确性和稳定性的情况下,也能较大起伏的优化包体积。现在特效侧现已尽量防止不必要的 rtti 和 exception 机制。
  • 留意:缺少反常处理和 rtti ,需要 coder 能写出更高品质的代码。
    • -fno-excpetion需要合作一定的代码修正:
    • if(!running)
      {
      //throwstd::runtime_error("runtimeerror")//不可用
      errCode=getRuntimeError();
      returnerrCode;
      }
      
    • -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"ANDANDROID)
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<typenameT>
staticvoidbindArgs(constDemo&d,Tfunc)
{
autom=createFun(func);
m->mName=d.name
for(autoi=0;i<m->getArgc();++i)
{
if(i<d.args.size())
m->mArgTypes[i].name=d.args[i];
}
}
template<typenameT>
staticvoidbindArgs(constDemo&d,Tfunc,constVar&arg1)
{
autom=createFun(func);
if(!m)
return;
m->mValues.push_back(arg1);
for(autoi=0;i<m->getArgc();++i)
{
if(i<d.args.size())
m->mArgTypes[i].name=d.args[i];
}
}

//staticvoidbindArgs(constDemo&d,Tfunc,constVar&arg1,constVar&arg2)
//{

可修正为:

//bindArgs提取出来
staticvoidbindArgs(constDemo&d,Fun*m)
{
for(autoi=0;i<m->getArgc();++i)
{
if(i<d.args.size())
m->mArgTypes[i].name=d.args[i];
}
}
template<typenameT>
staticvoidbindArgs(constDemo&d,Tfunc)
{
autom=createFun(func);
m->mName=d.name;
bindArgs(d,m);
}
template<typenameT>
staticvoidbindArgs(constDemo&d,Tfunc,constVar&arg1)
{
autom=createFun(func);
if(!m)
return;
m->mValues.push_back(arg1);
bindArgs(d,m);
}

3.2.3 防止不必要的 stl/std 运用

比方,部分回调能够运用函数指针:std::function <>作为一个 class ,它的体积成本必定比 void * fun 这样一个函数指针要来的高;

//usingFunInstantiate=std::function<FunInterface*()>;//不再运用
usingFunInstantiate=FunInterface*(*)();

比方,常量字符串引用时能够选用 const char* 类型,防止编译器调用隐式复制构造;

//voidDemoClass::fun(conststd::string&name,constDemoPtr&demoPtr)//不再运用
voidDemoClass::fun(constchar*name,constDmoePtr&demoPtr)
{
//...
}

3.2.4 头文件不要呈现 const、static 变量的定义

头文件中 const / static 型的变量,会被引进至对应的 cpp 文件,相当于每一份.o 都引进了一长串常量字符串。

3.2.5 不要呈现大的数组

大的数组会占用数组巨细的体积。

3.2.6 削减不必要的虚基类/虚函数

//classChild:virtualpublicParent//不再运用
classChild:publicParent
{
//...
}

4 包体积监测东西

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 文件进行组件单元、源文件剖析,截取部分输出成果:

FILESIZE
--------------
10.3%2.25Mi[section.rela.dyn]
7.2%1.58Mi[section.rodata]
7.2%1.57MiBindings.cpp
3.9%877Ki[section.data.rel.ro]
2.0%445Ki[section.text]
1.9%418Ki[section.gcc_except_table]
1.0%213Kibase/EffectManager.cpp
0.7%149Kibef_info_sticker_api.cpp
0.6%140Kibase/RenderManager.cpp
0.6%138KiRuntime/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 总结

通过上述代码体积优化积累、实时体积监控、需求增量落实到人三位一体,控制特效侧包体积有序增加,提高代码效能。

6 关于咱们

特效团队,旨在通过特效平台连接虚拟与现实,通过探究更多的特效和交互办法,激发内容发明,丰厚用户日子;现在,特效已深度支撑着抖音、剪映、西瓜、头条、轻颜、Faceu、飞书等产品;掩盖中/长/短视频创作、直播、社区、广告等行业范畴。咱们是特效用户体会优化团队,归于特效团队下的子团队,担任支撑解决特效事务场景中遇到的功能问题,通过持续优化算法、烘托计划,压榨设备功能释放,供给最极致的用户运用体会。欢迎扫描下方二维码进行简历投递,参加咱们!

特效功能优化工程师-抖音:北京/上海/杭州/深圳职位敞开

特效侧用户体验优化实战 —— 包体积篇

图形图像研制工程师-功能优化方向:北京/上海/杭州/深圳职位敞开

特效侧用户体验优化实战 —— 包体积篇