1.1一句话解释包体积是什么?
包体积主要指的是应用安装包大小的体积,比如AppStore里的安装包显示的安装大小。
1.2为什么要优化包体积?
随着应用的能力更新迭代,应用安装包体积将逐步增大,用户下载应用消耗流量产生资费进一步增长,用户下载意愿会相对下降;另一方面,随着包体积增大,安装应用的时间会相对变长,影响用户使用感受;对于ROM较小的低端手机,应用解压后内存占用更大,部分手机管家会提示内存不足提示卸载,直接影响用户使用。
1.3特效侧在抖音里的包体积贡献
抖音目前由多条业务线组成,每条业务线都类似中台的角色,特效中台是抖音其中一环;目前,特效由effect和lab聚合为EffectSDK,作为一条独立业务线结算包体积在抖音中的占比。
1.4特效侧的包体积组成
EffectSDK的包体积由两方面组成:二进制文件(即可执行文件)、其他资源文件(图片、配置文件等)。二进制文件主要是由代码生成的可执行文件,资源文件指代的如内置的模型文件、素材文件、配置文件等。

特效侧在抖音里的能力由C++代码编写支撑,编译后生成静态库,最后链接至可执行文件中。从代码至二进制文件的过程中,由编译器为我们做好预处理、编译、汇编、链接等过程,最后Android端生成ELF格式文件,iOS端生成Mach-O文件。ELF格式的文件有四种,包括可重定位文件(RelocatableFile)、可执行文件(ExecutableFile)、共享目标文件(SharedObjectFile)、核心转储文件(CoreDumpFile),其中,共享目标文件,即文件,包含可在两种上下文中链接的代码和数据,链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理,生成另外一个目标文件;另外,动态链接器(DynamicLinker)可能将它与某个可执行文件以及其它共享目标一起组合,创建进程映像。特效侧即以共享目标文件()的形式做好抖音特效拍摄能力支撑。



由于ELF文件参与程序的链接与执行,通常有两种视图方式:一种是链接视图,一种是执行视图(下述左图);编译器和链接器会按照链接视图,以节区(section)为单位,按节区头部表(sectionheadertable)形成节区的集合;加载器将按照执行视图,将文件以段(segment)为单位,按照程序头部表(programheadertable)将其视为段的集合。通常,可重定位文件()将包含节区头部表,可执行文件()将包含程序头部表,共享目标文件()两者都包含。


