包体积主要指的是应用安装包大小的体积,比如 App Store 里的安装包显示的安装大小。
随着应用的能力更新迭代,应用安装包体积将逐步增大,用户下载应用消耗流量产生资费进一步增长,用户下载意愿会相对下降;另一方面,随着包体积增大,安装应用的时间会相对变长,影响用户使用感受;对于ROM较小的低端手机,应用解压后内存占用更大,部分手机管家会提示内存不足提示卸载,直接影响用户使用。
抖音目前由多条业务线组成,每条业务线都类似中台的角色,特效中台是抖音其中一环;目前,特效由 effect 和 lab 聚合为EffectSDK,作为一条独立业务线结算包体积在抖音中的占比。
EffectSDK 的包体积由两方面组成:二进制文件(即可执行文件)、其他资源文件(图片、配置文件等)。二进制文件主要是由代码生成的可执行文件,资源文件指代的如内置的模型文件、素材文件、配置文件等。
作为中台,特效 EffectSDK 中二进制代码占用了绝大多数体积。与抖音、头条等应用做包体积优化思路不同,特效在资源压缩等部分能做得比较少;由于特效是作为中台对抖音进行业务支持,通过库的形式提供特效能力,在无用资源删除、无用代码去除、代码优化上有较大空间。因此,特效侧性能优化主要侧重于在支持多功能的基础上尽量减小包体积,提升代码质量,实现代码效率与代码体积的平衡。
特效侧在抖音里的能力由 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;
}
在了解了基础的包体积组成后,我们可以针对性的对编译选项、代码进行调整,以优化包体积。
iOS/Android 均可以通过优化编译选项来优化代码体积。整理了常用的一些。
编译选项
用-Oz
替代-Os
示例:
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Oz")
编译选项
-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")
编译选项
-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)
示例:
$ 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)
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()
编译选项
-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();
链接选项
-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 端均采用了这个选项。
-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
动态链接 libstdc++ 库,避免增大库文件。
一句话总结:代码量越少,包体积越小,从经验来看100行代码大概占用1~5K体积;超出这个行/体积 比,代码肯定有问题。
可以采用表驱动的方法实现 if else ,减少不必要的代码引用。
模板展开非常占据体积,尤其是对于同一种形式的代码,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);
}
比如,部分回调可以使用函数指针: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)
{
//...
}
头文件中 const / static 型的变量,会被引入至对应的 cpp 文件,相当于每一份.o 都引入了一长串常量字符串。
大的数组会占用数组大小的体积。
// class Child : virtual public Parent // 不再使用
class Child : public Parent
{
//...
}
抖音每个版本都会有非常多的新能力更新换代,每次更新每个需求均会导致包体积的变更。为了能更好的监测包体积的变化、确认包体积增长的原因,提升 ROI ,引入包体积监测工具,更直观的确认包体积增长原因,拦截异常增长,输出每个每个需求带来的包体积增长大小、包体积增长原因,及时给出包体积告警、定位异常增量 case ,减缓包体积增长,推动业务优化。
特效侧目前使用的包体积监测工具来源于 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
...
利用上述工具,即可较为清晰的定位各文件带来的包体积增长。
包体积监测工具是当前特效需求上车前必过的一环。所有需求在 MR(merge request)提出、CI 打包完成后都会经过包体积的检查,仅包体积增量符合预期的需求允许跟版合入,所有包体积增量与需求一一对应,记录在案。
包体积分析工具支持单个文件分析和版本迭代对比分析。
对于单文件分析,由于特效侧主要通过 .so 文件进行交付,在每个 MR 打包完成后,工具将自动获取对应的 .so 文件和 .so.symbol 文件后,对库文件的包体积组成、包体积来源进行分析,输出所有方法函数、节区(section)、编译单元(xxx.cpp)带来的包体积大小,确认大小后通过关键字匹配确认包体积的增量来源模块,给出最后的各模块单元、编译单元的包体积 profile 。
另一方面,由于特效侧能力总是通过需求更新迭代的,每次有实质性的需求提交时,将会对比上一版本与当前版本的包体积差异,做好每个版本需求带来的增量来源记录。当版本比对结果带来的增量超过预期值时,将调起通讯 api ,将包体积超标信息发出进行报警。
所有需求的包体积增量将记录在包体积记录本中:当服务收到需求事件时,将调用 bits/meego 接口,请求需求信息和包大小预设 exp_pack_size 增量写入 mr_pkg_size 表;等到本地出包完成后,实际的包大小增量 real_pack_size 将被记录入 mr_pkg_size 表,并将预期值与实际增量进行对比。
最终,所有的包体积增量与历史的需求增量来源被记录在案,并通过表查询接口,在网页端可根据需求名 / 时间段 / 分支名 / commit id 等条件按图索骥,确认包体积增长来源。
经过上述代码体积优化积累、实时体积监控、需求增量落实到人三位一体,控制特效侧包体积有序增长,提升代码效能。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/3ZrRW0iRSBjqs0ix4FWzZA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。