一、前言
百度APP包体积经过一期优化,如无用资源整理,无用类下线,Xcode编译相关优化,体积现已有了显着的削减。可是优化后APP包体积在iPhone11上仍有350M的空间占用。与此一起百度APP作为百度的旗舰APP,事务迭代十分多且敏捷,体积优化和防劣化仍然是当时阶段的一个中心任务。因而百度APP敞开了粒度更小,修正危险更高的无用办法整理相关作业。希望经过无用办法整理,有用降低百度APP的包体积,一起删去项目中的无用办法,冗余代码,进步代码的整洁度。
百度APP iOS端包体积优化实践系列文章回忆:
-
《百度APP iOS端包体积50M优化实践(一)总览》
-
《百度APP iOS端包体积50M优化实践(二) 图片优化》
-
《百度APP iOS端包体积50M优化实践(三) 资源优化》
-
《百度APP iOS端包体积50M优化实践(四) 代码优化》
-
《百度APP iOS端包体积50M优化实践(五) HEIC图片和无用类优化实践》
二、计划调研
针对无用办法整理,调研了各家厂商现在已发布的计划,主流计划根据Mach-O + LinkMap文件的剖析,可是首要存在以下问题:
1.准确度低
2.针对体系办法需求手动过滤
3.针对load、initilize、attribute 相关调用无法辨认
4.针对string反射调用无法辨认,Target-Action 注册,Observer注册办法等无法辨认
5.杂乱语法场景下无法辨认,如承继链中的办法调用,子类完成父类办法等场景
6.体系告诉等场景
由于现在已发布计划存在如上缺乏,一起由于下线代码敏感度十分高,相关事务都很慎重。因而推动相关无用办法整理,辨认准确度将十分重要,直接联系到相关事务下线无用代码的积极性,因而弃用了上述计划。
三、计划挑选
针对第二部分计划缺乏之处进行剖析,能够看到其准确度低的中心问题是,针对产品进行剖析,拿不到一切需求的信息,或许说还没有发现有用的手法去获取所希望取得的信息。而想要处理上面说到的问题,最佳途径便是获取到尽或许多的代码信息。已然从产品回溯不到所需求的,那么就能够考虑从源头也便是源码层面找到咱们所需求的详细信息。
源码肯定包括了一切的信息,可是针对源码怎么剖析呢,首要有以下三种:
- 经过脚本直接剖析源码
需求匹配源码的一切语法规矩,才能够针对源码进行有用的剖析,相当于写一个源码解析器,所以这个计划抛弃
- 经过脚本直接剖析AST(笼统语法树)
编译进程中发生的笼统语法树(AST)包括了需求的一切信息,而且clang也供给了指令行,运用该指令行能够直接获取到AST数据。可是clang 指令获取AST数据是以单个类为维度的,类与类之间的联系很难获取到,如承继联系,分类和主类的联系是无法获取的,所以这个计划相同抛弃
- 经过libtooling 和 Swift Compiler自建编译套件剖析AST (Swift相关会在下一篇文章中介绍)
已然经过clang指令生成的AST产品剖析仍然不能满足需求,那么直接介入编译进程,从编译内部生成AST进程中获取需求的信息,终究这个计划被选用。经过libtooling 和 Swift Compiler自建编译套件针对AST进行剖析,获取所需求的一切信息。
四、计划规划
如上所述百度APP终究选用了libtooling 和 Swift Compiler 静态剖析计划,那么下面就从原理和完成层面别离进行论述。
4.1 编译流程简介
4.1.1 Xcode编译整体结构
本节先简略聊一下编译器的结构,编译流程,和静态剖析是什么?
△图 4-1
如图4-1 所示 LLVM 选用如上三段结构(Three Phase Design),别离是编译前端(Frontend),编译优化模块,编译器后端(Backend)。那么这三段结构怎么对应到Xcode呢,如图4-2所示:
△图 4-2
日常运用Xcode编译时,Xcode调用了两个编译器前端,别离为Clang 和 Swift,经过两个编译器前端构建出通用的编译产品,然后一致经过LLVM后端编译器进行目标文件生成。
经过Xcode的编译log,能够看到针对Objective-C,C, C++ 运用了clang进行编译,针对上述三种不同语言别离用不同编译参数控制:
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang
针对swift 文件则选用了swift编译器进行了编译:
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-frontend
针对这两个可履行文件咱们能够自行解包Xcode,进行指令行调用,也能够经过其 –help指令检查其支持哪些编译参数或许功用。Xcode 内部编译器实践上是苹果对LLVM 和 Swift 开源版别的定制化版别, 和开源版别有必定的差异性。
4.1.2 Clang 和 Swift 编译流程
如下图所示Clang 和 Swift 前端编译流程,能够看到Swift 编译处理流程多了SIL部分,实践里边还有一个SIL Guaranteed Transformations,当然SIL部分不是要点。从图4-3中能够看到Clang 和 Swift compiler 都会生成AST 且发现AST中包括了咱们需求的绝大部分信息,而且Clang 和 Swift Compiler 也暴露了相关获取AST信息的接口,那么剩余的作业只有四点:
1.建立编译套件工程,保证它正常run起来
2.获取AST,而且根据Objective-C 或许 C,C++的语法特性获取所需求的数据
3.针对获取的数据进行事务剖析处理
4.开源版别LLVM和Xcode实践运用版别具有必定差异性,因而部分编译相关内容需求进行相关适配
△图 4-3
4.2 整体计划规划
针对一门程序语言的运用而言,如图4-4所示,包括两个层面,一个层面是声明,另一个层面是调用。声明类,协议,特点,办法,函数等等,一起声明的内容是为了被运用,所以相同声明的内容皆可调用,只不过是内部调用仍是公开调用问题。从技能视点而言,声明的一切内容 减去 被调用的声明内容,剩余的便是未被调用的内容,也便是咱们需求的 无用办法。当然技能层面的判别终究仍是要进行事务断定,由于有的归于根底能力对外供给,至于是否要删去则需求进一步讨论。本文首要讨论技能层面问题。
△图 4-4
从clang源码中能够知道声明和调用别离对应LLVM源码中的基类Decl 和 Expr,整体技能计划如下图 4-5所示,针对无用办法分为处理分为四层:
1.Basic 层:组装编译东西所需的编译参数 + 进行语法规矩匹配
2.Transformer层:针对语法规矩匹配数据进行转换,转换通用型数据格局
3.通用数据层:经过Transformer层产出的数据进行分类存储,所存储数据包括了代码的一切数据,如针对特点,办法,协议等数据均进行了分类存储
4.事务使用层:针对通用数据层产出的存储数据进行事务剖析即可
△图 4-5
4.3 详细计划完成
4.3.1 Objective-C 编译东西建立
编译东西的呈现方式是一个类似Xcode自带clang的可履行文件,如图4-6 红框所示内容。
/Users/UserName/Documents/XcodeEdition/Xcode14.2/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang
△图 4-6
简略来说经过源码构建的编译东西具有Xcode clang 的部分功用,利用其编译进程中发生的AST目标进行笼统语法树剖析,获取到所需求的编程语言的一切语法信息。
4.3.1.1 LLVM 源码构建
编译东西的建立需求依靠LLVM供给的静态库或动态库,这些库经过自己构建LLVM源码来取得。能够从github获取LLVM源码路径,进入LLVM github界面后有或许会困惑需求构建哪个分支或许tag的代码呢,哪个版别和Xcode运用的clang是对应的?现在Xcode的版别是 14.2 或许 14.3 ,运用指令 clang –version 能够看到Xcode用到的是clang 14,因而构建了release/14.x(没有找到对应联系,推理得出),构建成功后履行构建的clang –version 会发现开源版别clang 和 Xcode的小版别号是不一样的,这是由于Xcode 用的clang 苹果会根据开源代码进行定制,这从Xcode中clang 的依靠库或头文件数量。别的从编译log也能够看到,Xcode clang支持的部分参数,开源clang是不支持的。虽然苹果有一些定制,可是整体影响有限。因而也不用过于介意小版别号是否一致。(开始验证了一下构建最新的release/16.x clang16 也能够)。
△图 4-7
详细构建指令首要分两种,一个是Ninja 构建方式,一个是Xcode方式,需求Xcode调试源码能够挑选Xcode模式,可是终究集成到编译东西中的静态库,必定要构建成Release模式,这样东西体积会降到最低,一些警告类异常也会被屏蔽掉。能够参照LLVM 开源库中的start guide 构建进程进行构建,其间触及的组装指令能够自行拼接也能够用下面的指令:
构建进程
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
mkdir build (这个build文件夹能够自行命名,不固定。针对不同目标能够创立不同文件夹进行不同构建,如 mkdir ninjaBuild 或 mkdir xcodeBuild)
cd build (or cd xcodeBuild)
cmake -G "Ninja" -DCMAKE_BUILD_TYPE=Release ../llvm
cmake --build .
编译Xcode版别,Ninja替换为Xcode即可。
4.3.1.2 工程建立
LLVM供给了两种东西 libclang 和 libtooling,百度APP选用的是 libtooling,其异同点如下所示:
-
libclang:(网络资料,未实测)
1.供给稳定的 C 接口,具有遍历语法树,获取 Token,代码补全等能力。
2.接口稳定,clang 版别更新对齐影响不大
3.libclang 不能获取到 AST 的一切信息
-
libtooling:(实测)
1.供给 C++ 接口,产出的东西不依靠于编译器,可作为独立指令运用
2.接口不稳定,AST 有晋级需求更新相关依靠库
3.libtooling 能够取得 AST 的一切信息
终究挑选 libtooling 方式,中心原因便是 libtooling 能够获取 AST 的一切信息,一起能够不依靠于Xcode 独立运行。工程的建立本身并不杂乱,仍是归于API 运用层面,能够直接参照 libtooling的官方文档。
△图 4-8
整体代码流程如图 4-8所示,首要中心点是五个部分:
-
参数解析
-
创立 ClangTool 参照LLVM源码 ClangTooling -> Tooling.h Line309
-
创立 ASTFrontendAction,用于获取 AST 数据,创立 ASTConsumer 和 进行 ASTMatcher 绑定
-
针对 ASTMatcher 匹配项进行各语法规矩匹配
-
根据匹配数据进行数据过滤及事务处理
4.3.1.3 数据存储结构规划
数据存储结构选用 json 格局,以下为根底数据格局示例,能够根据实践需求拓宽:
"objc(协议or类)@类名(类办法or实例办法)@办法称号":{
"identifier":"objc(协议or类)@类名(类办法or实例办法)@办法称号",
"isInstance":true,
"kind":16,
"location":{
"col":36,
"filename":"文件称号",
"line":147
},
"name":"办法称号",
"paramters":"参数",
"returnType":"返回值类型",
"sourceCode":"源码"
}
{"declaration":{"identifier":"objc(协议or类)@类名(类办法or实例办法)@办法称号","isInstance":true,"kind":16,"location":{ "col":列数,"filename":"声明地点类名", "line":行数 },"name":"办法称号","paramters":"参数称号","returnType":"返回值类型","sourceCode":"源代码" },"kind":1,"location":{"col":5,"filename":"当时地点文件名","line":15 }}
五、遇到的问题及处理计划
1. 特点调用辨认问题
针对 Objective-C 的特点,在编译后对应两个办法 get 和 set 一个是 ivar,调用方有或许只调用 get 或许 set 或许 ivar,所以当只发生一种调用时,就算这个特点被调用,当时特点不归于无用办法。需求在结果中把别的两个办法剥离。
2. 提取办法内容时相同需求对头文件进行提取
办法的完成不用定只在.m 文件中,如C++的头文件是能够进行办法完成的,Objective-C 的.h 文件 经过 inline 完成一些办法,在语法上也是可行的。所以进行办法提取时分重视完成文件,一起也要重视头文件。
3. 针对承继问题
子类完成父类办法等场景,在辨认办法时,全部回溯其父类,以其父类称号作为 上文数据结构中 identifier 中类名部分,这样一切的办法都能够和其声明类匹配。
4. 过滤体系办法调用
LLVM供给了接口判别当时办法是否归于体系类。
5. 过滤事务类完成体系办法问题
针对当时类中一切的办法均在当时类 和 回溯其承继链条中的父类, 别离判别其是否归于体系办法,假如归于体系办规律直接过滤掉。
6. 针对协议办法的完成,现在还没有有用手法辨认,当时计划是直接过滤掉协议办法,一切协议办法均视为现已调用
在提取办法时,判别当时interface 遵从了哪些协议,遍历协议中的办法,判别其是否为协议办法,是则标记为已调用。
7. 子类完成父类协议问题
回溯当时类的承继链条,在承继链条中判别遍历其所遵从的协议,判别其是否为协议办法。
8. 正常事务完成协议,应该清晰标示当时类遵从了协议 如 interface ,可是实践场景中有许多代码在完成协议时并没有标示conformprotocol 这样就对协议办法的判别发生影响,如 6.7计划均失效了
假如组件中少数这种问题,当推动相关方修正此问题,需求清晰遵从协议。可是假如有的组件这种场景较多,短期不会修正一切,那么就需求进行临时性适配。针对这类组件搜集其当时组件所声明的协议的一切协议办法,用搜集的协议办法和当时组件提取的一切声明做差集,存在误伤的或许,但结果是相信的(组件仅仅一个维度,也能够针对其关联组件进行相关处理,由于有时他完成的组件不用定在当时组件内,这就需求当时组件的依靠联系了)。
无用办法case许多,列举部分供咱们参阅。
六、总结
这项技能实践上在百度APP早现已使用,由于笔者之前负责百度APP的接口改变审阅,组件完整性校验,隐私合规调用链剖析等均是依靠于此项技能,无用办法辨认仅仅笔者在做体积优化时想到的其功用的一个延展。当然如上描绘的技能问题,细节处理无用办法明显更细腻,case更多。后续文章会针对Swift无用办法剖析,接口改变审阅,组件完整性校验,隐私合规调用链剖析等逐个作出介绍。
** ——END——**
参阅资料:
[1]libclang:clang.llvm.org/doxygen/gro…
[2]libtooling 官方文档:clang.llvm.org/docs/LibToo…
[3]LLVM源码:github.com/llvm/llvm-p…
引荐阅读:
根据异常上线场景的实时拦截与问题分发战略
极致优化 SSD 并行读调度
AI文本创作在百度App发文的实践
DeeTune:根据 eBPF 的百度网络框架规划与使用
百度自研高性能ANN检索引擎,开源了