下面是使用binutils工具查看effect_中的section部分信息:
$greadelf-hlibeffect_:Magic:7f454c46020101000000000000000000Class:ELF64Data:2'scomplement,littleianVersion:1(current)OS/ABI:UNIX-SystemVABIVersion:0Type:DYN(Sharedobjectfile)Machine:AArch64Version:0x1Entrypointaddress:0x0Startofprogramheaders:64(bytesintofile)Startofsectionheaders:22954168(bytesintofile)Flags:0x0Sizeofthisheader:64(bytes)Sizeofprogramheaders:56(bytes)Numberofprogramheaders:8Sizeofsectionheaders:64(bytes)Numberofsectionheaders:29Sectionheaderstringtableindex:28$greadelf-Slibeffect_,startingatoffset0x15e40b8:SectionHeaders:[Nr]NameTypeAddressOffsetSizeEntSizeFlagsLinkInfoAlign[0]NULL00000000000000000000000000000000000000000000000000000000000[1].[]NOTE0000000000000200000002000000000000000090000000A004[2].[]NOTE000000000000029000000000000020000000A004[3].dynsymDYNSYM00000000000002c0000002c000000000000107e0000018A418[4].dynstrSTRTAB0000000000010aa800010aa001b0f90000000000000000A001[5]._HASH000000000002bba80002bba000347c0000000000000000A308[6].hashHASH000000000002f0280002f020004c0000004A308KeytoFlags: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)
intgInitVar=24;//--.datasectionintgUninitedVar;//--.bsssectionvoidfunc(inti){printf("%d\n",i);//--.textsection}intmain(void){staticintsVar=23;//--.datasectionstaticintsVar1;//--.bsssectioninta=1;intb;func(sVar+sVar1+a+b);//--.textsectionreturn0;}
在了解了基础的包体积组成后,我们可以针对性的对编译选项、代码进行调整,以优化包体积。
iOS/Android均可以通过优化编译选项来优化代码体积。整理了常用的一些。
3.1编译优化
3.1.1使用Oz替代Os
编译选项
用-Oz替代-Os
示例:
set(CMAKE_CXX_FLAGS_RELEASE"${CMAKE_CXX_FLAGS_RELEASE}-Oz")3.1.2减小unusedcode的体积
编译选项
-ffunction-sections
把每个function放到自己的COMDAT段(COMDAT段被多个目标文件所定义的辅助段。该段的作用是将在多个已编译模块中重复的代码和数据的逻辑块组合在一起。COMDAT在C++的虚函数表和模板的编译链接中,起着非常重要的作用。)
支持Linux/OSX,不支持windows
-fdata-sections
为源文件中每个变量启用一个elfsection的生成
示例:
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开启链接优化
编译选项
-fltoOz
-O3-flto
lto为link-timeoptimization,在编译和链接时需要同时开启。编译时,会将各文件写入专有的section,再链接时将它俩视为同一单元进行转换和优化。但有个缺点,会在一定程度上拖慢编译速度
注意:lto编译时可以和-Oz共存,但链接时只能跟O1/O2/O3共存,无法和Oz/Os共存,如果同时开启了,将会报下面的错误:
$clang-Os-fuse-ld=:error:-plugin-opt=Os:numberexpected,butgot's'clang-9:error:linkercommandfailedwithexitcode1(use-vtoseeinvocation)$clang-Oz-fuse-ld=: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")if()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")if()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也需要配合一定代码修改:
DerivedTargettarget=getTargetPtr();//dynamic_castBasicTarget*(())-fun();//不可再用static_castBasicTarget*(())-fun();
3.1.5自动删除引入的静态库中的符号
-Wl,--exclude-libs,ALL(Android端)
删除库"ALL"里自动导出的符号(这里ALL替换成不需要的库名,比如--exclude-libslib,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")foreach()if()目前特效在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不要有无效的判断逻辑(ifelse)
可以采用表驱动的方法实现ifelse,减少不必要的代码引用。
3.2.2减少模板展开、宏展开
模板展开非常占据体积,尤其是对于同一种形式的代码,template会扩充为多个不同的类。此时最好把公共的部分提取出来,声明为一个staticmethod。
如下面的绑定变量的方法:
templatetypenameTstaticvoidbindArgs(constDemod,Tfunc){autom=createFun(func);m-mName=(autoi=0;im-getArgc();++i){if(())m-mArgTypes[i].name=[i];}}templatetypenameTstaticvoidbindArgs(constDemod,Tfunc,constVararg1){autom=createFun(func);if(!m)return;_back(arg1);for(autoi=0;im-getArgc();++i){if(())m-mArgTypes[i].name=[i];}}//staticvoidbindArgs(constDemod,Tfunc,constVararg1,constVararg2)//{可修改为:
//bindArgs提取出来staticvoidbindArgs(constDemod,Fun*m){for(autoi=0;im-getArgc();++i){if(())m-mArgTypes[i].name=[i];}}templatetypenameTstaticvoidbindArgs(constDemod,Tfunc){autom=createFun(func);m-mName=;bindArgs(d,m);}templatetypenameTstaticvoidbindArgs(constDemod,Tfunc,constVararg1){autom=createFun(func);if(!m)return;_back(arg1);bindArgs(d,m);}3.2.3避免不必要的stl/std使用
比如,部分回调可以使用函数指针:std::function作为一个class,它的体积成本必然比void*fun这样一个函数指针要来的高;
//usingFunInstantiate=std::functionFunInterface*();//不再使用usingFunInstantiate=FunInterface*(*)();
比如,常量字符串引用时可以采用constchar*类型,避免编译器调用隐式拷贝构造;
//voidDemoClass::fun(conststd::stringname,constDemoPtrdemoPtr)//不再使用voidDemoClass::fun(constchar*name,constDmoePtrdemoPtr){//}3.2.4头文件不要出现const、static变量的定义
头文件中const/static型的变量,会被引入至对应的cpp文件,相当于每一份.o都引入了一长串常量字符串。
3.2.5不要出现大的数组
大的数组会占用数组大小的体积。
3.2.6减少不必要的虚基类/虚函数
//classChild:virtualpublicParent//不再使用classChild:publicParent{//}4包体积监测工具4.1为什么要做包体积监测工具
抖音每个版本都会有非常多的新能力更新换代,每次更新每个需求均会导致包体积的变更。为了能更好的监测包体积的变化、确认包体积增长的原因,提升ROI,引入包体积监测工具,更直观的确认包体积增长原因,拦截异常增长,输出每个每个需求带来的包体积增长大小、包体积增长原因,及时给出包体积告警、定位异常增量case,减缓包体积增长,推动业务优化。

以特效侧libeffect_为例,对.so文件进行组件单元、源文件分析,截取部分输出结果:
%2.25Mi[]7.2%1.58Mi[]7.2%1.57%877Ki[]2.0%445Ki[]1.9%418Ki[_except_table]1.0%213Kibase/%149Kibef_info_sticker_%140Kibase/%138KiRuntime/Engine/Foundation/
利用上述工具,即可较为清晰的定位各文件带来的包体积增长。
4.2.1包体积监控工具工作流程
包体积监测工具是当前特效需求上车前必过的一环。所有需求在MR(mergerequest)提出、CI打包完成后都会经过包体积的检查,仅包体积增量符合预期的需求允许跟版合入,所有包体积增量与需求一一对应,记录在案。

4.2.2包体积监测工具的分析能力
包体积分析工具支持单个文件分析和版本迭代对比分析。


4.2.3包体积数据记录本
所有需求的包体积增量将记录在包体积记录本中:当服务收到需求事件时,将调用bits/meego接口,请求需求信息和包大小预设exp_pack_size增量写入mr_pkg_size表;等到本地出包完成后,实际的包大小增量real_pack_size将被记录入mr_pkg_size表,并将预期值与实际增量进行对比。

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