爱美容
当前位置: 首页 美容百科

界面优化4个要点(特效侧用户体验优化实战)

时间:2023-05-24 作者: 小编 阅读量: 4 栏目名: 美容百科

二进制文件主要是由代码生成的可执行文件,资源文件指代的如内置的模型文件、素材文件、配置文件等。作为中台,特效EffectSDK中二进制代码占用了绝大多数体积。因此,特效侧性能优化主要侧重于在支持多功能的基础上尽量减小包体积,提升代码质量,实现代码效率与代码体积的平衡。说到底,特效侧最终的包体积由section和headers的大小共同决定。

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 -h libeffect_sdk.soELF Header:Magic:7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00Class:ELF64Data:2's complement, little endianVersion:1 (current)OS/ABI:UNIX - System VABI Version:0Type:DYN (Shared object file)Machine:AArch64Version:0x1Entry point address:0x0Start of program headers:64 (bytes into file)Start of section headers:22954168 (bytes into file)FLAGS:0x0Size of this header:64 (bytes)Size of program headers: 56 (bytes)number of program headers:8Size of section headers: 64 (bytes)Number of section headers:29Section header string table index: 28$ greadelf -S libeffect_sdk.soThere are 29 section headers, starting at offset 0x15e40b8:Section Headers:[Nr] NameTypeAddress OffsetSizeEntSizeFlagsLinkInfoAlign[ 0]NULL00000000000000000000000000000000000000000000000000000000 000[ 1] .note.androi[...] NOTE00000000000002000000020000000000000000980000000000000000A004[ 2] .note.gnu.bu[...] NOTE00000000000002980000029800000000000000240000000000000000A004[ 3] .dynsym DYNSYM 00000000000002c0000002c000000000000107e80000000000000018A418[ 4] .dynstr STRTAB 0000000000010aa800010aa8000000000001b0f90000000000000000A001[ 5] .gnu.hashGNU_HASH000000000002bba80002bba8000000000000347c0000000000000000A308[ 6] .hashHASH000000000002f0280002f0280000000000004c180000000000000004A308 ... ...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 sectionint gUninitedVar;//-- .bss section void func(int i){printf("%d\n", i); //-- .text section}int main(void){static int sVar = 23; //-- .data sectionstatic int sVar1; //-- .bss sectionint a = 1;int b;func(sVarsVar1ab); //-- .text sectionreturn 0;}

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.cld.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.cld.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.namefor (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.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.57MiBindings.cpp3.9%877Ki[section .data.rel.ro]2.0%445Ki[section .text]1.9%418Ki[section .gcc_except_table]1.0%213Kibase/EffectManager.cpp0.7%149Kibef_info_sticker_api.cpp0.6%140Kibase/RenderManager.cpp0.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 总结

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

    推荐阅读
  • 巴西暴雨对大豆的影响(农产品的收割和种植)

    巴西暴雨对大豆的影响作为世界重要的大豆产区,巴西马托格罗索州遭受暴雨袭击,大豆产量损失严重,影响到巴西国内大豆及大豆油市场。巴西马托格罗索州的农畜官表示,受恶劣天气影响,马托格罗索州连续普降大雨达一周之久。马托格罗索州的大豆产量占全国总产量的三成,此次大豆受损数量将近50万吨,这对巴西国内大豆市场和大豆油市场的影响不可小觑。

  • 梦见小白兔子是什么意思周公解梦 梦见小白兔子是什么意思周公解梦女

    梦见小白兔子,精神直觉上的认识和跳跃。小兔子被放出来表示精神重获自由,并且对过去的婚姻有了本质的认识。梦见手抱小白兔子、捉小白兔子,代表你会有客人到访,未有爱侣的将会觅得一个终生伴侣。梦见小白兔子梦见白兔,必有贵人所接。《敦煌本梦书》梦食白兔,吉。疾病者梦此,不用药亦有转机;怀孕者梦此,生女大贵,凡事皆吉。《敦煌本梦书》梦见兔子是吉兆,梦见养兔预示着你会远离灾祸,幸福平安。

  • 有多少人叫赵羽(赵羽迎融媒体时代)

    这一媒体的报道使“报纸消亡论”广为传播,继而引发众声喧哗、各抒己见。2008年10月,美国颇具影响力的报纸《基督教科学箴言报》宣布,将在2009年3月停止发行印刷报刊。实际上,菲利普·迈耶的“报纸消亡论”,不过是指出了纸媒的减少和衰落,并没有全盘否定纸媒的存在。

  • 二本选什么专业比较好就业(刚过二本分数线可以选择的专业)

    目前来说,专科应该是最难就业的,不仅要受到专业的限制还要受到学历的限制。而且本科生升职都是非常有利的,发展也是相当不错的。而且,这个专业的分数并不是特别高,有很多院校基本上是压线就能上。稳妥才是最重要的。对于这样的学生,只有一个原则,能走本科绝不走专科。这个概率只有一半。这对于学生而言,就是吃了大亏了。要知道,事物都是有概率的,就像复读一样,概率就是一半一半。高考的目的就是为了考大学,好就业。

  • 描写起床的优美句子(描写早晨的优美段落)

    睡眼惺忪迎接黎明,刷牙洗脸手要轻。阳光给我好心情,享受清新好空气。一杯热饮好甜蜜,家有人伴好温馨。新的一天,新期待,美好生活常伴您!早晨,美丽的、雄赳赳的和气昂昂的公鸡用激扬的叫声报晓着黎明的到来,此起彼伏地歌唱着。乡村慢慢地睁开睡眼惺忪的眼睛,在一阵舒服中醒来。一片无色的光线透过心爱的窗帘,照射在脸上。我快速地起床后,拉开帘布,推开窗户,浅吟低唱的微风轻轻地吹进,伴随着一股清鲜的气味扑鼻而来。

  • 我的世界里生成的村民怎么不干活(我的世界都有什么模式)

    我的世界里生成的村民怎么不干活?以下内容大家不妨参考一二希望能帮到您!我的世界里生成的村民怎么不干活村庄里面的村民,是有耕种范围的,自动农田需要远离村庄64格才能正常干活。玩家们可以在游戏中自由选择模式,在各种模式中体验不一样的有趣玩法,在生存模式中享受打怪、冒险等多种乐趣,在创造模式下享受当创世神的乐趣。

  • 九江德安春节期间黄码人员核酸检测点

    健康码黄码人员需接受必要的核酸检测、健康监测等措施。请提前报告社区,并在做好防护后再前往以下黄码人员核酸采样点。提交成功后,县区解码专员会在24小时内在线审核,实现网上转码全闭环操作。如还有其他需要解答的问题,请与属地指挥部或社区工作人员联系。

  • 四成网售儿童内裤不达标(网售4成儿童内裤抽检不合格)

    网售4成儿童内裤抽检不合格继不合格童鞋被曝光,儿童内裤也出了问题!前段时间,广西消委会委托广西产品质量检验研究院,对儿童内裤进行了测评结果发现:有四成网售儿童内裤不合格!!!涉及多个品牌,谨慎给孩子穿在这次测评中,消委会的工作。

  • 春节搞笑拜年祝福语(盘点春节搞笑拜年祝福语)

    春节搞笑拜年祝福语事业百尺竿头,爱情甜在心头,挣钱富得流油,祝你:好事连连,好梦圆圆!祝我最亲爱的朋友在新年里高举发财大旗,紧密团结在以人民币为核心的钱中央周围,坚持潇洒基本原则,把握艳遇、与钱俱进,把幸福的道理走到底!甭管大海再多水,大山再多伟,蜘蛛再多腿,辣椒再辣嘴,总之春节你最美!新年将至,为了地球环境与资源,请减少购买传统纸制贺卡,你可在大面值人民币上用铅笔填上贺词,寄给我!

  • 炖牛肉的方法(牛肉的炖煮方法)

    炖牛肉的方法炖牛肉时,应使用热水,不可用冷水,因为热水可以使牛肉表面蛋白质迅速凝固,防止氨基酸流失,保持肉味鲜美。旺火烧开后,揭开盖子炖20分钟以去除异味,然后加盖,改用微火,使汤面上浮油保持一定温度,以起到焖的作用。将少量茶叶用纱布包好,放入炉中与牛肉同炖煮,肉不仅熟得快,而且味道清香。加些酒或醋炖牛肉,可使肉更软嫩。放几个山楂或几片萝卜,令牛肉熟得快,而且可以驱除异味。